diff --git a/agv_app/app.py b/agv_app/app.py index a663f21..d46d745 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -8,10 +8,11 @@ import time import logging import threading import subprocess +import requests from flask import Flask, render_template, jsonify, request, Response, send_from_directory 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.agv_controller_ros2 import AGVController from utils.qr_scanner import QRScanner @@ -54,6 +55,7 @@ class GlobalState: self.qr_config = [] # 二维码配置(独立点位列表) self.navigator = None # Nav2Navigator 实例 self.current_customs = None # 当前设定的报关单信息 + self.inspection = None # 查验状态 {customs_id, customs_name, items: [{inventoryCode, inventoryName, spec, quantify, inspected}]} self.error_msg = "" # 错误弹窗消息(waiting_error 状态时) self.lock = threading.Lock() @@ -1213,10 +1215,8 @@ def api_arm_camera_refresh(): @app.route("/api/camera/arm_preview") def api_arm_camera_preview(): - """代理机械臂 MJPEG 视频流,逐帧上下翻转后返回。""" + """代理机械臂 MJPEG 视频流,直透返回(不翻转)。""" import requests - import cv2 - import numpy as np try: upstream = requests.get( ARM_CAMERA_CONFIG["url"], @@ -1241,16 +1241,8 @@ def api_arm_camera_preview(): if start != -1 and end != -1 and end > start: jpg_data = buf[start: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: break finally: @@ -1344,6 +1336,11 @@ def api_agv_reset(): def api_mission_start(): """开始执行任务(V3: M×N Grid 蛇形路径)""" data = request.json or {} + + # 必须先设置报关单(开始查验) + if not gs.inspection: + return jsonify({"ok": False, "error": "请先在「设置→报关单」中选择报关单并点击「开始查验」"}), 400 + single_step = bool(data.get("single_step", False)) # 任务步骤控制开关 options = { @@ -1538,6 +1535,18 @@ def api_mission_state(): result["waiting_step"] = 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) @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) ========== -_ZHIJIAN_BASE = "https://ts.zhijian168.com/prod-api/zhijian/integration" -_ZHIJIAN_AUTH = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX2tleSI6ImZhNTNkZTZiLWE3NjYtNDZmNC05MDUyLTQ2MjUzZTAyNjdmNSIsInVzZXJuYW1lIjoiYWRtaW4ifQ.lC4vKThZo4aAOLsekm2kPgaEJRqRx-YDQWKfHFqxdPNESCKy57l3eIqaKTj2ZjAMaoYAwYlMrv5M1zAOJsO_PA" +def _get_zhijian_base(): + """动态获取报关单 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") @@ -1767,36 +1811,49 @@ def api_customs_list(): import requests page = request.args.get("pageNum", 1) 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: 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: + logger.warning(f"[customs/list] ⚠️ 返回非200: {r.status_code}") return jsonify({"ok": False, "error": f"报关单API返回 {r.status_code}"}), 502 data = r.json() + rows = data.get("rows", []) + logger.info(f"[customs/list] ✅ 获取到 {len(rows)} 条报关单") return jsonify({"ok": True, "data": data}) except Exception as e: - logger.error(f"获取报关单列表失败: {e}") + logger.error(f"[customs/list] ❌ 失败: {e}") return jsonify({"ok": False, "error": str(e)}), 502 @app.route("/api/customs/machines") def api_customs_machines(): """根据报关单 ID 获取机器列表(代理外部 API) - 查询参数: customsId=xxx + 数据源:cjt_customs_item 表 → Java customsMachines 接口 """ import requests customs_id = request.args.get("customsId", "") + logger.info(f"[customs/machines] 📥 customsId={customs_id}") if not customs_id: + logger.warning("[customs/machines] ⚠️ 缺少 customsId") return jsonify({"ok": False, "error": "缺少 customsId 参数"}), 400 - url = f"{_ZHIJIAN_BASE}/customsMachines?customsId={customs_id}" + 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) + logger.info(f"[customs/machines] 📡 响应 HTTP {r.status_code}, body长度={len(r.text)}") if r.status_code != 200: - return jsonify({"ok": False, "error": f"机器列表API返回 {r.status_code}"}), 502 - data = r.json() - return jsonify({"ok": True, "data": data}) + logger.warning(f"[customs/machines] ⚠️ 返回非200: {r.status_code}, body={r.text[:300]}") + return jsonify({"ok": False, "error": "机器列表API返回非200"}), 502 + 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: - logger.error(f"获取报关单机器列表失败: {e}") + logger.error(f"[customs/machines] ❌ 失败: {e}") return jsonify({"ok": False, "error": str(e)}), 502 @@ -1820,6 +1877,188 @@ def api_customs_selected_get(): 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/") def photos(name): diff --git a/agv_app/config.py b/agv_app/config.py index 55593ad..95f938c 100644 --- a/agv_app/config.py +++ b/agv_app/config.py @@ -47,9 +47,30 @@ ARM_CAMERA_CONFIG = { "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 上传 ========== UPLOAD_CONFIG = { - "url": "https://ts.zhijian168.com/prod-api/file/uploadImage", + "url": f"{ZHIJIAN_BASE_URL}{API_PREFIX}/file/uploadImage", "timeout": 30, "max_retries": 3, } diff --git a/agv_app/static/css/style.css b/agv_app/static/css/style.css index fe2e0e0..e555658 100644 --- a/agv_app/static/css/style.css +++ b/agv_app/static/css/style.css @@ -59,6 +59,49 @@ a:hover { text-decoration: underline; } .status-item.paused { background: #3a2a1a; color: #ff9800; } .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 { background: #1a2332; @@ -466,7 +509,7 @@ a:hover { text-decoration: underline; } object-fit: cover; } .camera-img.arm { - transform: rotate(180deg); + /* no flip */ } .camera-placeholder { @@ -1191,3 +1234,74 @@ a:hover { text-decoration: underline; } justify-content: center; 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; +} diff --git a/agv_app/static/js/app.js b/agv_app/static/js/app.js index f0c91a8..1f39c71 100644 --- a/agv_app/static/js/app.js +++ b/agv_app/static/js/app.js @@ -20,7 +20,9 @@ createApp({ agvCameraError: false, hasAgvCamera: false, // AGV 车体是否有可用相机 armCameraError: false, - reconnectingDevice: null + reconnectingDevice: null, + // 环境切换 + testMode: true, } }, computed: { @@ -38,6 +40,7 @@ createApp({ mounted() { this.refresh() this.refreshCameraCapabilities() + this.loadEnvMode() setInterval(this.refreshStatus, 3000) this.refreshCams() setInterval(() => this.refreshCams(), 2000) @@ -133,6 +136,36 @@ createApp({ } else { 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') diff --git a/agv_app/static/js/running.js b/agv_app/static/js/running.js index 598c519..5445620 100644 --- a/agv_app/static/js/running.js +++ b/agv_app/static/js/running.js @@ -28,6 +28,7 @@ createApp({ errorMsg: '', waitingStep: false, stepLabel: '', + qrMessage: '所有姿态均未识别到二维码,请手动输入:', // 任务步骤控制开关(机械臂初始化并入AGV移动) agvMoveEnabled: true, qrScanEnabled: true, @@ -36,6 +37,8 @@ createApp({ // 速度控制 agvSpeed: 1.0, armSpeed: 1000, + // 查验 + inspection: null, } }, computed: { @@ -51,6 +54,14 @@ createApp({ } 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() { this.poll() @@ -101,6 +112,7 @@ createApp({ if (data.grid) this.missionGrid = data.grid if (data.point_status) this.pointStatus = data.point_status if (data.machine_status) this.machineStatus = data.machine_status + if (data.inspection) this.inspection = data.inspection this.armCameraOpened = data.arm_camera_opened if (this.armCameraOpened && !this.armPreviewUrl) { this.armPreviewUrl = API + '/api/camera/arm_preview' @@ -122,6 +134,11 @@ createApp({ this.waitingStep = false } + // QR 弹窗消息 + if (data.qr_message) { + this.qrMessage = data.qr_message + } + // QR 弹窗(防止提交后重复弹出) if (this.missionState !== 'waiting_qr') { this.qrSubmitting = false @@ -129,6 +146,9 @@ createApp({ if (this.missionState === 'waiting_qr' && !this.showQrModal && !this.qrSubmitting) { this.showQrModal = true this.qrValue = '' + if (!this.qrMessage) { + this.qrMessage = '所有姿态均未识别到二维码,请手动输入:' + } } // 完成后获取报告 @@ -156,6 +176,11 @@ createApp({ }, async startMission() { if (this.missionState !== 'idle') return + // 没有设置报关单时阻止启动(后端也会校验,这里提前友好提示) + if (!this.inspection) { + alert('⚠️ 请先在「设置→报关单」中选择报关单并点击「开始查验」') + return + } this.logs = [] this.progress = 0 this.report = null @@ -186,6 +211,11 @@ createApp({ }, async startSingleStep() { if (this.missionState !== 'idle') return + // 没有设置报关单时阻止启动(后端会校验,这里提前友好提示) + if (!this.inspection) { + alert('⚠️ 请先在「设置→报关单」中选择报关单并点击「开始查验」') + return + } this.logs = [] this.progress = 0 this.report = null diff --git a/agv_app/static/js/setting.js b/agv_app/static/js/setting.js index 625b394..4cbe574 100644 --- a/agv_app/static/js/setting.js +++ b/agv_app/static/js/setting.js @@ -82,8 +82,9 @@ createApp({ this.refreshAngles() this.loadQrConfigs() 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: { customsTotalPages() { return Math.max(1, Math.ceil(this.customsTotal / this.customsPageSize)) @@ -1203,64 +1204,121 @@ createApp({ alert('❌ 复位请求失败: ' + e.message) } }, - }, - // ===== 报关单方法 ===== - async loadCustomsList() { - this.customsLoading = true - try { - const url = API + '/api/customs/list?pageNum=' + this.customsPage + '&pageSize=' + this.customsPageSize - const res = await fetch(url) - const d = await res.json() - if (d.ok && d.data) { - const raw = d.data - let list = [] - let total = 0 - if (raw.rows) { list = raw.rows; total = raw.total || list.length } - else if (raw.records) { list = raw.records; total = raw.total || list.length } - else if (Array.isArray(raw)) { list = raw; total = list.length } - else if (raw.data && raw.data.rows) { list = raw.data.rows; total = raw.data.total || list.length } - else if (raw.data && raw.data.records) { list = raw.data.records; total = raw.data.total || list.length } - else if (raw.data && Array.isArray(raw.data)) { list = raw.data; total = list.length } - this.customsList = list - this.customsTotal = total || list.length - } else { + // ===== 报关单方法 ===== + async loadCustomsList() { + this.customsLoading = true + try { + 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 d = await res.json() + if (d.ok && d.data) { + const raw = d.data + let list = [] + let total = 0 + if (raw.rows) { list = raw.rows; total = raw.total || list.length } + else if (raw.records) { list = raw.records; total = raw.total || list.length } + else if (Array.isArray(raw)) { list = raw; total = list.length } + else if (raw.data && raw.data.rows) { list = raw.data.rows; total = raw.data.total || list.length } + else if (raw.data && raw.data.records) { list = raw.data.records; total = raw.data.total || list.length } + else if (raw.data && Array.isArray(raw.data)) { list = raw.data; total = list.length } + this.customsList = list + this.customsTotal = total || list.length + } else { + this.customsList = [] + this.customsTotal = 0 + } + } catch (e) { + console.error('加载报关单列表失败', e) this.customsList = [] this.customsTotal = 0 + } finally { + this.customsLoading = false } - } catch (e) { - console.error('加载报关单列表失败', e) - this.customsList = [] - this.customsTotal = 0 - } finally { - this.customsLoading = false - } - }, - async selectCustomsRow(item) { - const id = item.id || item.customsId || item.customs_id || '' - if (!id) return - this.selectedCustomsId = id - this.selectedCustomsName = item.customsNo || item.customs_no || item.name || item.customsName || item.customs_name || id - this.customsMachines = [] - try { - const res = await fetch(API + '/api/customs/machines?customsId=' + encodeURIComponent(id)) - const d = await res.json() - if (d.ok && d.data) { - const raw = d.data - let machines = [] - if (raw.rows) { machines = raw.rows } - else if (raw.records) { machines = raw.records } - else if (raw.data && Array.isArray(raw.data)) { machines = raw.data } - else if (Array.isArray(raw)) { machines = raw } - else if (Array.isArray(raw.data)) { machines = raw.data } - this.customsMachines = machines - } else { + }, + async selectCustomsRow(item) { + // 新数据结构: { customs:{id,orderId,..}, orderCode, drawCode } + const id = (item.customs && item.customs.id) || item.id || item.customsId || item.customs_id || '' + if (!id) return + this.selectedCustomsId = id + this.selectedCustomsName = (item.customs && item.customs.customsCode) || item.orderCode || item.drawCode || id + this.customsMachines = [] + try { + const url = API + '/api/customs/machines?customsId=' + encodeURIComponent(id) + const res = await fetch(url) + const d = await res.json() + if (d.ok && d.data) { + const raw = d.data + let machines = [] + // customsMachines 返回格式: {"code":"0","data":[{serialNumber,inventoryName,...}]} + if (raw.rows) { machines = raw.rows } + else if (raw.records) { machines = raw.records } + else if (raw.data && Array.isArray(raw.data)) { machines = raw.data } + else if (Array.isArray(raw)) { machines = raw } + else if (Array.isArray(raw.data)) { machines = raw.data } + this.customsMachines = machines + } else { + this.customsMachines = [] + } + } catch (e) { + console.error('加载机器列表失败', e) this.customsMachines = [] } - } catch (e) { - console.error('加载机器列表失败', e) - 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') diff --git a/agv_app/templates/index.html b/agv_app/templates/index.html index b6e0045..6df181b 100644 --- a/agv_app/templates/index.html +++ b/agv_app/templates/index.html @@ -16,7 +16,13 @@ ⚙️ 设置 ▶️ 运行 -
+
+ {% raw %}{{ statusText }}{% endraw %} diff --git a/agv_app/templates/running.html b/agv_app/templates/running.html index 28b9138..a54e34a 100644 --- a/agv_app/templates/running.html +++ b/agv_app/templates/running.html @@ -4,7 +4,7 @@ 运行监控 - AGV 拍摄系统 - +
@@ -48,6 +48,30 @@
+ +
+

🔍 查验进度 — {% raw %}{{ inspection.customsName }}{% endraw %}

+

+ 总进度: {% raw %}{{ inspectionTotal }}{% endraw %} / {% raw %}{{ inspectionTarget }}{% endraw %} 台 + ✅ 已完成 +

+
+
+
{% raw %}{{ item.inventoryName }}{% endraw %}
+
{% raw %}{{ item.inventoryCode }}{% endraw %}
+
{% raw %}{{ item.spec }}{% endraw %}
+
+ {% raw %}{{ item.inspected }}{% endraw %} + / + {% raw %}{{ item.quantify }}{% endraw %} +
+
+
+
+
+
+
+

🎛️ 任务步骤控制

@@ -202,7 +226,7 @@