定制化需求更新

main
COT001\李旭光 2026-03-06 15:27:09 +08:00
parent daeef833f8
commit 6d45d12656
12 changed files with 439 additions and 17 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
__pycache__/ __pycache__/
backend/build/ backend/build/
backend/dist/ backend/dist/
venv/
frontend/build/
frontend/dist/

View File

@ -113,6 +113,45 @@ def claim_work_order():
}), 500 }), 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']) @app.route('/api/work-orders/submit', methods=['POST'])
def submit_work_order(): def submit_work_order():
""" """

View File

@ -24,8 +24,9 @@ class Database:
def get_connection(self): def get_connection(self):
"""获取数据库连接""" """获取数据库连接"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path, timeout=1.0, check_same_thread=False)
conn.row_factory = sqlite3.Row # 使查询结果可以按列名访问 conn.row_factory = sqlite3.Row
conn.execute('PRAGMA journal_mode=WAL')
return conn return conn
def init_database(self): def init_database(self):
@ -352,6 +353,19 @@ class Database:
print(f"查询认领工单失败: {e}") print(f"查询认领工单失败: {e}")
return None 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: def claim_work_order(self, trace_id: str, process_id: str, operator: str) -> bool:
""" """
认领工单 认领工单
@ -411,6 +425,24 @@ class Database:
print(f"释放工单失败: {e}") print(f"释放工单失败: {e}")
return False return False
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], def submit_work_order(self, trace_id: str, process_id: str, bolts: List[Dict],
device_sn: str = None, device_name: str = None) -> bool: device_sn: str = None, device_name: str = None) -> bool:
""" """

Binary file not shown.

View File

@ -4,7 +4,7 @@
"description": "后端API服务地址例如http://localhost:5000 或 http://192.168.1.100:5000" "description": "后端API服务地址例如http://localhost:5000 或 http://192.168.1.100:5000"
}, },
"wrench": { "wrench": {
"host": "127.0.0.1", "host": "192.168.110.122",
"port": 7888, "port": 7888,
"timeout": 30, "timeout": 30,
"address": 1, "address": 1,

148
frontend/admin_panel.py Normal file
View File

@ -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()

View File

@ -195,7 +195,7 @@ class DeviceManagerWindow:
def add_device(self): 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): def edit_device(self):
"""编辑设备""" """编辑设备"""
@ -209,7 +209,7 @@ class DeviceManagerWindow:
break break
if device: 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): def delete_device(self):
"""删除设备""" """删除设备"""
@ -299,11 +299,12 @@ class DeviceManagerWindow:
class DeviceEditDialog: 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.parent = parent
self.api_base_url = api_base_url self.api_base_url = api_base_url
self.device = device self.device = device
self.callback = callback self.callback = callback
self.parent_refresh_callback = parent_refresh_callback
self.dialog = tk.Toplevel(parent) self.dialog = tk.Toplevel(parent)
self.dialog.title("添加设备" if not device else "编辑设备") self.dialog.title("添加设备" if not device else "编辑设备")
@ -410,6 +411,8 @@ class DeviceEditDialog:
self.dialog.destroy() self.dialog.destroy()
if self.callback: if self.callback:
self.callback() self.callback()
if self.parent_refresh_callback:
self.parent_refresh_callback()
else: else:
messagebox.showerror("错误", data.get('message', '保存失败')) messagebox.showerror("错误", data.get('message', '保存失败'))
else: else:

7
frontend/start Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
echo "========================================"
echo "启动管理员面板"
echo "========================================"
echo ""
cd "$(dirname "$0")"
python3 admin_panel.py

9
frontend/start_admin.bat Normal file
View File

@ -0,0 +1,9 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 启动管理员面板
echo ========================================
echo.
cd /d %~dp0
python admin_panel.py
pause

View File

@ -199,8 +199,12 @@ class WrenchGUI:
col1_frame.grid(row=0, column=0, padx=(0, 20), sticky=tk.W) 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)) 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( self.claim_button = tk.Button(
col1_frame, order_buttons_frame,
text="认领工单", text="认领工单",
font=("微软雅黑", 10), font=("微软雅黑", 10),
bg="#f39c12", bg="#f39c12",
@ -210,7 +214,47 @@ class WrenchGUI:
cursor="hand2", cursor="hand2",
command=self.claim_work_order 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) col2_frame = tk.Frame(button_frame)
@ -478,7 +522,7 @@ class WrenchGUI:
try: try:
url = f"{self.api_base_url}/work-orders" 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: if response.status_code == 200:
data = response.json() data = response.json()
@ -584,16 +628,22 @@ class WrenchGUI:
while self.is_polling: while self.is_polling:
try: try:
url = f"{self.api_base_url}/work-orders" url = f"{self.api_base_url}/work-orders"
# 不传参数,获取所有可用工单 response = requests.get(url, timeout=2)
response = requests.get(url, timeout=5)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
if data.get("success"): if data.get("success"):
orders = data.get("data", []) orders = data.get("data", [])
# 只在数据变化时更新
self.root.after(0, lambda o=orders: self.update_order_list(o)) 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: if len(orders) > 0:
self.root.after(0, lambda: self.poll_status_label.config( self.root.after(0, lambda: self.poll_status_label.config(
text=f"🔄 已找到 {len(orders)} 个可用工单", fg="#27ae60" text=f"🔄 已找到 {len(orders)} 个可用工单", fg="#27ae60"
@ -627,7 +677,7 @@ class WrenchGUI:
"process_id": process_id, "process_id": process_id,
"operator": "操作员" # 可以从配置或输入获取 "operator": "操作员" # 可以从配置或输入获取
} }
response = requests.post(url, json=data, timeout=5) response = requests.post(url, json=data, timeout=2)
if response.status_code == 200: if response.status_code == 200:
result = response.json() result = response.json()
@ -636,7 +686,14 @@ class WrenchGUI:
self.update_work_order_info() self.update_work_order_info()
self.update_bolt_list() self.update_bolt_list()
self.claim_button.config(state=tk.DISABLED) self.claim_button.config(state=tk.DISABLED)
self.unclaim_button.config(state=tk.NORMAL)
self.start_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") self.log(f"✅ 成功认领工单: {trace_id} - {process_id}", "SUCCESS")
else: else:
messagebox.showerror("错误", result.get("message", "认领失败")) messagebox.showerror("错误", result.get("message", "认领失败"))
@ -648,6 +705,89 @@ class WrenchGUI:
messagebox.showerror("错误", f"无法连接到后端服务器\n\n错误: {e}") messagebox.showerror("错误", f"无法连接到后端服务器\n\n错误: {e}")
self.log(f"认领失败: {e}", "ERROR") 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): def update_work_order_info(self):
"""更新工单信息显示""" """更新工单信息显示"""
if self.work_order: if self.work_order:
@ -796,6 +936,10 @@ class WrenchGUI:
self.log("✅ 已启用远程控制", "SUCCESS") self.log("✅ 已启用远程控制", "SUCCESS")
# 等待扳手初始化完成
time.sleep(0.5)
self.log("扳手初始化完成,准备开始拧紧")
# 遍历所有螺栓 # 遍历所有螺栓
bolts = self.work_order.get('bolts', []) bolts = self.work_order.get('bolts', [])
bolt_results = [] bolt_results = []
@ -1120,12 +1264,38 @@ class WrenchGUI:
"""打开设备管理窗口""" """打开设备管理窗口"""
DeviceManagerWindow(self.root, self.api_base_url, self.load_wrench_devices) 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(): def main():
root = tk.Tk() root = tk.Tk()
app = WrenchGUI(root) app = WrenchGUI(root)
def on_closing(): 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() app.stop_polling()
root.destroy() root.destroy()

View File

@ -8,6 +8,7 @@
import socket import socket
import struct import struct
import json import json
import time
from datetime import datetime from datetime import datetime
from typing import Tuple, Optional from typing import Tuple, Optional
from pathlib import Path from pathlib import Path
@ -439,7 +440,13 @@ class WrenchController:
print(f" 报文: {data.hex(' ').upper()}") 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) return bytes(data)

View File

@ -325,6 +325,10 @@ class WrenchGUI:
self.log("✅ 已启用远程控制", "SUCCESS") self.log("✅ 已启用远程控制", "SUCCESS")
# 等待扳手初始化完成
time.sleep(0.5)
self.log("扳手初始化完成,准备开始拧紧")
# 遍历所有螺栓 # 遍历所有螺栓
bolts = self.work_order['bolts'] bolts = self.work_order['bolts']
for index, bolt in enumerate(bolts): for index, bolt in enumerate(bolts):