diff --git a/.gitignore b/.gitignore index 158b43a..5cbdb97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ __pycache__/ backend/build/ backend/dist/ +venv/ +frontend/build/ +frontend/dist/ diff --git a/backend/app.py b/backend/app.py index 8c652a8..406e97d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -113,6 +113,45 @@ def claim_work_order(): }), 500 +@app.route('/api/work-orders/unclaim', methods=['POST']) +def unclaim_work_order(): + """退领工单""" + data = request.get_json() + trace_id = data.get('trace_id') + process_id = data.get('process_id') + + if not trace_id or not process_id: + return jsonify({"success": False, "message": "追溯号和工序号不能为空"}), 400 + + if db.unclaim_work_order(trace_id, process_id): + return jsonify({"success": True, "message": "退领成功"}) + else: + return jsonify({"success": False, "message": "退领失败"}), 500 + + +@app.route('/api/work-orders/delete', methods=['POST']) +def delete_work_order(): + """删除工单""" + data = request.get_json() + trace_id = data.get('trace_id') + process_id = data.get('process_id') + + if not trace_id or not process_id: + return jsonify({"success": False, "message": "追溯号和工序号不能为空"}), 400 + + if db.delete_work_order(trace_id, process_id): + return jsonify({"success": True, "message": "删除成功"}) + else: + return jsonify({"success": False, "message": "删除失败"}), 500 + + +@app.route('/api/work-orders/claimed', methods=['GET']) +def get_claimed_orders(): + """获取所有已认领的工单""" + orders = db.get_all_claimed_orders() + return jsonify({"success": True, "data": orders}) + + @app.route('/api/work-orders/submit', methods=['POST']) def submit_work_order(): """ diff --git a/backend/database.py b/backend/database.py index 3e90c58..3a83fbf 100644 --- a/backend/database.py +++ b/backend/database.py @@ -24,8 +24,9 @@ class Database: def get_connection(self): """获取数据库连接""" - conn = sqlite3.connect(self.db_path) - conn.row_factory = sqlite3.Row # 使查询结果可以按列名访问 + conn = sqlite3.connect(self.db_path, timeout=1.0, check_same_thread=False) + conn.row_factory = sqlite3.Row + conn.execute('PRAGMA journal_mode=WAL') return conn def init_database(self): @@ -352,6 +353,19 @@ class Database: print(f"查询认领工单失败: {e}") return None + def get_all_claimed_orders(self) -> List[Dict]: + """获取所有已认领的工单""" + try: + conn = self.get_connection() + cursor = conn.cursor() + cursor.execute('SELECT * FROM claimed_orders WHERE status = "claimed" ORDER BY claimed_at DESC') + rows = cursor.fetchall() + conn.close() + return [dict(row) for row in rows] + except Exception as e: + print(f"获取已认领工单失败: {e}") + return [] + def claim_work_order(self, trace_id: str, process_id: str, operator: str) -> bool: """ 认领工单 @@ -411,7 +425,25 @@ class Database: print(f"释放工单失败: {e}") return False - def submit_work_order(self, trace_id: str, process_id: str, bolts: List[Dict], + def unclaim_work_order(self, trace_id: str, process_id: str) -> bool: + """退领工单""" + return self.release_work_order(trace_id, process_id) + + def delete_work_order(self, trace_id: str, process_id: str) -> bool: + """删除工单""" + try: + conn = self.get_connection() + cursor = conn.cursor() + cursor.execute('DELETE FROM work_orders WHERE trace_id = ? AND process_id = ?', (trace_id, process_id)) + cursor.execute('DELETE FROM claimed_orders WHERE trace_id = ? AND process_id = ?', (trace_id, process_id)) + conn.commit() + conn.close() + return True + except Exception as e: + print(f"删除工单失败: {e}") + return False + + def submit_work_order(self, trace_id: str, process_id: str, bolts: List[Dict], device_sn: str = None, device_name: str = None) -> bool: """ 提交工单结果 diff --git a/backend/wrench.db b/backend/wrench.db index c3ceff7..e0998d7 100644 Binary files a/backend/wrench.db and b/backend/wrench.db differ diff --git a/config.json b/config.json index a602df5..d0badba 100644 --- a/config.json +++ b/config.json @@ -4,7 +4,7 @@ "description": "后端API服务地址,例如:http://localhost:5000 或 http://192.168.1.100:5000" }, "wrench": { - "host": "127.0.0.1", + "host": "192.168.110.122", "port": 7888, "timeout": 30, "address": 1, diff --git a/frontend/admin_panel.py b/frontend/admin_panel.py new file mode 100644 index 0000000..8273d79 --- /dev/null +++ b/frontend/admin_panel.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +管理员面板 - 管理异常工单 +""" + +import tkinter as tk +from tkinter import ttk, messagebox +import requests +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +class AdminPanel: + """管理员面板""" + + def __init__(self, root, api_base_url): + self.root = root + self.root.title("管理员面板 - 工单管理") + self.root.geometry("900x600") + if isinstance(root, tk.Toplevel): + self.root.transient(root.master) + self.api_base_url = api_base_url + self.claimed_orders = [] + + self._create_widgets() + self.load_claimed_orders() + + def _create_widgets(self): + """创建界面""" + # 标题 + title_frame = tk.Frame(self.root, bg="#2c3e50", height=60) + title_frame.pack(fill=tk.X) + title_frame.pack_propagate(False) + + tk.Label(title_frame, text="管理员面板 - 已认领工单管理", font=("微软雅黑", 18, "bold"), fg="white", bg="#2c3e50").pack(pady=15) + + # 工单列表 + list_frame = tk.LabelFrame(self.root, text="已认领工单列表", font=("微软雅黑", 12), padx=10, pady=10) + list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + columns = ("追溯号", "工序号", "工序名称", "操作员", "认领时间", "状态") + self.tree = ttk.Treeview(list_frame, columns=columns, show="headings") + + for col in columns: + self.tree.heading(col, text=col) + self.tree.column(col, width=150, anchor=tk.CENTER) + + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview) + self.tree.configure(yscrollcommand=scrollbar.set) + self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # 按钮 + button_frame = tk.Frame(self.root, padx=10, pady=10) + button_frame.pack(fill=tk.X) + + tk.Button(button_frame, text="刷新列表", font=("微软雅黑", 10), bg="#3498db", fg="white", width=12, command=self.load_claimed_orders).pack(side=tk.LEFT, padx=5) + tk.Button(button_frame, text="退领选中工单", font=("微软雅黑", 10), bg="#e67e22", fg="white", width=12, command=self.unclaim_selected).pack(side=tk.LEFT, padx=5) + tk.Button(button_frame, text="批量退领", font=("微软雅黑", 10), bg="#e74c3c", fg="white", width=12, command=self.unclaim_all).pack(side=tk.LEFT, padx=5) + + def load_claimed_orders(self): + """加载已认领工单""" + try: + response = requests.get(f"{self.api_base_url}/work-orders/claimed", timeout=2) + if response.status_code == 200: + data = response.json() + if data.get("success"): + self.claimed_orders = data.get("data", []) + self.update_list() + except Exception as e: + messagebox.showerror("错误", f"加载失败: {e}") + + def update_list(self): + """更新列表""" + self.tree.delete(*self.tree.get_children()) + for order in self.claimed_orders: + self.tree.insert("", tk.END, values=( + order.get('trace_id'), order.get('process_id'), order.get('process_name', '--'), + order.get('operator', '--'), order.get('claimed_at', '--'), order.get('status', '--') + )) + + def unclaim_selected(self): + """退领选中工单""" + selected = self.tree.selection() + if not selected: + messagebox.showwarning("警告", "请选择要退领的工单") + return + + item = self.tree.item(selected[0]) + values = item['values'] + trace_id, process_id = values[0], values[1] + + if not messagebox.askyesno("确认", f"确定退领工单 {trace_id} - {process_id}?"): + return + + try: + response = requests.post(f"{self.api_base_url}/work-orders/unclaim", json={"trace_id": trace_id, "process_id": process_id}, timeout=2) + if response.status_code == 200 and response.json().get("success"): + messagebox.showinfo("成功", "退领成功") + self.load_claimed_orders() + except Exception as e: + messagebox.showerror("错误", f"退领失败: {e}") + + def unclaim_all(self): + """批量退领所有工单""" + if not self.claimed_orders: + messagebox.showinfo("提示", "没有需要退领的工单") + return + + if not messagebox.askyesno("确认", f"确定退领所有 {len(self.claimed_orders)} 个工单?"): + return + + success_count = 0 + for order in self.claimed_orders: + try: + requests.post(f"{self.api_base_url}/work-orders/unclaim", json={"trace_id": order['trace_id'], "process_id": order['process_id']}, timeout=2) + success_count += 1 + except: + pass + + messagebox.showinfo("完成", f"成功退领 {success_count}/{len(self.claimed_orders)} 个工单") + self.load_claimed_orders() + + +def main(): + import json + api_url = "http://localhost:5000/api" + try: + config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json") + if os.path.exists(config_path): + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + api_url = config.get("api", {}).get("base_url", api_url) + if not api_url.endswith("/api"): + api_url = api_url.rstrip("/") + "/api" + except: + pass + + root = tk.Tk() + AdminPanel(root, api_url) + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/frontend/device_manager.py b/frontend/device_manager.py index 91d4d57..147f8f9 100644 --- a/frontend/device_manager.py +++ b/frontend/device_manager.py @@ -195,7 +195,7 @@ class DeviceManagerWindow: def add_device(self): """添加设备""" - DeviceEditDialog(self.window, self.api_base_url, None, self.load_devices) + DeviceEditDialog(self.window, self.api_base_url, None, self.load_devices, self.refresh_callback) def edit_device(self): """编辑设备""" @@ -209,7 +209,7 @@ class DeviceManagerWindow: break if device: - DeviceEditDialog(self.window, self.api_base_url, device, self.load_devices) + DeviceEditDialog(self.window, self.api_base_url, device, self.load_devices, self.refresh_callback) def delete_device(self): """删除设备""" @@ -299,11 +299,12 @@ class DeviceManagerWindow: class DeviceEditDialog: """设备编辑对话框""" - def __init__(self, parent, api_base_url, device=None, callback=None): + def __init__(self, parent, api_base_url, device=None, callback=None, parent_refresh_callback=None): self.parent = parent self.api_base_url = api_base_url self.device = device self.callback = callback + self.parent_refresh_callback = parent_refresh_callback self.dialog = tk.Toplevel(parent) self.dialog.title("添加设备" if not device else "编辑设备") @@ -410,6 +411,8 @@ class DeviceEditDialog: self.dialog.destroy() if self.callback: self.callback() + if self.parent_refresh_callback: + self.parent_refresh_callback() else: messagebox.showerror("错误", data.get('message', '保存失败')) else: diff --git a/frontend/start b/frontend/start new file mode 100644 index 0000000..900edf4 --- /dev/null +++ b/frontend/start @@ -0,0 +1,7 @@ +#!/bin/bash +echo "========================================" +echo "启动管理员面板" +echo "========================================" +echo "" +cd "$(dirname "$0")" +python3 admin_panel.py diff --git a/frontend/start_admin.bat b/frontend/start_admin.bat new file mode 100644 index 0000000..4ba6f6a --- /dev/null +++ b/frontend/start_admin.bat @@ -0,0 +1,9 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo 启动管理员面板 +echo ======================================== +echo. +cd /d %~dp0 +python admin_panel.py +pause diff --git a/frontend/wrench_gui.py b/frontend/wrench_gui.py index ac25385..c407ec9 100644 --- a/frontend/wrench_gui.py +++ b/frontend/wrench_gui.py @@ -199,9 +199,13 @@ class WrenchGUI: col1_frame.grid(row=0, column=0, padx=(0, 20), sticky=tk.W) tk.Label(col1_frame, text="工单操作", font=("微软雅黑", 9, "bold"), fg="#2c3e50").pack(anchor=tk.W, pady=(0, 3)) + + order_buttons_frame = tk.Frame(col1_frame) + order_buttons_frame.pack() + self.claim_button = tk.Button( - col1_frame, - text="认领工单", + order_buttons_frame, + text="认领工单", font=("微软雅黑", 10), bg="#f39c12", fg="white", @@ -210,7 +214,47 @@ class WrenchGUI: cursor="hand2", command=self.claim_work_order ) - self.claim_button.pack() + self.claim_button.pack(side=tk.LEFT, padx=(0, 5)) + + self.unclaim_button = tk.Button( + order_buttons_frame, + text="退领工单", + font=("微软雅黑", 10), + bg="#e67e22", + fg="white", + width=10, + height=1, + cursor="hand2", + state=tk.DISABLED, + command=self.unclaim_work_order + ) + self.unclaim_button.pack(side=tk.LEFT, padx=(0, 5)) + + self.delete_button = tk.Button( + order_buttons_frame, + text="删除工单", + font=("微软雅黑", 10), + bg="#e74c3c", + fg="white", + width=10, + height=1, + cursor="hand2", + command=self.delete_work_order + ) + self.delete_button.pack(side=tk.LEFT, padx=(0, 5)) + + self.admin_button = tk.Button( + order_buttons_frame, + text="管理工单", + font=("微软雅黑", 10), + bg="#9b59b6", + fg="white", + width=10, + height=1, + cursor="hand2", + command=self.open_admin_panel + ) + self.admin_button.pack(side=tk.LEFT) # 第二列:设备选择 col2_frame = tk.Frame(button_frame) @@ -478,7 +522,7 @@ class WrenchGUI: try: url = f"{self.api_base_url}/work-orders" # 不传参数,获取所有可用工单 - response = requests.get(url, timeout=5) + response = requests.get(url, timeout=2) if response.status_code == 200: data = response.json() @@ -584,16 +628,22 @@ class WrenchGUI: while self.is_polling: try: url = f"{self.api_base_url}/work-orders" - # 不传参数,获取所有可用工单 - response = requests.get(url, timeout=5) + response = requests.get(url, timeout=2) if response.status_code == 200: data = response.json() if data.get("success"): orders = data.get("data", []) - # 只在数据变化时更新 self.root.after(0, lambda o=orders: self.update_order_list(o)) - # 更新状态标签 + + # 检查当前认领的工单是否被退领 + if self.work_order: + trace_id = self.work_order.get('trace_id') + process_id = self.work_order.get('process_id') + # 如果当前工单出现在可用列表中,说明被退领了 + if any(o.get('trace_id') == trace_id and o.get('process_id') == process_id for o in orders): + self.root.after(0, self.clear_claimed_order) + if len(orders) > 0: self.root.after(0, lambda: self.poll_status_label.config( text=f"🔄 已找到 {len(orders)} 个可用工单", fg="#27ae60" @@ -627,7 +677,7 @@ class WrenchGUI: "process_id": process_id, "operator": "操作员" # 可以从配置或输入获取 } - response = requests.post(url, json=data, timeout=5) + response = requests.post(url, json=data, timeout=2) if response.status_code == 200: result = response.json() @@ -636,7 +686,14 @@ class WrenchGUI: self.update_work_order_info() self.update_bolt_list() self.claim_button.config(state=tk.DISABLED) + self.unclaim_button.config(state=tk.NORMAL) self.start_button.config(state=tk.NORMAL) + # 从列表中移除已认领的工单 + for item in self.order_tree.get_children(): + values = self.order_tree.item(item)['values'] + if values[0] == trace_id and values[1] == process_id: + self.order_tree.delete(item) + break self.log(f"✅ 成功认领工单: {trace_id} - {process_id}", "SUCCESS") else: messagebox.showerror("错误", result.get("message", "认领失败")) @@ -648,6 +705,89 @@ class WrenchGUI: messagebox.showerror("错误", f"无法连接到后端服务器\n\n错误: {e}") self.log(f"认领失败: {e}", "ERROR") + def unclaim_work_order(self): + """退领工单""" + if not self.work_order: + messagebox.showwarning("警告", "当前没有已认领的工单") + return + + if self.is_running: + messagebox.showwarning("警告", "工单正在执行中,无法退领") + return + + trace_id = self.work_order.get('trace_id') + process_id = self.work_order.get('process_id') + + result = messagebox.askyesno("确认退领", f"确定要退领工单 {trace_id} - {process_id} 吗?") + if not result: + return + + try: + url = f"{self.api_base_url}/work-orders/unclaim" + data = {"trace_id": trace_id, "process_id": process_id} + response = requests.post(url, json=data, timeout=2) + + if response.status_code == 200: + result = response.json() + if result.get("success"): + self.work_order = None + self.update_work_order_info() + self.update_bolt_list() + self.claim_button.config(state=tk.NORMAL) + self.unclaim_button.config(state=tk.DISABLED) + self.start_button.config(state=tk.DISABLED) + self.log(f"✅ 成功退领工单: {trace_id} - {process_id}", "SUCCESS") + self.query_work_orders() + else: + messagebox.showerror("错误", result.get("message", "退领失败")) + else: + messagebox.showerror("错误", f"退领失败: HTTP {response.status_code}") + except requests.exceptions.RequestException as e: + messagebox.showerror("错误", f"无法连接到后端服务器\n\n错误: {e}") + + def delete_work_order(self): + """删除工单""" + selected = self.order_tree.selection() + if not selected: + messagebox.showwarning("警告", "请先选择要删除的工单") + return + + item = self.order_tree.item(selected[0]) + values = item['values'] + trace_id = values[0] + process_id = values[1] + + result = messagebox.askyesno("确认删除", f"确定要删除工单 {trace_id} - {process_id} 吗?\n此操作不可恢复!") + if not result: + return + + try: + url = f"{self.api_base_url}/work-orders/delete" + data = {"trace_id": trace_id, "process_id": process_id} + response = requests.post(url, json=data, timeout=2) + + if response.status_code == 200: + result = response.json() + if result.get("success"): + self.log(f"✅ 成功删除工单: {trace_id} - {process_id}", "SUCCESS") + self.query_work_orders() + else: + messagebox.showerror("错误", result.get("message", "删除失败")) + else: + messagebox.showerror("错误", f"删除失败: HTTP {response.status_code}") + except requests.exceptions.RequestException as e: + messagebox.showerror("错误", f"无法连接到后端服务器\n\n错误: {e}") + + def clear_claimed_order(self): + """清空当前认领的工单""" + self.work_order = None + self.update_work_order_info() + self.update_bolt_list() + self.claim_button.config(state=tk.NORMAL) + self.unclaim_button.config(state=tk.DISABLED) + self.start_button.config(state=tk.DISABLED) + self.log("⚠️ 当前工单已被管理员退领", "WARN") + def update_work_order_info(self): """更新工单信息显示""" if self.work_order: @@ -796,6 +936,10 @@ class WrenchGUI: self.log("✅ 已启用远程控制", "SUCCESS") + # 等待扳手初始化完成 + time.sleep(0.5) + self.log("扳手初始化完成,准备开始拧紧") + # 遍历所有螺栓 bolts = self.work_order.get('bolts', []) bolt_results = [] @@ -1119,6 +1263,17 @@ class WrenchGUI: def open_device_manager(self): """打开设备管理窗口""" DeviceManagerWindow(self.root, self.api_base_url, self.load_wrench_devices) + + def open_admin_panel(self): + """打开管理员面板(需要密码)""" + from tkinter import simpledialog + password = simpledialog.askstring("管理员验证", "请输入管理员密码:", show='*') + if password == "123": + from admin_panel import AdminPanel + admin_window = tk.Toplevel(self.root) + AdminPanel(admin_window, self.api_base_url) + elif password is not None: + messagebox.showerror("错误", "密码错误") def main(): @@ -1126,6 +1281,21 @@ def main(): app = WrenchGUI(root) def on_closing(): + # 如果有未完成的工单,提示用户 + if app.work_order: + trace_id = app.work_order.get('trace_id') + process_id = app.work_order.get('process_id') + result = messagebox.askokcancel( + "确认关闭", + f"当前有工单 {trace_id} - {process_id} 未完成\n\n关闭程序将自动退领该工单" + ) + if not result: + return + try: + url = f"{app.api_base_url}/work-orders/unclaim" + requests.post(url, json={"trace_id": trace_id, "process_id": process_id}, timeout=1) + except: + pass app.stop_polling() root.destroy() diff --git a/wrench_controller.py b/wrench_controller.py index 4783494..7c84b7e 100644 --- a/wrench_controller.py +++ b/wrench_controller.py @@ -8,6 +8,7 @@ import socket import struct import json +import time from datetime import datetime from typing import Tuple, Optional from pathlib import Path @@ -439,7 +440,13 @@ class WrenchController: print(f" 报文: {data.hex(' ').upper()}") # 发送命令 - self._send_command(bytes(data)) + if not self._send_command(bytes(data)): + print("❌ 发送设定参数命令失败") + return bytes(data) + + # 等待扳手处理参数(0x10命令无响应,需要短暂延迟) + time.sleep(1) + print("✅ 参数设定命令已发送,等待扳手处理完成") return bytes(data) diff --git a/wrench_gui.py b/wrench_gui.py index 7896fab..bb37d85 100644 --- a/wrench_gui.py +++ b/wrench_gui.py @@ -325,6 +325,10 @@ class WrenchGUI: self.log("✅ 已启用远程控制", "SUCCESS") + # 等待扳手初始化完成 + time.sleep(0.5) + self.log("扳手初始化完成,准备开始拧紧") + # 遍历所有螺栓 bolts = self.work_order['bolts'] for index, bolt in enumerate(bolts):