定制化需求更新

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__/
backend/build/
backend/dist/
venv/
frontend/build/
frontend/dist/

View File

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

View File

@ -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:
"""
提交工单结果

Binary file not shown.

View File

@ -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,

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):
"""添加设备"""
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:

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

View File

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

View File

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