查验
This commit is contained in:
+262
-23
@@ -8,10 +8,11 @@ import time
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import requests
|
||||||
from flask import Flask, render_template, jsonify, request, Response, send_from_directory
|
from flask import Flask, render_template, jsonify, request, Response, send_from_directory
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
from config import SERVER_CONFIG, ARM_CONFIG, AGV_CONFIG, UPLOAD_CONFIG, MAP_CONFIG, ARM_CAMERA_CONFIG, CAMERA_CONFIG, DATA_DIR, State
|
from config import SERVER_CONFIG, ARM_CONFIG, AGV_CONFIG, UPLOAD_CONFIG, MAP_CONFIG, ARM_CAMERA_CONFIG, CAMERA_CONFIG, DATA_DIR, State, ZHIJIAN_BASE_URL, ZHIJIAN_AUTH_TOKEN, set_api_mode
|
||||||
from utils.arm_client import ArmClient
|
from utils.arm_client import ArmClient
|
||||||
from utils.agv_controller_ros2 import AGVController
|
from utils.agv_controller_ros2 import AGVController
|
||||||
from utils.qr_scanner import QRScanner
|
from utils.qr_scanner import QRScanner
|
||||||
@@ -54,6 +55,7 @@ class GlobalState:
|
|||||||
self.qr_config = [] # 二维码配置(独立点位列表)
|
self.qr_config = [] # 二维码配置(独立点位列表)
|
||||||
self.navigator = None # Nav2Navigator 实例
|
self.navigator = None # Nav2Navigator 实例
|
||||||
self.current_customs = None # 当前设定的报关单信息
|
self.current_customs = None # 当前设定的报关单信息
|
||||||
|
self.inspection = None # 查验状态 {customs_id, customs_name, items: [{inventoryCode, inventoryName, spec, quantify, inspected}]}
|
||||||
self.error_msg = "" # 错误弹窗消息(waiting_error 状态时)
|
self.error_msg = "" # 错误弹窗消息(waiting_error 状态时)
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
@@ -1213,10 +1215,8 @@ def api_arm_camera_refresh():
|
|||||||
|
|
||||||
@app.route("/api/camera/arm_preview")
|
@app.route("/api/camera/arm_preview")
|
||||||
def api_arm_camera_preview():
|
def api_arm_camera_preview():
|
||||||
"""代理机械臂 MJPEG 视频流,逐帧上下翻转后返回。"""
|
"""代理机械臂 MJPEG 视频流,直透返回(不翻转)。"""
|
||||||
import requests
|
import requests
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
try:
|
try:
|
||||||
upstream = requests.get(
|
upstream = requests.get(
|
||||||
ARM_CAMERA_CONFIG["url"],
|
ARM_CAMERA_CONFIG["url"],
|
||||||
@@ -1241,15 +1241,7 @@ def api_arm_camera_preview():
|
|||||||
if start != -1 and end != -1 and end > start:
|
if start != -1 and end != -1 and end > start:
|
||||||
jpg_data = buf[start:end+2]
|
jpg_data = buf[start:end+2]
|
||||||
buf = buf[end+2:]
|
buf = buf[end+2:]
|
||||||
# 解码 → 上下翻转 → 编码
|
# 直透原始帧(不翻转)
|
||||||
img = cv2.imdecode(np.frombuffer(jpg_data, dtype=np.uint8), cv2.IMREAD_COLOR)
|
|
||||||
if img is not None:
|
|
||||||
img = cv2.flip(img, 0)
|
|
||||||
ret, flipped_jpg = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
|
||||||
if ret:
|
|
||||||
yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + flipped_jpg.tobytes() + b"\r\n"
|
|
||||||
else:
|
|
||||||
# 解码失败,直透原始帧(不应发生)
|
|
||||||
yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + jpg_data + b"\r\n"
|
yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + jpg_data + b"\r\n"
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
@@ -1344,6 +1336,11 @@ def api_agv_reset():
|
|||||||
def api_mission_start():
|
def api_mission_start():
|
||||||
"""开始执行任务(V3: M×N Grid 蛇形路径)"""
|
"""开始执行任务(V3: M×N Grid 蛇形路径)"""
|
||||||
data = request.json or {}
|
data = request.json or {}
|
||||||
|
|
||||||
|
# 必须先设置报关单(开始查验)
|
||||||
|
if not gs.inspection:
|
||||||
|
return jsonify({"ok": False, "error": "请先在「设置→报关单」中选择报关单并点击「开始查验」"}), 400
|
||||||
|
|
||||||
single_step = bool(data.get("single_step", False))
|
single_step = bool(data.get("single_step", False))
|
||||||
# 任务步骤控制开关
|
# 任务步骤控制开关
|
||||||
options = {
|
options = {
|
||||||
@@ -1538,6 +1535,18 @@ def api_mission_state():
|
|||||||
result["waiting_step"] = False
|
result["waiting_step"] = False
|
||||||
result["waiting_error"] = False
|
result["waiting_error"] = False
|
||||||
|
|
||||||
|
# 查验状态
|
||||||
|
result["inspection"] = gs.inspection
|
||||||
|
|
||||||
|
# QR 输入弹窗消息
|
||||||
|
if hasattr(MissionExecutorV3, "_instance") and MissionExecutorV3._instance:
|
||||||
|
rpt = MissionExecutorV3._instance.report
|
||||||
|
result["qr_message"] = rpt.get("qr_message", "")
|
||||||
|
result["step_label"] = rpt.get("step_label", "")
|
||||||
|
else:
|
||||||
|
result["qr_message"] = ""
|
||||||
|
result["step_label"] = ""
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
@app.route("/api/mission/log", methods=["GET"])
|
@app.route("/api/mission/log", methods=["GET"])
|
||||||
@@ -1755,10 +1764,45 @@ def api_qr_config_scan(qr_id):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ========== 环境切换 API ==========
|
||||||
|
|
||||||
|
@app.route("/api/config/mode", methods=["GET"])
|
||||||
|
def api_config_mode_get():
|
||||||
|
"""获取当前 API 环境模式"""
|
||||||
|
import config
|
||||||
|
return jsonify({
|
||||||
|
"ok": True,
|
||||||
|
"test_mode": config.TEST_MODE,
|
||||||
|
"base_url": config.ZHIJIAN_BASE_URL,
|
||||||
|
"label": "测试环境" if config.TEST_MODE else "正式环境"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/config/mode", methods=["POST"])
|
||||||
|
def api_config_mode_set():
|
||||||
|
"""切换 API 环境"""
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
test_mode = body.get("test_mode", True)
|
||||||
|
set_api_mode(test_mode)
|
||||||
|
import config
|
||||||
|
logger.info(f"API 环境已切换为: {'测试' if test_mode else '正式'} → {config.ZHIJIAN_BASE_URL}")
|
||||||
|
return jsonify({
|
||||||
|
"ok": True,
|
||||||
|
"test_mode": config.TEST_MODE,
|
||||||
|
"base_url": config.ZHIJIAN_BASE_URL,
|
||||||
|
"label": "测试环境" if config.TEST_MODE else "正式环境"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# ========== 报关单接口(代理外部 API) ==========
|
# ========== 报关单接口(代理外部 API) ==========
|
||||||
|
|
||||||
_ZHIJIAN_BASE = "https://ts.zhijian168.com/prod-api/zhijian/integration"
|
def _get_zhijian_base():
|
||||||
_ZHIJIAN_AUTH = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX2tleSI6ImZhNTNkZTZiLWE3NjYtNDZmNC05MDUyLTQ2MjUzZTAyNjdmNSIsInVzZXJuYW1lIjoiYWRtaW4ifQ.lC4vKThZo4aAOLsekm2kPgaEJRqRx-YDQWKfHFqxdPNESCKy57l3eIqaKTj2ZjAMaoYAwYlMrv5M1zAOJsO_PA"
|
"""动态获取报关单 API base,跟随环境切换"""
|
||||||
|
import config
|
||||||
|
base = f"{config.ZHIJIAN_BASE_URL}{config.API_PREFIX}/zhijian/integration"
|
||||||
|
return base
|
||||||
|
|
||||||
|
_ZHIJIAN_AUTH = ZHIJIAN_AUTH_TOKEN
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/customs/list")
|
@app.route("/api/customs/list")
|
||||||
@@ -1767,36 +1811,49 @@ def api_customs_list():
|
|||||||
import requests
|
import requests
|
||||||
page = request.args.get("pageNum", 1)
|
page = request.args.get("pageNum", 1)
|
||||||
size = request.args.get("pageSize", 50)
|
size = request.args.get("pageSize", 50)
|
||||||
url = f"{_ZHIJIAN_BASE}/customsListPage?pageNum={page}&pageSize={size}"
|
url = f"{_get_zhijian_base()}/customsListPage?pageNum={page}&pageSize={size}"
|
||||||
|
logger.info(f"[customs/list] 🔍 请求 → {url}")
|
||||||
try:
|
try:
|
||||||
r = requests.get(url, headers={"Authorization": _ZHIJIAN_AUTH}, timeout=15)
|
r = requests.get(url, headers={"Authorization": _ZHIJIAN_AUTH}, timeout=15)
|
||||||
|
logger.info(f"[customs/list] 📡 响应 HTTP {r.status_code}, body长度={len(r.text)}")
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
|
logger.warning(f"[customs/list] ⚠️ 返回非200: {r.status_code}")
|
||||||
return jsonify({"ok": False, "error": f"报关单API返回 {r.status_code}"}), 502
|
return jsonify({"ok": False, "error": f"报关单API返回 {r.status_code}"}), 502
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
rows = data.get("rows", [])
|
||||||
|
logger.info(f"[customs/list] ✅ 获取到 {len(rows)} 条报关单")
|
||||||
return jsonify({"ok": True, "data": data})
|
return jsonify({"ok": True, "data": data})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取报关单列表失败: {e}")
|
logger.error(f"[customs/list] ❌ 失败: {e}")
|
||||||
return jsonify({"ok": False, "error": str(e)}), 502
|
return jsonify({"ok": False, "error": str(e)}), 502
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/customs/machines")
|
@app.route("/api/customs/machines")
|
||||||
def api_customs_machines():
|
def api_customs_machines():
|
||||||
"""根据报关单 ID 获取机器列表(代理外部 API)
|
"""根据报关单 ID 获取机器列表(代理外部 API)
|
||||||
查询参数: customsId=xxx
|
数据源:cjt_customs_item 表 → Java customsMachines 接口
|
||||||
"""
|
"""
|
||||||
import requests
|
import requests
|
||||||
customs_id = request.args.get("customsId", "")
|
customs_id = request.args.get("customsId", "")
|
||||||
|
logger.info(f"[customs/machines] 📥 customsId={customs_id}")
|
||||||
if not customs_id:
|
if not customs_id:
|
||||||
|
logger.warning("[customs/machines] ⚠️ 缺少 customsId")
|
||||||
return jsonify({"ok": False, "error": "缺少 customsId 参数"}), 400
|
return jsonify({"ok": False, "error": "缺少 customsId 参数"}), 400
|
||||||
url = f"{_ZHIJIAN_BASE}/customsMachines?customsId={customs_id}"
|
|
||||||
try:
|
try:
|
||||||
|
url = f"{_get_zhijian_base()}/customsMachines?customsId={customs_id}"
|
||||||
|
logger.info(f"[customs/machines] 🔍 请求 → {url}")
|
||||||
r = requests.get(url, headers={"Authorization": _ZHIJIAN_AUTH}, timeout=15)
|
r = requests.get(url, headers={"Authorization": _ZHIJIAN_AUTH}, timeout=15)
|
||||||
|
logger.info(f"[customs/machines] 📡 响应 HTTP {r.status_code}, body长度={len(r.text)}")
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
return jsonify({"ok": False, "error": f"机器列表API返回 {r.status_code}"}), 502
|
logger.warning(f"[customs/machines] ⚠️ 返回非200: {r.status_code}, body={r.text[:300]}")
|
||||||
data = r.json()
|
return jsonify({"ok": False, "error": "机器列表API返回非200"}), 502
|
||||||
return jsonify({"ok": True, "data": data})
|
result = r.json()
|
||||||
|
machines = result.get("data") or []
|
||||||
|
logger.info(f"[customs/machines] ✅ 获取到 {len(machines)} 条机器记录")
|
||||||
|
return jsonify({"ok": True, "data": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取报关单机器列表失败: {e}")
|
logger.error(f"[customs/machines] ❌ 失败: {e}")
|
||||||
return jsonify({"ok": False, "error": str(e)}), 502
|
return jsonify({"ok": False, "error": str(e)}), 502
|
||||||
|
|
||||||
|
|
||||||
@@ -1820,6 +1877,188 @@ def api_customs_selected_get():
|
|||||||
return jsonify({"ok": True, "customs": c})
|
return jsonify({"ok": True, "customs": c})
|
||||||
|
|
||||||
|
|
||||||
|
# ========== 查验 API ==========
|
||||||
|
@app.route("/api/customs/inspection/start", methods=["POST"])
|
||||||
|
def api_customs_inspection_start():
|
||||||
|
"""开始查验:加载报关单机器列表,初始化查验计数"""
|
||||||
|
data = request.json or {}
|
||||||
|
customs_id = data.get("customsId", "")
|
||||||
|
if not customs_id:
|
||||||
|
return jsonify({"ok": False, "error": "缺少 customsId"}), 400
|
||||||
|
|
||||||
|
# 获取报关单机器列表
|
||||||
|
try:
|
||||||
|
machines_url = f"{_get_zhijian_base()}/customsMachines?customsId={customs_id}"
|
||||||
|
logger.info(f"[inspection/start] 🔍 获取机器列表 → {machines_url}")
|
||||||
|
r = requests.get(
|
||||||
|
machines_url,
|
||||||
|
headers={"Authorization": _ZHIJIAN_AUTH},
|
||||||
|
timeout=15
|
||||||
|
)
|
||||||
|
logger.info(f"[inspection/start] 📡 机器列表响应 HTTP {r.status_code}, body长度={len(r.text)}")
|
||||||
|
if r.status_code != 200:
|
||||||
|
logger.warning(f"[inspection/start] ⚠️ 返回非200: {r.status_code}, body={r.text[:300]}")
|
||||||
|
return jsonify({"ok": False, "error": f"接口返回 {r.status_code}"}), 502
|
||||||
|
j = r.json()
|
||||||
|
machines = j.get("data") or []
|
||||||
|
logger.info(f"[inspection/start] ✅ 获取到 {len(machines)} 条机器记录")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[inspection/start] ❌ 获取机器列表失败: {e}")
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 502
|
||||||
|
|
||||||
|
# 按 inventoryCode 聚合(同一物料可能有多条序列号记录)
|
||||||
|
items_dict = {}
|
||||||
|
for m in machines:
|
||||||
|
code = m.get("inventoryCode") or m.get("machineCode") or "unknown"
|
||||||
|
if code not in items_dict:
|
||||||
|
items_dict[code] = {
|
||||||
|
"inventoryCode": code,
|
||||||
|
"inventoryName": m.get("inventoryName") or m.get("machineName") or "-",
|
||||||
|
"spec": m.get("inventorySpecification") or m.get("spec") or "-",
|
||||||
|
"quantify": 0,
|
||||||
|
"inspected": 0,
|
||||||
|
}
|
||||||
|
# 累计数量(quantify 字段可能来自 customsMachines 的返回值)
|
||||||
|
q = m.get("quantify", 0)
|
||||||
|
if q:
|
||||||
|
items_dict[code]["quantify"] += int(float(q))
|
||||||
|
|
||||||
|
# 如果 quantify 全部为 0,用机器条目数作为数量
|
||||||
|
for item in items_dict.values():
|
||||||
|
if item["quantify"] == 0:
|
||||||
|
item["quantify"] = sum(1 for m in machines if (m.get("inventoryCode") or "") == item["inventoryCode"])
|
||||||
|
|
||||||
|
logger.info(f"[inspection/start] 📊 聚合结果: {len(items_dict)} 种机型, total {sum(i['quantify'] for i in items_dict.values())} 台")
|
||||||
|
|
||||||
|
# 获取报关单名称
|
||||||
|
customs_name = data.get("customsName") or customs_id
|
||||||
|
try:
|
||||||
|
list_url = f"{_get_zhijian_base()}/customsListPage?pageNum=1&pageSize=100"
|
||||||
|
logger.info(f"[inspection/start] 🔍 获取报关单名称 → {list_url}")
|
||||||
|
r2 = requests.get(
|
||||||
|
list_url,
|
||||||
|
headers={"Authorization": _ZHIJIAN_AUTH},
|
||||||
|
timeout=15
|
||||||
|
)
|
||||||
|
if r2.status_code == 200:
|
||||||
|
j2 = r2.json()
|
||||||
|
for row in j2.get("rows", []):
|
||||||
|
c = row.get("customs", {})
|
||||||
|
if str(c.get("id", "")) == str(customs_id):
|
||||||
|
customs_name = c.get("customsCode") or row.get("orderCode") or customs_name
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
gs.inspection = {
|
||||||
|
"customsId": customs_id,
|
||||||
|
"customsName": customs_name,
|
||||||
|
"items": list(items_dict.values()),
|
||||||
|
"startedAt": time.time(),
|
||||||
|
}
|
||||||
|
logger.info(f"开始查验: {customs_name} ({len(gs.inspection['items'])} 种机型,共 {sum(i['quantify'] for i in gs.inspection['items'])} 台)")
|
||||||
|
return jsonify({"ok": True, "inspection": gs.inspection})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/customs/inspection", methods=["GET"])
|
||||||
|
def api_customs_inspection():
|
||||||
|
"""获取当前查验状态"""
|
||||||
|
return jsonify({"ok": True, "inspection": gs.inspection})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/customs/inspection/end", methods=["POST"])
|
||||||
|
def api_customs_inspection_end():
|
||||||
|
"""结束查验"""
|
||||||
|
gs.inspection = None
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/customs/printer", methods=["GET"])
|
||||||
|
def api_customs_printer():
|
||||||
|
"""代理 /zhijian/profile/printer 查询,同时更新查验计数
|
||||||
|
GET ?serialNumber=xxx
|
||||||
|
"""
|
||||||
|
sn = request.args.get("serialNumber", "").strip()
|
||||||
|
logger.info(f"[printer] 📥 收到查询请求 serialNumber={sn}")
|
||||||
|
if not sn:
|
||||||
|
logger.warning("[printer] ⚠️ 缺少 serialNumber 参数")
|
||||||
|
return jsonify({"ok": False, "error": "缺少 serialNumber"}), 400
|
||||||
|
|
||||||
|
# 调用 Java profile/printer 接口
|
||||||
|
api_base = _get_zhijian_base().rstrip("/")
|
||||||
|
profile_url = f"{api_base[:api_base.rfind('/')]}/profile/printer?serialNumber={sn}"
|
||||||
|
logger.info(f"[printer] 🔍 请求 Java → {profile_url}")
|
||||||
|
try:
|
||||||
|
r = requests.get(profile_url, headers={"Authorization": _ZHIJIAN_AUTH}, timeout=15)
|
||||||
|
logger.info(f"[printer] 📡 Java响应 HTTP {r.status_code}: {r.text[:500]}")
|
||||||
|
if r.status_code != 200:
|
||||||
|
logger.warning(f"[printer] ⚠️ Java返回非200: {r.status_code}")
|
||||||
|
return jsonify({"ok": False, "error": f"profile/printer 返回 {r.status_code}"}), 502
|
||||||
|
j = r.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[printer] ❌ 查询 Java printer 失败: {e}")
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 502
|
||||||
|
|
||||||
|
data = j.get("data", j)
|
||||||
|
printer = data.get("printer")
|
||||||
|
order_item = data.get("orderItem")
|
||||||
|
logger.info(f"[printer] 📊 Java返回解析: printer={'yes' if printer else 'no'}, orderItem={'yes' if order_item else 'no'}")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"ok": True,
|
||||||
|
"printer": printer,
|
||||||
|
"orderItem": order_item,
|
||||||
|
"modelName": "机器1", # 默认
|
||||||
|
"inventoryCode": None,
|
||||||
|
"matchedItem": None,
|
||||||
|
"hasInspection": gs.inspection is not None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 提取 inventory 信息(优先级: orderItem.inventory > printer.inventory > printer.model/machineModel)
|
||||||
|
if order_item and order_item.get("inventory"):
|
||||||
|
inv = order_item["inventory"]
|
||||||
|
result["modelName"] = inv.get("inventoryName") or inv.get("name") or "机器1"
|
||||||
|
result["inventoryCode"] = inv.get("inventoryCode") or inv.get("code")
|
||||||
|
logger.info(f"[printer] 🏷️ 从 orderItem.inventory 提取: modelName={result['modelName']}, inventoryCode={result['inventoryCode']}")
|
||||||
|
elif printer and printer.get("inventory"):
|
||||||
|
inv = printer["inventory"]
|
||||||
|
result["modelName"] = inv.get("inventoryName") or inv.get("name") or "机器1"
|
||||||
|
result["inventoryCode"] = inv.get("inventoryCode") or inv.get("code")
|
||||||
|
logger.info(f"[printer] 🏷️ 从 printer.inventory 提取: modelName={result['modelName']}, inventoryCode={result['inventoryCode']}")
|
||||||
|
elif printer:
|
||||||
|
result["modelName"] = printer.get("model") or printer.get("machineModel") or "机器1"
|
||||||
|
logger.info(f"[printer] 🏷️ 从 printer.model/machineModel 提取: modelName={result['modelName']}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"[printer] ⚠️ printer 和 orderItem 均为空,回退 modelName=机器1")
|
||||||
|
|
||||||
|
# 更新查验计数
|
||||||
|
if gs.inspection and result["inventoryCode"]:
|
||||||
|
for item in gs.inspection["items"]:
|
||||||
|
if item["inventoryCode"] == result["inventoryCode"]:
|
||||||
|
item["inspected"] += 1
|
||||||
|
result["matchedItem"] = item
|
||||||
|
logger.info(f"[printer] ✅ 查验计数更新: {item['inventoryName']} → {item['inspected']}/{item['quantify']}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.warning(f"[printer] ⚠️ inventoryCode={result['inventoryCode']} 不在查验清单中")
|
||||||
|
logger.info(f"[printer] 📤 返回结果: modelName={result['modelName']}, inventoryCode={result['inventoryCode']}, hasInspection={result['hasInspection']}, matched={'yes' if result['matchedItem'] else 'no'}")
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/customs/inspection/update", methods=["POST"])
|
||||||
|
def api_customs_inspection_update():
|
||||||
|
"""直接更新查验计数(由执行器调用)"""
|
||||||
|
data = request.json or {}
|
||||||
|
code = data.get("inventoryCode", "")
|
||||||
|
if not gs.inspection or not code:
|
||||||
|
return jsonify({"ok": False})
|
||||||
|
for item in gs.inspection["items"]:
|
||||||
|
if item["inventoryCode"] == code:
|
||||||
|
item["inspected"] += 1
|
||||||
|
return jsonify({"ok": True, "item": item})
|
||||||
|
return jsonify({"ok": False, "error": "未匹配"})
|
||||||
|
|
||||||
|
|
||||||
# ========== 静态资源 ==========
|
# ========== 静态资源 ==========
|
||||||
@app.route("/photos/<name>")
|
@app.route("/photos/<name>")
|
||||||
def photos(name):
|
def photos(name):
|
||||||
|
|||||||
+22
-1
@@ -47,9 +47,30 @@ ARM_CAMERA_CONFIG = {
|
|||||||
"snapshot_url": f"http://{ARM_HOST}:5003/api/camera/snapshot",
|
"snapshot_url": f"http://{ARM_HOST}:5003/api/camera/snapshot",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ========== 外部 API 环境 ==========
|
||||||
|
# 切换测试/正式环境只需改 TEST_MODE 一个变量
|
||||||
|
TEST_MODE = False # True=测试环境(192.168.60.159), False=正式环境(ts.zhijian168.com)
|
||||||
|
PROD_BASE_URL = "https://ts.zhijian168.com"
|
||||||
|
TEST_BASE_URL = "http://192.168.60.159:8080"
|
||||||
|
PROD_API_PREFIX = "/prod-api"
|
||||||
|
TEST_API_PREFIX = "" # 测试服务器无 /prod-api 网关前缀
|
||||||
|
ZHIJIAN_BASE_URL = TEST_BASE_URL if TEST_MODE else PROD_BASE_URL
|
||||||
|
API_PREFIX = TEST_API_PREFIX if TEST_MODE else PROD_API_PREFIX
|
||||||
|
ZHIJIAN_AUTH_TOKEN = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX2tleSI6ImZhNTNkZTZiLWE3NjYtNDZmNC05MDUyLTQ2MjUzZTAyNjdmNSIsInVzZXJuYW1lIjoiYWRtaW4ifQ.lC4vKThZo4aAOLsekm2kPgaEJRqRx-YDQWKfHFqxdPNESCKy57l3eIqaKTj2ZjAMaoYAwYlMrv5M1zAOJsO_PA"
|
||||||
|
|
||||||
|
|
||||||
|
def set_api_mode(test_mode):
|
||||||
|
"""运行时切换 API 环境 — 无需重启 Flask"""
|
||||||
|
global TEST_MODE, ZHIJIAN_BASE_URL, API_PREFIX, UPLOAD_CONFIG
|
||||||
|
TEST_MODE = bool(test_mode)
|
||||||
|
ZHIJIAN_BASE_URL = TEST_BASE_URL if TEST_MODE else PROD_BASE_URL
|
||||||
|
API_PREFIX = TEST_API_PREFIX if TEST_MODE else PROD_API_PREFIX
|
||||||
|
UPLOAD_CONFIG["url"] = f"{ZHIJIAN_BASE_URL}{API_PREFIX}/file/uploadImage"
|
||||||
|
|
||||||
|
|
||||||
# ========== HTTP 上传 ==========
|
# ========== HTTP 上传 ==========
|
||||||
UPLOAD_CONFIG = {
|
UPLOAD_CONFIG = {
|
||||||
"url": "https://ts.zhijian168.com/prod-api/file/uploadImage",
|
"url": f"{ZHIJIAN_BASE_URL}{API_PREFIX}/file/uploadImage",
|
||||||
"timeout": 30,
|
"timeout": 30,
|
||||||
"max_retries": 3,
|
"max_retries": 3,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,49 @@ a:hover { text-decoration: underline; }
|
|||||||
.status-item.paused { background: #3a2a1a; color: #ff9800; }
|
.status-item.paused { background: #3a2a1a; color: #ff9800; }
|
||||||
.status-item.idle { background: #2a2a2a; color: #9aa0a6; }
|
.status-item.idle { background: #2a2a2a; color: #9aa0a6; }
|
||||||
|
|
||||||
|
/* ========== 环境切换开关 ========== */
|
||||||
|
.env-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.env-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 48px;
|
||||||
|
text-align: right;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.env-label.test { color: #ff9800; }
|
||||||
|
.env-label.prod { color: #4fc3f7; }
|
||||||
|
.toggle-switch {
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
background: #3a3a3a;
|
||||||
|
border-radius: 11px;
|
||||||
|
position: relative;
|
||||||
|
transition: background 0.25s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.toggle-switch.active {
|
||||||
|
background: #ff9800;
|
||||||
|
}
|
||||||
|
.toggle-knob {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
transition: left 0.25s;
|
||||||
|
}
|
||||||
|
.toggle-switch.active .toggle-knob {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ========== 卡片 ========== */
|
/* ========== 卡片 ========== */
|
||||||
.card {
|
.card {
|
||||||
background: #1a2332;
|
background: #1a2332;
|
||||||
@@ -466,7 +509,7 @@ a:hover { text-decoration: underline; }
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.camera-img.arm {
|
.camera-img.arm {
|
||||||
transform: rotate(180deg);
|
/* no flip */
|
||||||
}
|
}
|
||||||
|
|
||||||
.camera-placeholder {
|
.camera-placeholder {
|
||||||
@@ -1191,3 +1234,74 @@ a:hover { text-decoration: underline; }
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 16px 0 8px;
|
padding: 16px 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 查验进度 ===== */
|
||||||
|
.inspection-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.inspection-item {
|
||||||
|
background: rgba(26, 26, 46, 0.7);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #2a2a3e;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.inspection-item.insp-done {
|
||||||
|
border-color: #4caf50;
|
||||||
|
background: rgba(76, 175, 80, 0.08);
|
||||||
|
}
|
||||||
|
.inspection-item.insp-active {
|
||||||
|
border-color: #ff9800;
|
||||||
|
background: rgba(255, 152, 0, 0.08);
|
||||||
|
}
|
||||||
|
.insp-name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
.insp-code {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8899aa;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.insp-spec {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #667788;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.insp-count {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.insp-num {
|
||||||
|
color: #4fc3f7;
|
||||||
|
}
|
||||||
|
.insp-sep {
|
||||||
|
color: #667788;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.insp-total {
|
||||||
|
color: #8899aa;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.insp-bar {
|
||||||
|
height: 4px;
|
||||||
|
background: #0a0a14;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.insp-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4fc3f7, #4caf50);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ createApp({
|
|||||||
agvCameraError: false,
|
agvCameraError: false,
|
||||||
hasAgvCamera: false, // AGV 车体是否有可用相机
|
hasAgvCamera: false, // AGV 车体是否有可用相机
|
||||||
armCameraError: false,
|
armCameraError: false,
|
||||||
reconnectingDevice: null
|
reconnectingDevice: null,
|
||||||
|
// 环境切换
|
||||||
|
testMode: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -38,6 +40,7 @@ createApp({
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.refresh()
|
this.refresh()
|
||||||
this.refreshCameraCapabilities()
|
this.refreshCameraCapabilities()
|
||||||
|
this.loadEnvMode()
|
||||||
setInterval(this.refreshStatus, 3000)
|
setInterval(this.refreshStatus, 3000)
|
||||||
this.refreshCams()
|
this.refreshCams()
|
||||||
setInterval(() => this.refreshCams(), 2000)
|
setInterval(() => this.refreshCams(), 2000)
|
||||||
@@ -133,6 +136,36 @@ createApp({
|
|||||||
} else {
|
} else {
|
||||||
window.location.href = '/running'
|
window.location.href = '/running'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async loadEnvMode() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(API + '/api/config/mode')
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.ok) {
|
||||||
|
this.testMode = data.test_mode
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载环境配置失败:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async toggleEnvMode() {
|
||||||
|
const newMode = !this.testMode
|
||||||
|
try {
|
||||||
|
const res = await fetch(API + '/api/config/mode', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({test_mode: newMode})
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.ok) {
|
||||||
|
this.testMode = data.test_mode
|
||||||
|
alert('已切换至: ' + data.label)
|
||||||
|
} else {
|
||||||
|
alert('切换失败: ' + (data.error || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('切换请求失败: ' + e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}).mount('#app')
|
}).mount('#app')
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ createApp({
|
|||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
waitingStep: false,
|
waitingStep: false,
|
||||||
stepLabel: '',
|
stepLabel: '',
|
||||||
|
qrMessage: '所有姿态均未识别到二维码,请手动输入:',
|
||||||
// 任务步骤控制开关(机械臂初始化并入AGV移动)
|
// 任务步骤控制开关(机械臂初始化并入AGV移动)
|
||||||
agvMoveEnabled: true,
|
agvMoveEnabled: true,
|
||||||
qrScanEnabled: true,
|
qrScanEnabled: true,
|
||||||
@@ -36,6 +37,8 @@ createApp({
|
|||||||
// 速度控制
|
// 速度控制
|
||||||
agvSpeed: 1.0,
|
agvSpeed: 1.0,
|
||||||
armSpeed: 1000,
|
armSpeed: 1000,
|
||||||
|
// 查验
|
||||||
|
inspection: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -51,6 +54,14 @@ createApp({
|
|||||||
}
|
}
|
||||||
return map[this.missionState] || '未知'
|
return map[this.missionState] || '未知'
|
||||||
},
|
},
|
||||||
|
inspectionTotal() {
|
||||||
|
if (!this.inspection || !this.inspection.items) return 0
|
||||||
|
return this.inspection.items.reduce((s, i) => s + (i.inspected || 0), 0)
|
||||||
|
},
|
||||||
|
inspectionTarget() {
|
||||||
|
if (!this.inspection || !this.inspection.items) return 0
|
||||||
|
return this.inspection.items.reduce((s, i) => s + (i.quantify || 0), 0)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.poll()
|
this.poll()
|
||||||
@@ -101,6 +112,7 @@ createApp({
|
|||||||
if (data.grid) this.missionGrid = data.grid
|
if (data.grid) this.missionGrid = data.grid
|
||||||
if (data.point_status) this.pointStatus = data.point_status
|
if (data.point_status) this.pointStatus = data.point_status
|
||||||
if (data.machine_status) this.machineStatus = data.machine_status
|
if (data.machine_status) this.machineStatus = data.machine_status
|
||||||
|
if (data.inspection) this.inspection = data.inspection
|
||||||
this.armCameraOpened = data.arm_camera_opened
|
this.armCameraOpened = data.arm_camera_opened
|
||||||
if (this.armCameraOpened && !this.armPreviewUrl) {
|
if (this.armCameraOpened && !this.armPreviewUrl) {
|
||||||
this.armPreviewUrl = API + '/api/camera/arm_preview'
|
this.armPreviewUrl = API + '/api/camera/arm_preview'
|
||||||
@@ -122,6 +134,11 @@ createApp({
|
|||||||
this.waitingStep = false
|
this.waitingStep = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QR 弹窗消息
|
||||||
|
if (data.qr_message) {
|
||||||
|
this.qrMessage = data.qr_message
|
||||||
|
}
|
||||||
|
|
||||||
// QR 弹窗(防止提交后重复弹出)
|
// QR 弹窗(防止提交后重复弹出)
|
||||||
if (this.missionState !== 'waiting_qr') {
|
if (this.missionState !== 'waiting_qr') {
|
||||||
this.qrSubmitting = false
|
this.qrSubmitting = false
|
||||||
@@ -129,6 +146,9 @@ createApp({
|
|||||||
if (this.missionState === 'waiting_qr' && !this.showQrModal && !this.qrSubmitting) {
|
if (this.missionState === 'waiting_qr' && !this.showQrModal && !this.qrSubmitting) {
|
||||||
this.showQrModal = true
|
this.showQrModal = true
|
||||||
this.qrValue = ''
|
this.qrValue = ''
|
||||||
|
if (!this.qrMessage) {
|
||||||
|
this.qrMessage = '所有姿态均未识别到二维码,请手动输入:'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 完成后获取报告
|
// 完成后获取报告
|
||||||
@@ -156,6 +176,11 @@ createApp({
|
|||||||
},
|
},
|
||||||
async startMission() {
|
async startMission() {
|
||||||
if (this.missionState !== 'idle') return
|
if (this.missionState !== 'idle') return
|
||||||
|
// 没有设置报关单时阻止启动(后端也会校验,这里提前友好提示)
|
||||||
|
if (!this.inspection) {
|
||||||
|
alert('⚠️ 请先在「设置→报关单」中选择报关单并点击「开始查验」')
|
||||||
|
return
|
||||||
|
}
|
||||||
this.logs = []
|
this.logs = []
|
||||||
this.progress = 0
|
this.progress = 0
|
||||||
this.report = null
|
this.report = null
|
||||||
@@ -186,6 +211,11 @@ createApp({
|
|||||||
},
|
},
|
||||||
async startSingleStep() {
|
async startSingleStep() {
|
||||||
if (this.missionState !== 'idle') return
|
if (this.missionState !== 'idle') return
|
||||||
|
// 没有设置报关单时阻止启动(后端会校验,这里提前友好提示)
|
||||||
|
if (!this.inspection) {
|
||||||
|
alert('⚠️ 请先在「设置→报关单」中选择报关单并点击「开始查验」')
|
||||||
|
return
|
||||||
|
}
|
||||||
this.logs = []
|
this.logs = []
|
||||||
this.progress = 0
|
this.progress = 0
|
||||||
this.report = null
|
this.report = null
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ createApp({
|
|||||||
this.refreshAngles()
|
this.refreshAngles()
|
||||||
this.loadQrConfigs()
|
this.loadQrConfigs()
|
||||||
this.nav2Timer = setInterval(this.refreshNavStatus, 3000)
|
this.nav2Timer = setInterval(this.refreshNavStatus, 3000)
|
||||||
this.armCameraUrl = API + '/api/camera/arm_preview?t=' + Date.now()
|
this.armSnapshotUrl = ""; this.armCameraUrl = API + '/api/camera/arm_preview?t=' + Date.now()
|
||||||
|
this.armSnapshotUrl = ""; this.armCameraUrl = API + "/api/camera/arm_preview?t=" + Date.now()
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
customsTotalPages() {
|
customsTotalPages() {
|
||||||
@@ -1203,12 +1204,11 @@ createApp({
|
|||||||
alert('❌ 复位请求失败: ' + e.message)
|
alert('❌ 复位请求失败: ' + e.message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
|
||||||
// ===== 报关单方法 =====
|
// ===== 报关单方法 =====
|
||||||
async loadCustomsList() {
|
async loadCustomsList() {
|
||||||
this.customsLoading = true
|
this.customsLoading = true
|
||||||
try {
|
try {
|
||||||
const url = API + '/api/customs/list?pageNum=' + this.customsPage + '&pageSize=' + this.customsPageSize
|
const url = API + '/api/customs/list?pageNum=' + this.customsPage + '&pageSize=' + this.customsPageSize + '&customsName=' + encodeURIComponent(this.customsName) + '&customsNo=' + encodeURIComponent(this.customsNo)
|
||||||
const res = await fetch(url)
|
const res = await fetch(url)
|
||||||
const d = await res.json()
|
const d = await res.json()
|
||||||
if (d.ok && d.data) {
|
if (d.ok && d.data) {
|
||||||
@@ -1236,17 +1236,20 @@ createApp({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async selectCustomsRow(item) {
|
async selectCustomsRow(item) {
|
||||||
const id = item.id || item.customsId || item.customs_id || ''
|
// 新数据结构: { customs:{id,orderId,..}, orderCode, drawCode }
|
||||||
|
const id = (item.customs && item.customs.id) || item.id || item.customsId || item.customs_id || ''
|
||||||
if (!id) return
|
if (!id) return
|
||||||
this.selectedCustomsId = id
|
this.selectedCustomsId = id
|
||||||
this.selectedCustomsName = item.customsNo || item.customs_no || item.name || item.customsName || item.customs_name || id
|
this.selectedCustomsName = (item.customs && item.customs.customsCode) || item.orderCode || item.drawCode || id
|
||||||
this.customsMachines = []
|
this.customsMachines = []
|
||||||
try {
|
try {
|
||||||
const res = await fetch(API + '/api/customs/machines?customsId=' + encodeURIComponent(id))
|
const url = API + '/api/customs/machines?customsId=' + encodeURIComponent(id)
|
||||||
|
const res = await fetch(url)
|
||||||
const d = await res.json()
|
const d = await res.json()
|
||||||
if (d.ok && d.data) {
|
if (d.ok && d.data) {
|
||||||
const raw = d.data
|
const raw = d.data
|
||||||
let machines = []
|
let machines = []
|
||||||
|
// customsMachines 返回格式: {"code":"0","data":[{serialNumber,inventoryName,...}]}
|
||||||
if (raw.rows) { machines = raw.rows }
|
if (raw.rows) { machines = raw.rows }
|
||||||
else if (raw.records) { machines = raw.records }
|
else if (raw.records) { machines = raw.records }
|
||||||
else if (raw.data && Array.isArray(raw.data)) { machines = raw.data }
|
else if (raw.data && Array.isArray(raw.data)) { machines = raw.data }
|
||||||
@@ -1261,6 +1264,61 @@ createApp({
|
|||||||
this.customsMachines = []
|
this.customsMachines = []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
async startInspection(item) {
|
||||||
|
const id = (item.customs && item.customs.id) || item.id || item.customsId || item.customs_id || ''
|
||||||
|
const name = (item.customs && item.customs.customsCode) || item.orderCode || item.drawCode || id
|
||||||
|
if (!id) return
|
||||||
|
if (!confirm(`确定要对报关单「${name}」开始查验吗?\n点击确定后,运行页将以该报关单的机器进行查验。`)) return
|
||||||
|
try {
|
||||||
|
const res = await fetch(API + '/api/customs/inspection/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ customsId: id, customsName: name })
|
||||||
|
})
|
||||||
|
const d = await res.json()
|
||||||
|
if (d.ok) {
|
||||||
|
alert(`✅ 查验已开始!\n报关单: ${name}\n机型: ${d.inspection.items.length} 种\n总数: ${d.inspection.items.reduce((s,i)=>s+i.quantify,0)} 台\n\n请前往「运行」页执行任务。`)
|
||||||
|
// 同时选中该报关单,显示机器列表
|
||||||
|
this.selectedCustomsId = id
|
||||||
|
this.selectedCustomsName = name
|
||||||
|
// 用 inspection items 填充 customsMachines 显示(聚合后)
|
||||||
|
this.customsMachines = d.inspection.items.map(it => ({
|
||||||
|
inventoryCode: it.inventoryCode,
|
||||||
|
inventoryName: it.inventoryName,
|
||||||
|
inventorySpecification: it.spec,
|
||||||
|
serialNumber: '',
|
||||||
|
quantify: it.quantify,
|
||||||
|
inspectionCount: it.inspected,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
alert('❌ 开始查验失败: ' + (d.error || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('❌ 请求失败: ' + e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadInspectionCounts() {
|
||||||
|
// 轮询查验计数,更新 customsMachines 的 inspectionCount
|
||||||
|
try {
|
||||||
|
const res = await fetch(API + '/api/customs/inspection')
|
||||||
|
const d = await res.json()
|
||||||
|
if (d.ok && d.inspection && this.customsMachines.length) {
|
||||||
|
for (const item of d.inspection.items) {
|
||||||
|
const match = this.customsMachines.find(m => m.inventoryCode === item.inventoryCode)
|
||||||
|
if (match) {
|
||||||
|
match.inspectionCount = item.inspected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
tab(newVal) {
|
||||||
|
if (newVal === 'customs' && this.customsMachines.length > 0) {
|
||||||
|
// 切换到报关单 tab 时刷新查验计数
|
||||||
|
this.loadInspectionCounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}).mount('#app')
|
}).mount('#app')
|
||||||
|
|||||||
@@ -16,7 +16,13 @@
|
|||||||
<a href="/setting" class="nav-link">⚙️ 设置</a>
|
<a href="/setting" class="nav-link">⚙️ 设置</a>
|
||||||
<a href="/running" class="nav-link">▶️ 运行</a>
|
<a href="/running" class="nav-link">▶️ 运行</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="status-bar">
|
<div class="status-bar" style="display:flex;align-items:center;gap:12px">
|
||||||
|
<label class="env-toggle" title="切换测试/正式环境">
|
||||||
|
<span class="env-label" :class="testMode ? 'test' : 'prod'">{% raw %}{{ testMode ? '🧪 测试' : '🏭 正式' }}{% endraw %}</span>
|
||||||
|
<div class="toggle-switch" @click="toggleEnvMode" :class="{active: testMode}">
|
||||||
|
<div class="toggle-knob"></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
<span class="status-item" :class="statusClass">
|
<span class="status-item" :class="statusClass">
|
||||||
{% raw %}{{ statusText }}{% endraw %}
|
{% raw %}{{ statusText }}{% endraw %}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>运行监控 - AGV 拍摄系统</title>
|
<title>运行监控 - AGV 拍摄系统</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=20260529a">
|
<link rel="stylesheet" href="/static/css/style.css?v=20260616a">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -48,6 +48,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 查验进度 -->
|
||||||
|
<section class="card" v-if="inspection">
|
||||||
|
<h2>🔍 查验进度 — {% raw %}{{ inspection.customsName }}{% endraw %}</h2>
|
||||||
|
<p class="hint" style="margin-bottom:12px">
|
||||||
|
总进度: {% raw %}{{ inspectionTotal }}{% endraw %} / {% raw %}{{ inspectionTarget }}{% endraw %} 台
|
||||||
|
<span v-if="inspectionTotal >= inspectionTarget && inspectionTarget > 0" style="color:#4caf50;font-weight:bold"> ✅ 已完成</span>
|
||||||
|
</p>
|
||||||
|
<div class="inspection-grid">
|
||||||
|
<div v-for="(item, ii) in inspection.items" :key="ii" class="inspection-item" :class="{ 'insp-done': item.inspected >= item.quantify, 'insp-active': item.inspected > 0 && item.inspected < item.quantify }">
|
||||||
|
<div class="insp-name">{% raw %}{{ item.inventoryName }}{% endraw %}</div>
|
||||||
|
<div class="insp-code">{% raw %}{{ item.inventoryCode }}{% endraw %}</div>
|
||||||
|
<div class="insp-spec">{% raw %}{{ item.spec }}{% endraw %}</div>
|
||||||
|
<div class="insp-count">
|
||||||
|
<span class="insp-num">{% raw %}{{ item.inspected }}{% endraw %}</span>
|
||||||
|
<span class="insp-sep">/</span>
|
||||||
|
<span class="insp-total">{% raw %}{{ item.quantify }}{% endraw %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="insp-bar">
|
||||||
|
<div class="insp-fill" :style="{width: (item.quantify > 0 ? (item.inspected / item.quantify * 100) : 0) + '%'}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 任务步骤控制开关 -->
|
<!-- 任务步骤控制开关 -->
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🎛️ 任务步骤控制</h2>
|
<h2>🎛️ 任务步骤控制</h2>
|
||||||
@@ -202,7 +226,7 @@
|
|||||||
<div class="modal-overlay" v-if="showQrModal">
|
<div class="modal-overlay" v-if="showQrModal">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<h3>⌨️ 手动输入二维码</h3>
|
<h3>⌨️ 手动输入二维码</h3>
|
||||||
<p>所有姿态均未识别到二维码,请手动输入:</p>
|
<p>{% raw %}{{ qrMessage }}{% endraw %}</p>
|
||||||
<input type="text" v-model="qrValue" placeholder="输入二维码内容" autofocus @keyup.enter="submitQr">
|
<input type="text" v-model="qrValue" placeholder="输入二维码内容" autofocus @keyup.enter="submitQr">
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn btn-success" @click="rescanQr" style="margin-right:auto">🔄 重新扫描</button>
|
<button class="btn btn-success" @click="rescanQr" style="margin-right:auto">🔄 重新扫描</button>
|
||||||
@@ -241,6 +265,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/vue3.global.prod.js"></script>
|
<script src="/static/js/vue3.global.prod.js"></script>
|
||||||
<script src="/static/js/running.js?v=20260605b"></script>
|
<script src="/static/js/running.js?v=20260616c"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr style="background:#1a2332;text-align:left">
|
<tr style="background:#1a2332;text-align:left">
|
||||||
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">ID</th>
|
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">ID</th>
|
||||||
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">机型名称</th>
|
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">机型申请时间</th>
|
||||||
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">描述</th>
|
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">描述</th>
|
||||||
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">备注</th>
|
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">备注</th>
|
||||||
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px;text-align:center">操作</th>
|
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px;text-align:center">操作</th>
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
<div style="margin-top:8px">
|
<div style="margin-top:8px">
|
||||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||||
<input type="text" v-model="newPoseForm[m.id + '_front']"
|
<input type="text" v-model="newPoseForm[m.id + '_front']"
|
||||||
placeholder="姿态名称(如:取料)"
|
placeholder="姿态申请时间(如:取料)"
|
||||||
style="flex:1;min-width:120px;padding:6px;border:1px solid #2a3441;border-radius:4px">
|
style="flex:1;min-width:120px;padding:6px;border:1px solid #2a3441;border-radius:4px">
|
||||||
<button class="btn btn-secondary btn-small" @click="addPose(m.id, 'front', newPoseForm[m.id + '_front'])">➕ 添加正面姿态(当前角度)</button>
|
<button class="btn btn-secondary btn-small" @click="addPose(m.id, 'front', newPoseForm[m.id + '_front'])">➕ 添加正面姿态(当前角度)</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
<div style="margin-top:8px">
|
<div style="margin-top:8px">
|
||||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||||
<input type="text" v-model="newPoseForm[m.id + '_back']"
|
<input type="text" v-model="newPoseForm[m.id + '_back']"
|
||||||
placeholder="姿态名称(如:放料)"
|
placeholder="姿态申请时间(如:放料)"
|
||||||
style="flex:1;min-width:120px;padding:6px;border:1px solid #2a3441;border-radius:4px">
|
style="flex:1;min-width:120px;padding:6px;border:1px solid #2a3441;border-radius:4px">
|
||||||
<button class="btn btn-secondary btn-small" @click="addPose(m.id, 'back', newPoseForm[m.id + '_back'])">➕ 添加背面姿态(当前角度)</button>
|
<button class="btn btn-secondary btn-small" @click="addPose(m.id, 'back', newPoseForm[m.id + '_back'])">➕ 添加背面姿态(当前角度)</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
<button class="btn-icon" @click="showAddModelModal = false">✕</button>
|
<button class="btn-icon" @click="showAddModelModal = false">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:12px">
|
<div class="form-group" style="margin-bottom:12px">
|
||||||
<label>机型名称</label>
|
<label>机型申请时间</label>
|
||||||
<input type="text" v-model="newModelName" placeholder="例如:SMT-A" style="width:100%;padding:8px;border:1px solid #2a3441;border-radius:4px">
|
<input type="text" v-model="newModelName" placeholder="例如:SMT-A" style="width:100%;padding:8px;border:1px solid #2a3441;border-radius:4px">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:12px">
|
<div class="form-group" style="margin-bottom:12px">
|
||||||
@@ -445,7 +445,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;justify-content:flex-end;margin-bottom:16px">
|
<div style="display:flex;justify-content:flex-end;margin-bottom:16px">
|
||||||
<input type="text" v-model="newQrName" placeholder="输入名称..." style="background:#0f1923;border:1px solid #2a3441;color:#fff;padding:8px 12px;border-radius:6px;margin-right:8px;width:180px">
|
<input type="text" v-model="newQrName" placeholder="输入申请时间..." style="background:#0f1923;border:1px solid #2a3441;color:#fff;padding:8px 12px;border-radius:6px;margin-right:8px;width:180px">
|
||||||
<button class="btn btn-primary" @click="addQrConfig()">➕ 添加</button>
|
<button class="btn btn-primary" @click="addQrConfig()">➕ 添加</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="qrConfigs.length === 0" style="text-align:center;color:#9aa0a6;padding:40px">
|
<div v-if="qrConfigs.length === 0" style="text-align:center;color:#9aa0a6;padding:40px">
|
||||||
@@ -454,7 +454,7 @@
|
|||||||
<table v-else style="width:100%;border-collapse:collapse;margin-bottom:16px">
|
<table v-else style="width:100%;border-collapse:collapse;margin-bottom:16px">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background:#1a2332;text-align:left">
|
<tr style="background:#1a2332;text-align:left">
|
||||||
<th style="padding:10px 8px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">名称</th>
|
<th style="padding:10px 8px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">申请时间</th>
|
||||||
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J1</th>
|
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J1</th>
|
||||||
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J2</th>
|
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J2</th>
|
||||||
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J3</th>
|
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J3</th>
|
||||||
@@ -606,30 +606,33 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width:60px">序号</th>
|
<th style="width:60px">序号</th>
|
||||||
<th>报关单号</th>
|
<th>报关单号</th>
|
||||||
<th>名称</th>
|
<th>申请时间</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>机器数</th>
|
<th>机器数</th>
|
||||||
<th style="width:80px">操作</th>
|
<th style="width:80px">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(item, idx) in customsPageData" :key="item.id || idx"
|
<tr v-for="(item, idx) in customsPageData" :key="(item.customs && item.customs.id) || item.id || idx"
|
||||||
class="clickable-row"
|
class="clickable-row"
|
||||||
:class="{ 'row-selected': selectedCustomsId === (item.id || item.customsId || item.customs_id) }"
|
:class="{ 'row-selected': selectedCustomsId === ((item.customs && item.customs.id) || item.id || item.customsId || item.customs_id) }"
|
||||||
@click="selectCustomsRow(item, idx)">
|
@click="selectCustomsRow(item)">
|
||||||
<td>{% raw %}{{ (customsPage - 1) * customsPageSize + idx + 1 }}{% endraw %}</td>
|
<td>{% raw %}{{ (customsPage - 1) * customsPageSize + idx + 1 }}{% endraw %}</td>
|
||||||
<td><strong>{% raw %}{{ item.customsNo || item.customs_no || item.id || '-' }}{% endraw %}</strong></td>
|
<td><strong>{% raw %}{{ (item.customs && item.customs.customsCode) || item.orderCode || (item.customs && item.customs.id) || '-' }}{% endraw %}</strong></td>
|
||||||
<td>{% raw %}{{ item.name || item.customsName || item.customs_name || '-' }}{% endraw %}</td>
|
<td>{% raw %}{{ item.orderCode || item.drawCode || '-' }}{% endraw %}</td>
|
||||||
<td><span class="badge" :class="'badge-' + (item.status || 'unknown')">{% raw %}{{ item.status || '未知' }}{% endraw %}</span></td>
|
<td><span class="badge" :class="((item.customs && item.customs.customsCode) ? 'badge-success' : 'badge-pending')">{% raw %}{{ (item.customs && item.customs.customsCode) ? '已报关' : '待报关' }}{% endraw %}</span></td>
|
||||||
<td>{% raw %}{{ item.machineCount || item.machine_count || item.machineNum || item.machine_num || '?' }}{% endraw %}</td>
|
<td>{% raw %}{{ (item.customs && item.customs.orderId) ? item.customs.orderId.split(',').length : '?' }}{% endraw %}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-small btn-primary" @click.stop="selectCustomsRow(item, idx)">
|
<button class="btn btn-small btn-primary" @click.stop="selectCustomsRow(item)">
|
||||||
📦 查看机器
|
📦 查看机器
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-small btn-success" style="margin-left:4px" @click.stop="startInspection(item)">
|
||||||
|
🔍 开始查验
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!customsPageData.length && !customsLoading">
|
<tr v-if="!customsPageData.length && !customsLoading">
|
||||||
<td colspan="6" style="text-align:center;color:#8899aa;padding:24px">暂无报关单数据</td>
|
<td colspan="7" style="text-align:center;color:#8899aa;padding:24px">暂无报关单数据</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -657,21 +660,23 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:50px">序号</th>
|
<th style="width:50px">序号</th>
|
||||||
<th>机器编号</th>
|
<th>物料编码</th>
|
||||||
<th>机器名称</th>
|
<th>物料名称</th>
|
||||||
<th>型号</th>
|
<th>规格</th>
|
||||||
<th>二维码值</th>
|
<th>序列号</th>
|
||||||
<th>状态</th>
|
<th>数量</th>
|
||||||
|
<th>查验数量</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(m, mi) in customsMachines" :key="mi">
|
<tr v-for="(m, mi) in customsMachines" :key="mi">
|
||||||
<td>{% raw %}{{ mi + 1 }}{% endraw %}</td>
|
<td>{% raw %}{{ mi + 1 }}{% endraw %}</td>
|
||||||
<td><strong>{% raw %}{{ m.serialNumber || m.serial_number || m.serialNo || m.serial_no || m.machineNo || m.machine_no || m.id || '-' }}{% endraw %}</strong></td>
|
<td><strong>{% raw %}{{ m.inventoryCode || m.machineCode || '-' }}{% endraw %}</strong></td>
|
||||||
<td>{% raw %}{{ m.name || m.machineName || m.machine_name || m.model || '-' }}{% endraw %}</td>
|
<td>{% raw %}{{ m.inventoryName || m.machineName || m.name || '-' }}{% endraw %}</td>
|
||||||
<td>{% raw %}{{ m.modelName || m.model_name || m.model || '-' }}{% endraw %}</td>
|
<td>{% raw %}{{ m.inventorySpecification || m.spec || '-' }}{% endraw %}</td>
|
||||||
<td style="font-family:monospace;color:#4fc3f7">{% raw %}{{ m.qrValue || m.qr_value || m.qr || m.qrCode || m.qr_code || m.serialNumber || m.serial_number || '-' }}{% endraw %}</td>
|
<td style="font-family:monospace;color:#4fc3f7">{% raw %}{{ m.serialNumber || m.serialNumbers || m.qrValue || '-' }}{% endraw %}</td>
|
||||||
<td><span class="badge" :class="'badge-' + (m.status || 'normal')">{% raw %}{{ m.status || '正常' }}{% endraw %}</span></td>
|
<td>{% raw %}{{ m.quantify || m.quantity || (m.quantify ? m.quantify : '?') }}{% endraw %}</td>
|
||||||
|
<td><span :class="(m.inspectionCount > 0) ? 'badge badge-success' : 'badge badge-pending'">{% raw %}{{ m.inspectionCount || 0 }}{% endraw %}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -697,7 +702,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 机械臂截图弹窗 -->
|
<!-- 机械臂截图弹窗 -->
|
||||||
<div class="modal-overlay" v-if="showArmSnapshot" @click.self="showArmSnapshot = false">
|
<div class="modal-overlay" v-if="showArmSnapshot && armSnapshotUrl" @click.self="showArmSnapshot = false">
|
||||||
<div class="modal" style="max-width:800px">
|
<div class="modal" style="max-width:800px">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
<h3>📸 机械臂摄像头截图</h3>
|
<h3>📸 机械臂摄像头截图</h3>
|
||||||
@@ -713,6 +718,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/vue3.global.prod.js?v=20260526a"></script>
|
<script src="/static/js/vue3.global.prod.js?v=20260526a"></script>
|
||||||
<script src="/static/js/setting.js?v=20260612a"></script>
|
<script src="/static/js/setting.js?v=20260616f"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ from utils.nav2_navigator import Nav2Navigator, Nav2Status
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
ROS2_SETUP_CMD = "source /opt/ros/humble/setup.bash && source ~/agv_pro_ros2/install/setup.bash"
|
ROS2_SETUP_CMD = "source /opt/ros/humble/setup.bash && source ~/agv_pro_ros2/install/setup.bash"
|
||||||
from config import ARM_CAMERA_CONFIG
|
from config import ARM_CAMERA_CONFIG, UPLOAD_CONFIG, ZHIJIAN_AUTH_TOKEN
|
||||||
ARM_CAMERA_SNAPSHOT = ARM_CAMERA_CONFIG["snapshot_url"]
|
ARM_CAMERA_SNAPSHOT = ARM_CAMERA_CONFIG["snapshot_url"]
|
||||||
PHOTOS_DIR = "/home/elephant/photos"
|
PHOTOS_DIR = "/home/elephant/photos"
|
||||||
UPLOAD_URL = "https://ts.zhijian168.com/prod-api/file/uploadImage"
|
# UPLOAD_CONFIG["url"] 随环境切换动态变化,每次使用时直接读取
|
||||||
|
|
||||||
# 二维码扫描重试参数
|
# 二维码扫描重试参数
|
||||||
QR_SCAN_TIMEOUT = 5 # 单次扫描超时
|
QR_SCAN_TIMEOUT = 5 # 单次扫描超时
|
||||||
@@ -719,6 +719,7 @@ class MissionExecutorV3:
|
|||||||
try:
|
try:
|
||||||
resp = requests.get(ARM_CAMERA_SNAPSHOT, timeout=QR_SCAN_TIMEOUT)
|
resp = requests.get(ARM_CAMERA_SNAPSHOT, timeout=QR_SCAN_TIMEOUT)
|
||||||
if resp.status_code != 200 or not resp.content:
|
if resp.status_code != 200 or not resp.content:
|
||||||
|
self._log(f" 📷 arm snapshot attempt {attempt+1}: HTTP {resp.status_code}, size={len(resp.content) if resp.content else 0}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
arr = np.frombuffer(resp.content, dtype=np.uint8)
|
arr = np.frombuffer(resp.content, dtype=np.uint8)
|
||||||
@@ -735,12 +736,17 @@ class MissionExecutorV3:
|
|||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _request_manual_qr(self) -> Optional[str]:
|
def _request_manual_qr(self, message: str = None) -> Optional[str]:
|
||||||
"""暂停任务,等待手动输入(支持重新扫描)"""
|
"""暂停任务,等待手动输入(支持重新扫描)
|
||||||
|
message: 自定义弹窗消息(None 则使用默认消息)"""
|
||||||
while True:
|
while True:
|
||||||
self.status = MissionStatus.WAITING_QR
|
self.status = MissionStatus.WAITING_QR
|
||||||
self.report["status"] = "waiting_qr"
|
self.report["status"] = "waiting_qr"
|
||||||
self.report["step"] = "等待手动输入二维码"
|
self.report["step"] = message or "等待手动输入二维码"
|
||||||
|
self.report["qr_message"] = message or "所有姿态均未识别到二维码,请手动输入:"
|
||||||
|
if message:
|
||||||
|
self._log(f" ⌨️ {message}")
|
||||||
|
else:
|
||||||
self._log(" ⌨️ 弹窗等待手动输入二维码...")
|
self._log(" ⌨️ 弹窗等待手动输入二维码...")
|
||||||
|
|
||||||
self._qr_event.clear()
|
self._qr_event.clear()
|
||||||
@@ -773,8 +779,61 @@ class MissionExecutorV3:
|
|||||||
# ==================== 机型查询 ====================
|
# ==================== 机型查询 ====================
|
||||||
|
|
||||||
def _lookup_model(self, qr_value: Optional[str]) -> str:
|
def _lookup_model(self, qr_value: Optional[str]) -> str:
|
||||||
"""TODO: 后续通过 HTTP 接口查询机型"""
|
"""通过 /api/customs/printer 接口查询机型,同时更新查验计数
|
||||||
|
如果机型不在当前报关单中,弹窗要求重新扫码/输入"""
|
||||||
|
if not qr_value:
|
||||||
return "机器1"
|
return "机器1"
|
||||||
|
while True:
|
||||||
|
if self._stop.is_set():
|
||||||
|
return "机器1"
|
||||||
|
try:
|
||||||
|
printer_url = f"http://127.0.0.1:5000/api/customs/printer?serialNumber={qr_value}"
|
||||||
|
self._log(f" 🔍 查询机型 → {printer_url}")
|
||||||
|
resp = requests.get(printer_url, timeout=10)
|
||||||
|
self._log(f" 📡 printer 响应 HTTP {resp.status_code}: {resp.text[:500]}")
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
if data.get("ok"):
|
||||||
|
model = data.get("modelName", "机器1")
|
||||||
|
inv_code = data.get("inventoryCode", "")
|
||||||
|
matched = data.get("matchedItem")
|
||||||
|
has_inspection = data.get("hasInspection", False)
|
||||||
|
self._log(f" 📊 解析结果: modelName={model}, inventoryCode={inv_code}, hasInspection={has_inspection}, matched={'yes' if matched else 'no'}")
|
||||||
|
if matched:
|
||||||
|
self._log(f" 🏷️ 机型: {model} (物料:{inv_code}) — 查验 {matched['inspected']}/{matched['quantify']}")
|
||||||
|
return model
|
||||||
|
elif has_inspection and not matched:
|
||||||
|
# 有查验但机型不在报关单中 → 弹窗
|
||||||
|
self._log(f" ⚠️ 机型「{model}」(物料:{inv_code})不在当前报关单中")
|
||||||
|
new_qr = self._request_manual_qr(
|
||||||
|
f"二维码「{qr_value}」对应机型「{model}」不在当前报关单中,\n请重新扫描或手动输入正确的二维码:"
|
||||||
|
)
|
||||||
|
if new_qr is None or self._stop.is_set():
|
||||||
|
return model # 用户跳过/停止,保留原机型
|
||||||
|
qr_value = new_qr
|
||||||
|
continue # 用新二维码重试
|
||||||
|
else:
|
||||||
|
# 无查验或匹配成功
|
||||||
|
self._log(f" 🏷️ 机型: {model} (物料:{inv_code})")
|
||||||
|
return model
|
||||||
|
else:
|
||||||
|
self._log(f" ⚠️ printer 返回 ok=false: {data}")
|
||||||
|
# API 失败也弹窗
|
||||||
|
self._log(f" ⚠️ 查询机型失败, HTTP {resp.status_code}")
|
||||||
|
new_qr = self._request_manual_qr(
|
||||||
|
f"无法查询二维码「{qr_value}」对应的机型,\n请重新扫描或手动输入正确的二维码:"
|
||||||
|
)
|
||||||
|
if new_qr is None or self._stop.is_set():
|
||||||
|
return "机器1"
|
||||||
|
qr_value = new_qr
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f" ⚠️ 查询机型失败: {e}")
|
||||||
|
new_qr = self._request_manual_qr(
|
||||||
|
f"查询机型接口异常,请重新扫描或手动输入正确的二维码:"
|
||||||
|
)
|
||||||
|
if new_qr is None or self._stop.is_set():
|
||||||
|
return "机器1"
|
||||||
|
qr_value = new_qr
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_model(models: list, name: str) -> Optional[dict]:
|
def _find_model(models: list, name: str) -> Optional[dict]:
|
||||||
@@ -852,7 +911,7 @@ class MissionExecutorV3:
|
|||||||
try:
|
try:
|
||||||
resp = requests.get(ARM_CAMERA_SNAPSHOT, timeout=10)
|
resp = requests.get(ARM_CAMERA_SNAPSHOT, timeout=10)
|
||||||
if resp.status_code != 200 or not resp.content:
|
if resp.status_code != 200 or not resp.content:
|
||||||
logger.error("arm snapshot 请求失败")
|
self._log(f" 📷 拍照 arm snapshot 失败: HTTP {resp.status_code}, size={len(resp.content) if resp.content else 0}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 生成文件名(用于上传)
|
# 生成文件名(用于上传)
|
||||||
@@ -861,13 +920,14 @@ class MissionExecutorV3:
|
|||||||
|
|
||||||
# 直接上传到服务器(不保存本地)
|
# 直接上传到服务器(不保存本地)
|
||||||
if qr_value:
|
if qr_value:
|
||||||
|
self._log(f" 📷 拍照成功 {len(resp.content)} bytes → {fname}")
|
||||||
self._upload_photo_bytes(fname, resp.content, qr_value, upload_index)
|
self._upload_photo_bytes(fname, resp.content, qr_value, upload_index)
|
||||||
else:
|
else:
|
||||||
self._log(" ⚠️ 无二维码,跳过上传")
|
self._log(" ⚠️ 无二维码,跳过上传")
|
||||||
|
|
||||||
return fname # 返回文件名(用于日志)
|
return fname # 返回文件名(用于日志)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"拍照异常: {e}")
|
self._log(f" ❌ 拍照异常: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _upload_photo(self, filepath: str, serial_number: str, index: int) -> bool:
|
def _upload_photo(self, filepath: str, serial_number: str, index: int) -> bool:
|
||||||
@@ -880,13 +940,14 @@ class MissionExecutorV3:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
filename = os.path.basename(filepath)
|
filename = os.path.basename(filepath)
|
||||||
headers = {
|
headers = {"Authorization": ZHIJIAN_AUTH_TOKEN}
|
||||||
"Authorization": "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX2tleSI6ImZhNTNkZTZiLWE3NjYtNDZmNC05MDUyLTQ2MjUzZTAyNjdmNSIsInVzZXJuYW1lIjoiYWRtaW4ifQ.lC4vKThZo4aAOLsekm2kPgaEJRqRx-YDQWKfHFqxdPNESCKy57l3eIqaKTj2ZjAMaoYAwYlMrv5M1zAOJsO_PA"
|
upload_url = UPLOAD_CONFIG["url"]
|
||||||
}
|
self._log(f" 📤 上传请求 → {upload_url} | serialNumber={serial_number} | index={index} | file={filename}")
|
||||||
with open(filepath, "rb") as f:
|
with open(filepath, "rb") as f:
|
||||||
files = {"file": (filename, f, "image/jpeg")}
|
files = {"file": (filename, f, "image/jpeg")}
|
||||||
data = {"serialNumber": serial_number, "index": str(index)}
|
data = {"serialNumber": serial_number, "index": str(index)}
|
||||||
resp = requests.post(UPLOAD_URL, files=files, data=data, headers=headers, timeout=30)
|
resp = requests.post(upload_url, files=files, data=data, headers=headers, timeout=30)
|
||||||
|
self._log(f" 📡 上传响应 HTTP {resp.status_code}: {resp.text[:300]}")
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
self._log(f" ☁️ 上传成功 [{index}]: {filename}")
|
self._log(f" ☁️ 上传成功 [{index}]: {filename}")
|
||||||
return True
|
return True
|
||||||
@@ -907,12 +968,13 @@ class MissionExecutorV3:
|
|||||||
index: 上传序号(从1开始递增)
|
index: 上传序号(从1开始递增)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
headers = {
|
headers = {"Authorization": ZHIJIAN_AUTH_TOKEN}
|
||||||
"Authorization": "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX2tleSI6ImZhNTNkZTZiLWE3NjYtNDZmNC05MDUyLTQ2MjUzZTAyNjdmNSIsInVzZXJuYW1lIjoiYWRtaW4ifQ.lC4vKThZo4aAOLsekm2kPgaEJRqRx-YDQWKfHFqxdPNESCKy57l3eIqaKTj2ZjAMaoYAwYlMrv5M1zAOJsO_PA"
|
upload_url = UPLOAD_CONFIG["url"]
|
||||||
}
|
self._log(f" 📤 上传请求(内存) → {upload_url} | serialNumber={serial_number} | index={index} | file={filename}")
|
||||||
files = {"file": (filename, image_data, "image/jpeg")}
|
files = {"file": (filename, image_data, "image/jpeg")}
|
||||||
data = {"serialNumber": serial_number, "index": str(index)}
|
data = {"serialNumber": serial_number, "index": str(index)}
|
||||||
resp = requests.post(UPLOAD_URL, files=files, data=data, headers=headers, timeout=30)
|
resp = requests.post(upload_url, files=files, data=data, headers=headers, timeout=30)
|
||||||
|
self._log(f" 📡 上传响应 HTTP {resp.status_code}: {resp.text[:300]}")
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
self._log(f" ☁️ 上传成功 [{index}]: {filename}")
|
self._log(f" ☁️ 上传成功 [{index}]: {filename}")
|
||||||
return True
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user