commit 94c1043f4d2de46b6e63fa7fd8277555e26a06e3 Author: ywb <347742090@qq.com> Date: Thu May 14 21:43:35 2026 +0800 init diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..fd73781 Binary files /dev/null and b/.DS_Store differ diff --git a/agv_app/__init__.py b/agv_app/__init__.py new file mode 100644 index 0000000..34f275e --- /dev/null +++ b/agv_app/__init__.py @@ -0,0 +1,3 @@ +from .app import app + +__all__ = ["app"] diff --git a/agv_app/app.py b/agv_app/app.py new file mode 100644 index 0000000..ce3de20 --- /dev/null +++ b/agv_app/app.py @@ -0,0 +1,1205 @@ +""" +AGV 拍摄系统 - Flask 主程序 +运行在 AGV 上,端口 5000 +""" +import os +import json +import time +import logging +import threading +import subprocess +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 utils.arm_client import ArmClient +from utils.agv_controller_ros2 import AGVController +from utils.qr_scanner import QRScanner +from utils.image_uploader import ImageUploader +from utils.mission_executor import MissionExecutor, TaskStatus +from utils.map_navigator import MapNavigator, NavStatus + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" +) +logger = logging.getLogger("agv_app") + +app = Flask(__name__, template_folder="templates", static_folder="static", static_url_path="/static") +app.config["SECRET_KEY"] = SERVER_CONFIG["secret_key"] +CORS(app) + +# ========== 全局状态 ========== +class GlobalState: + def __init__(self): + self.state = State.IDLE # setting / running / paused / idle + self.mission_data = None # 当前任务配置 + self.mission_report = None # 任务执行报告 + self.arm_client = None # ArmClient 实例 + self.agv_controller = None # AGVController 实例 + self.qr_scanner = None # QRScanner 实例 + self.camera_opened = False + self.arm_camera_opened = False + self.map_config = {} # 地图配置 + self.points_config = [] # 点位配置 + self.models_config = [] # 机型配置(姿态) + self.mission_config = { # 任务配置(M×N网格) + "rows": 2, + "cols": 3, + "grid": [], # M×N 布尔矩阵 + "positions": [] # 独立点位配置 [{row, col, side, coords, poses}] + } + self.machines_config = [] # 机器配置(每台机器的正面/背面点位+姿态) + self.navigator = None # MapNavigator 实例 + self.lock = threading.Lock() + + def reset(self): + with self.lock: + self.state = State.IDLE + self.mission_data = None + self.mission_report = None + +gs = GlobalState() + + +# ========== 辅助函数 ========== +def get_data_path(name): + return os.path.join(DATA_DIR, name) + +def save_json(name, data): + with open(get_data_path(name), "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + +def load_json(name, default=None): + path = get_data_path(name) + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + return default if default is not None else {} + + +# ========== 启动时加载持久化配置 ========== +def load_persisted_config(): + """Flask 启动时加载已保存的配置""" + # 加载地图配置 + map_cfg = load_json("map_config.json") + if map_cfg and "map_yaml" in map_cfg: + gs.map_config = map_cfg + print(f"[启动] 加载地图配置: {map_cfg['map_yaml']}") + + # 加载点位配置 + points_cfg = load_json("points_config.json", []) + if points_cfg: + gs.points_config = points_cfg + print(f"[启动] 加载点位配置: {len(points_cfg)} 个点位") + + # 加载机型配置(姿态) + models_cfg = load_json("models_config.json", []) + if models_cfg: + gs.models_config = models_cfg + print(f"[启动] 加载机型配置: {len(models_cfg)} 个机型") + + # 加载任务配置(网格尺寸) + mission_cfg = load_json("mission_config.json", {}) + if mission_cfg: + gs.mission_config = mission_cfg + print(f"[启动] 加载任务配置: {mission_cfg.get('rows', 0)}行×{mission_cfg.get('cols', 0)}列") + + # 加载机器配置(正面/背面点位+姿态) + machines_cfg = load_json("machines_config.json", []) + if machines_cfg: + gs.machines_config = machines_cfg + print(f"[启动] 加载机器配置: {len(machines_cfg)} 台机器") + +# 在 Flask 2.3+ 使用 @app.before_serving,兼容旧版用 before_first_request +try: + from flask import has_app_context + # Flask 2.3+ 方式 + with app.app_context(): + load_persisted_config() +except: + # 兼容旧版 Flask + @app.before_first_request + def startup_load(): + load_persisted_config() + +# ========== 页面路由 ========== +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/setting") +def setting_page(): + return render_template("setting.html") + +@app.route("/running") +def running_page(): + return render_template("running.html") + + +# ========== 系统状态 API ========== +@app.route("/api/status") +def api_status(): + """获取系统整体状态""" + with gs.lock: + # 实际验证机械臂连接(尝试发送一个简单命令) + arm_connected = False + if gs.arm_client and gs.arm_client._sock: + try: + # 设置短超时尝试获取角度,验证连接是否有效 + gs.arm_client._sock.settimeout(2) + ok, _ = gs.arm_client.get_angles() + arm_connected = ok + except: + arm_connected = False + # 连接已断开,清理 socket + if gs.arm_client: + gs.arm_client._sock = None + + # 实际验证 AGV 连接 + agv_connected = False + if gs.agv_controller: + agv_connected = gs.agv_controller.is_connected() + + return jsonify({ + "state": gs.state, + "agv_connected": agv_connected, + "arm_connected": arm_connected, + "camera_opened": gs.camera_opened, + "arm_camera_opened": gs.arm_camera_opened, + "map_loaded": bool(gs.map_config), + "points_count": len(gs.points_config), + "models_count": len(gs.models_config), + "mission_rows": gs.mission_config.get("rows", 0), + "mission_cols": gs.mission_config.get("cols", 0), + "machines_count": len(gs.machines_config) + }) + +@app.route("/api/system/connect", methods=["POST"]) +def api_connect(): + """连接 AGV 和机械臂""" + results = {"agv": False, "arm": False, "camera": False, "errors": []} + + # 连接 AGV + try: + gs.agv_controller = AGVController() + results["agv"] = gs.agv_controller.connect() + except Exception as e: + results["errors"].append(f"AGV: {e}") + + # 连接机械臂 + try: + gs.arm_client = ArmClient(ARM_CONFIG["host"], ARM_CONFIG["port"]) + results["arm"] = gs.arm_client.connect() + if results["arm"]: + # 尝试上电并激活 + gs.arm_client.power_on() + time.sleep(0.5) + gs.arm_client.state_on() + except Exception as e: + results["errors"].append(f"机械臂: {e}") + + # 打开摄像头 + try: + gs.qr_scanner = QRScanner(CAMERA_CONFIG["device_index"]) + results["camera"] = gs.qr_scanner.open() + gs.camera_opened = results["camera"] + except Exception as e: + results["errors"].append(f"摄像头: {e}") + + # 检查机械臂摄像头 + try: + import requests as _req + r2 = _req.get(ARM_CAMERA_CONFIG["url"], stream=True, timeout=3) + gs.arm_camera_opened = (r2.status_code == 200) + r2.close() + except: + gs.arm_camera_opened = False + + all_ok = results["agv"] and results["arm"] and results["camera"] + with gs.lock: + if all_ok: + gs.state = State.SETTING + else: + gs.state = State.IDLE + + return jsonify(results) + +@app.route("/api/system/disconnect", methods=["POST"]) +def api_disconnect(): + """断开所有连接""" + if gs.arm_client: + gs.arm_client.close() + gs.arm_client = None + if gs.agv_controller: + gs.agv_controller.disconnect() + gs.agv_controller = None + if gs.qr_scanner: + gs.qr_scanner.close() + gs.qr_scanner = None + gs.camera_opened = False + gs.state = State.IDLE + return jsonify({"ok": True}) + +@app.route("/api/device/connect", methods=["POST"]) +def api_device_connect(): + """连接单个设备,device: agv | arm | camera | arm_camera""" + data = request.json or {} + device = data.get("device", "") + result = {"device": device, "ok": False, "error": ""} + + done_event = threading.Event() + res_holder = {} + + def _do_connect(): + try: + if device == "agv": + gs.agv_controller = AGVController() + res_holder["ok"] = gs.agv_controller.connect() + if not res_holder["ok"]: + res_holder["error"] = "AGV 连接失败,请检查网络或 ROS2" + elif device == "arm": + gs.arm_client = ArmClient(ARM_CONFIG["host"], ARM_CONFIG["port"]) + res_holder["ok"] = gs.arm_client.connect() + if res_holder["ok"]: + gs.arm_client.power_on() + time.sleep(0.3) + gs.arm_client.state_on() + else: + res_holder["error"] = "机械臂连接失败" + elif device == "camera": + gs.qr_scanner = QRScanner(CAMERA_CONFIG["device_index"]) + res_holder["ok"] = gs.qr_scanner.open() + gs.camera_opened = res_holder["ok"] + if not res_holder["ok"]: + res_holder["error"] = "AGV 摄像头打开失败" + elif device == "arm_camera": + import requests as _req + r = _req.get(ARM_CAMERA_CONFIG["url"], stream=True, timeout=5) + res_holder["ok"] = (r.status_code == 200) + r.close() + gs.arm_camera_opened = res_holder["ok"] + if not res_holder["ok"]: + res_holder["error"] = "机械臂摄像头无响应" + else: + res_holder["error"] = f"未知设备: {device}" + except Exception as e: + res_holder["error"] = str(e) + finally: + done_event.set() + + t = threading.Thread(target=_do_connect) + t.daemon = True + t.start() + # 最多等 10 秒,超时则返回失败 + if not done_event.wait(10): + result["ok"] = False + result["error"] = "连接超时(10秒),设备无响应" + else: + result["ok"] = res_holder.get("ok", False) + result["error"] = res_holder.get("error", "") + + return jsonify(result) + + +# ========== 地图配置 API ========== +@app.route("/api/map/load", methods=["POST"]) +def api_map_load(): + """加载地图文件""" + data = request.json + map_dir = data.get("map_dir", MAP_CONFIG["map_dir"]) + map_file = data.get("map_file", MAP_CONFIG["map_file"]) + map_yaml = os.path.join(map_dir, map_file) + + if not os.path.exists(map_yaml): + return jsonify({"ok": False, "error": f"地图文件不存在: {map_yaml}"}), 400 + + gs.map_config = {"map_dir": map_dir, "map_file": map_file, "map_yaml": map_yaml} + save_json("map_config.json", gs.map_config) + return jsonify({"ok": True, "map": gs.map_config}) + +@app.route("/api/map/save", methods=["POST"]) +def api_map_save(): + """保存地图配置""" + data = request.json + gs.map_config = data + save_json("map_config.json", data) + return jsonify({"ok": True}) + +@app.route("/api/map/image") +def api_map_image(): + """返回地图图像(PNG)""" + if not gs.map_config or "map_yaml" not in gs.map_config: + return jsonify({"error": "地图未加载"}), 404 + + map_yaml = gs.map_config["map_yaml"] + map_dir = os.path.dirname(map_yaml) + + # 解析 YAML 获取 PGM 文件名 + try: + import yaml + with open(map_yaml, 'r') as f: + meta = yaml.safe_load(f) + pgm_file = meta.get('image', 'map.pgm') + pgm_path = os.path.join(map_dir, pgm_file) + + if not os.path.exists(pgm_path): + return jsonify({"error": f"PGM 文件不存在: {pgm_path}"}), 404 + + # 读取 PGM 并转换为 PNG + import cv2 + img = cv2.imread(pgm_path, cv2.IMREAD_GRAYSCALE) + if img is None: + return jsonify({"error": "无法读取 PGM 文件"}), 500 + + # 反转颜色(PGM 中 0=占用,255=空闲,ROS 地图显示习惯) + img = 255 - img + + # 编码为 PNG + _, buf = cv2.imencode('.png', img) + return Response(buf.tobytes(), mimetype='image/png') + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/map/meta") +def api_map_meta(): + """返回地图元数据(分辨率、原点、尺寸)""" + if not gs.map_config or "map_yaml" not in gs.map_config: + return jsonify({"ok": False, "error": "地图未加载"}), 404 + + map_yaml = gs.map_config["map_yaml"] + map_dir = os.path.dirname(map_yaml) + + try: + import yaml + with open(map_yaml, 'r') as f: + meta = yaml.safe_load(f) + pgm_file = meta.get('image', 'map.pgm') + pgm_path = os.path.join(map_dir, pgm_file) + + import cv2 + img = cv2.imread(pgm_path, cv2.IMREAD_GRAYSCALE) + height, width = img.shape if img is not None else (0, 0) + + return jsonify({ + "ok": True, + "resolution": meta.get('resolution', 0.05), + "origin": meta.get('origin', [0, 0, 0]), + "width": width, + "height": height + }) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + +# ========== 地图导航 API ========== + +@app.route("/api/navigate/to", methods=["POST"]) +def api_navigate_to(): + """导航到目标坐标""" + if not gs.map_config or "map_yaml" not in gs.map_config: + return jsonify({"ok": False, "error": "地图未加载,请先在设置中加载地图"}), 400 + + data = request.json + goal_x = data.get("x") + goal_y = data.get("y") + if goal_x is None or goal_y is None: + return jsonify({"ok": False, "error": "缺少目标坐标 x, y"}), 400 + + if not gs.agv_controller or not gs.agv_controller.is_connected(): + return jsonify({"ok": False, "error": "AGV 未连接,请先连接 AGV"}), 400 + + try: + if gs.navigator is None: + gs.navigator = MapNavigator(gs.map_config["map_yaml"]) + ok = gs.navigator.navigate_to(float(goal_x), float(goal_y)) + if ok: + return jsonify({"ok": True, "message": "导航已启动"}) + else: + return jsonify({"ok": False, "error": "路径规划失败或导航正在进行中"}), 400 + except Exception as e: + logger.error(f"导航失败: {e}") + return jsonify({"ok": False, "error": str(e)}), 500 + + +@app.route("/api/navigate/stop", methods=["POST"]) +def api_navigate_stop(): + """停止导航""" + if gs.navigator: + gs.navigator.stop() + return jsonify({"ok": True, "message": "导航已停止"}) + return jsonify({"ok": False, "error": "导航器未初始化"}), 400 + + +@app.route("/api/navigate/status", methods=["GET"]) +def api_navigate_status(): + """获取导航状态""" + if gs.navigator: + return jsonify(gs.navigator.get_status()) + return jsonify({"status": "idle", "current_position": [0, 0, 0], "path_length": 0, "path": []}) + + +@app.route("/api/navigate/path", methods=["POST"]) +def api_navigate_path(): + """预览路径(仅规划不执行)""" + if not gs.map_config or "map_yaml" not in gs.map_config: + return jsonify({"ok": False, "error": "地图未加载"}), 400 + + data = request.json + goal_x = data.get("x") + goal_y = data.get("y") + if goal_x is None or goal_y is None: + return jsonify({"ok": False, "error": "缺少目标坐标 x, y"}), 400 + + try: + if gs.navigator is None: + gs.navigator = MapNavigator(gs.map_config["map_yaml"]) + path = gs.navigator.get_path_preview(float(goal_x), float(goal_y)) + if path is not None: + return jsonify({"ok": True, "path": path, "length": len(path)}) + else: + return jsonify({"ok": False, "error": "路径规划失败,目标点不可达"}), 400 + except Exception as e: + logger.error(f"路径预览失败: {e}") + return jsonify({"ok": False, "error": str(e)}), 500 + + +# ========== 点位配置 API ========== +@app.route("/api/points/list", methods=["GET"]) +def api_points_list(): + """获取所有点位""" + return jsonify({"points": gs.points_config}) + +@app.route("/api/points/add", methods=["POST"]) +def api_points_add(): + """添加点位(从当前位置)""" + data = request.json + point_name = data.get("name", f"point_{len(gs.points_config) + 1}") + + # 获取 AGV 当前位置 + position = None + if gs.agv_controller and gs.agv_controller.is_connected(): + position = gs.agv_controller.get_position() + + point = { + "id": f"p_{int(time.time())}", + "name": point_name, + "coords": position or [0.0, 0.0, 0.0], # [x, y, yaw] + "photo_mode": data.get("photo_mode", "front"), # front / back / both + "sequence": data.get("sequence", ["front", "back"]), # both 时的执行顺序 + } + gs.points_config.append(point) + save_json("points_config.json", gs.points_config) + return jsonify({"ok": True, "point": point}) + +@app.route("/api/points/update/", methods=["POST"]) +def api_points_update(point_id): + """更新点位""" + data = request.json + for i, p in enumerate(gs.points_config): + if p["id"] == point_id: + gs.points_config[i].update(data) + save_json("points_config.json", gs.points_config) + return jsonify({"ok": True}) + return jsonify({"ok": False, "error": "点位不存在"}), 404 + +@app.route("/api/points/delete/", methods=["DELETE"]) +def api_points_delete(point_id): + """删除点位""" + gs.points_config = [p for p in gs.points_config if p["id"] != point_id] + save_json("points_config.json", gs.points_config) + return jsonify({"ok": True}) + +@app.route("/api/points/save", methods=["POST"]) +def api_points_save(): + """保存点位配置""" + data = request.json + gs.points_config = data.get("points", []) + save_json("points_config.json", gs.points_config) + return jsonify({"ok": True, "count": len(gs.points_config)}) + +@app.route("/api/points/load", methods=["GET"]) +def api_points_load(): + """加载已保存的点位配置""" + loaded = load_json("points_config.json", []) + if loaded: + gs.points_config = loaded + return jsonify({"ok": True, "points": loaded or []}) + + +# ========== 机型(姿态组)API ========== +@app.route("/api/models/list", methods=["GET"]) +def api_models_list(): + """获取所有机型""" + return jsonify({"models": gs.models_config}) + +@app.route("/api/models/add", methods=["POST"]) +def api_models_add(): + """添加机型""" + data = request.json + model_name = data.get("name", f"model_{len(gs.models_config) + 1}") + model = { + "id": f"m_{int(time.time())}", + "name": model_name, + "serial_prefix": data.get("serial_prefix", ""), # 二维码型号前缀 + "poses": [], # 姿态列表 + "description": data.get("description", ""), + "notes": data.get("notes", ""), # 备注 + } + gs.models_config.append(model) + save_json("models_config.json", gs.models_config) + return jsonify({"ok": True, "model": model}) + +@app.route("/api/models/update/", methods=["POST"]) +def api_models_update(model_id): + """更新机型""" + data = request.json + for i, m in enumerate(gs.models_config): + if m["id"] == model_id: + gs.models_config[i].update(data) + save_json("models_config.json", gs.models_config) + return jsonify({"ok": True}) + return jsonify({"ok": False, "error": "机型不存在"}), 404 + +@app.route("/api/models/delete/", methods=["DELETE"]) +def api_models_delete(model_id): + """删除机型""" + gs.models_config = [m for m in gs.models_config if m["id"] != model_id] + save_json("models_config.json", gs.models_config) + return jsonify({"ok": True}) + +@app.route("/api/models/poses/add", methods=["POST"]) +def api_poses_add(): + """添加姿态到机型(需指定 model_id)""" + data = request.json + model_id = data.get("model_id") + if not model_id: + return jsonify({"ok": False, "error": "缺少 model_id"}), 400 + for m in gs.models_config: + if m["id"] == model_id: + pose = { + "id": f"pose_{int(time.time())}", + "name": data.get("name", f"姿态{len(m['poses']) + 1}"), + "photo_type": data.get("photo_type", "front"), # front / back / nameplate + "arm_angles": data.get("arm_angles", [0.0]*6), + "speed": data.get("speed", 500), + "description": data.get("description", ""), + } + m["poses"].append(pose) + save_json("models_config.json", gs.models_config) + return jsonify({"ok": True, "pose": pose}) + return jsonify({"ok": False, "error": "机型不存在"}), 404 + +@app.route("/api/models//poses", methods=["GET"]) +def api_model_poses_get(model_id): + """获取机型的姿态列表""" + for m in gs.models_config: + if m["id"] == model_id: + return jsonify({"poses": m.get("poses", [])}) + return jsonify({"poses": []}) + +@app.route("/api/models//poses/", methods=["PUT"]) +def api_model_poses_update(model_id, pose_id): + """更新姿态""" + data = request.json + for m in gs.models_config: + if m["id"] == model_id: + for i, pose in enumerate(m.get("poses", [])): + if pose["id"] == pose_id: + m["poses"][i].update(data) + save_json("models_config.json", gs.models_config) + return jsonify({"ok": True}) + return jsonify({"ok": False}), 404 + +@app.route("/api/models//poses/", methods=["DELETE"]) +def api_model_poses_delete(model_id, pose_id): + """删除姿态""" + for m in gs.models_config: + if m["id"] == model_id: + m["poses"] = [p for p in m.get("poses", []) if p["id"] != pose_id] + save_json("models_config.json", gs.models_config) + return jsonify({"ok": True}) + return jsonify({"ok": False}), 404 + + +# ========== 任务配置 API(M×N 网格)========== +@app.route("/api/mission/position", methods=["GET"]) +def api_mission_position(): + """读取 AGV 当前坐标(供设置点位时使用)""" + if not gs.agv_controller or not gs.agv_controller.is_connected(): + return jsonify({"ok": False, "error": "AGV 未连接"}), 400 + pos = gs.agv_controller.get_position() + if not pos: + return jsonify({"ok": False, "error": "AGV 未发布位置数据(/odom 无数据),请检查 AGV 传感器是否正常"}), 400 + battery = gs.agv_controller.get_battery() + return jsonify({"ok": True, "position": pos, "battery": battery}) + + +@app.route("/api/mission/config", methods=["GET"]) +def api_mission_config_get(): + """获取任务配置(网格尺寸和空位矩阵)""" + return jsonify({"ok": True, "config": gs.mission_config, "machines": gs.machines_config}) + +@app.route("/api/mission/config", methods=["POST"]) +def api_mission_config_set(): + """"设置任务配置(网格尺寸+空位矩阵)""" + data = request.json + rows = data.get("rows", 2) + cols = data.get("cols", 3) + grid = data.get("grid", []) + gs.mission_config["rows"] = rows + gs.mission_config["cols"] = cols + gs.mission_config["grid"] = grid + save_json("mission_config.json", gs.mission_config) + return jsonify({"ok": True, "config": gs.mission_config}) + + +@app.route("/api/mission/machines", methods=["GET"]) +def api_mission_machines_list(): + """获取所有机器配置""" + return jsonify({"ok": True, "machines": gs.machines_config}) + +@app.route("/api/mission/machines/", methods=["GET"]) +def api_mission_machine_get(machine_id): + """获取单台机器配置""" + for m in gs.machines_config: + if m["id"] == machine_id: + return jsonify({"ok": True, "machine": m}) + return jsonify({"ok": False, "error": "机器不存在"}), 404 + +@app.route("/api/mission/machines", methods=["POST"]) +def api_mission_machines_save(): + """"批量保存/更新机器配置""" + data = request.json + machines = data.get("machines", []) + gs.machines_config = machines + save_json("machines_config.json", gs.machines_config) + return jsonify({"ok": True, "count": len(gs.machines_config)}) + +@app.route("/api/mission/machines/add", methods=["POST"]) +def api_mission_machine_add(): + """添加单台机器配置""" + data = request.json + machine_id = data.get("id", f"m_{data.get('row', 0)}_{data.get('col', 0)}") + # 检查是否已存在 + for m in gs.machines_config: + if m["id"] == machine_id or (m.get("row") == data.get("row") and m.get("col") == data.get("col")): + return jsonify({"ok": False, "error": "该位置已有机器"}), 400 + machine = { + "id": machine_id, + "row": data.get("row", 0), + "col": data.get("col", 0), + "front": data.get("front", {"coords": [0, 0, 0], "poses": []}), + "back": data.get("back", {"coords": [0, 0, 0], "poses": []}), + } + gs.machines_config.append(machine) + save_json("machines_config.json", gs.machines_config) + return jsonify({"ok": True, "machine": machine}) + +@app.route("/api/mission/machines/", methods=["PUT"]) +def api_mission_machine_update(machine_id): + """更新单台机器配置(正面/背面点位+姿态)""" + data = request.json + for i, m in enumerate(gs.machines_config): + if m["id"] == machine_id: + gs.machines_config[i].update(data) + save_json("machines_config.json", gs.machines_config) + return jsonify({"ok": True}) + return jsonify({"ok": False, "error": "机器不存在"}), 404 + + +@app.route("/api/mission/machines/", methods=["DELETE"]) +def api_mission_machine_delete(machine_id): + """删除单台机器配置""" + gs.machines_config = [m for m in gs.machines_config if m["id"] != machine_id] + save_json("machines_config.json", gs.machines_config) + return jsonify({"ok": True}) + +@app.route("/api/mission/poses//", methods=["GET"]) +def api_mission_poses_get(machine_id, side): + """获取机器指定侧的姿态列表(side: front | back)""" + for m in gs.machines_config: + if m["id"] == machine_id: + return jsonify({"ok": True, "poses": m.get(side, {}).get("poses", [])}) + return jsonify({"ok": False, "poses": []}), 404 + +@app.route("/api/mission/poses//", methods=["POST"]) +def api_mission_poses_add(machine_id, side): + """添加姿态到机器指定侧(side: front | back)""" + data = request.json + for m in gs.machines_config: + if m["id"] == machine_id: + if side not in m: + return jsonify({"ok": False, "error": f"机器无此侧: {side}"}), 400 + pose = { + "id": f"pose_{int(time.time())}", + "name": data.get("name", f"姿态"), + "arm_angles": data.get("arm_angles", [0.0]*6), + "speed": data.get("speed", 500), + "description": data.get("description", ""), + } + m[side]["poses"].append(pose) + save_json("machines_config.json", gs.machines_config) + return jsonify({"ok": True, "pose": pose}) + return jsonify({"ok": False, "error": "机器不存在"}), 404 + +@app.route("/api/mission/poses///", methods=["DELETE"]) +def api_mission_poses_delete(machine_id, side, pose_id): + """删除机器指定侧的姿态""" + for m in gs.machines_config: + if m["id"] == machine_id: + if side not in m: + return jsonify({"ok": False}), 404 + m[side]["poses"] = [p for p in m[side]["poses"] if p["id"] != pose_id] + save_json("machines_config.json", gs.machines_config) + return jsonify({"ok": True}) + return jsonify({"ok": False}), 404 + +@app.route("/api/mission/generate_sequence", methods=["GET"]) +def api_mission_generate_sequence(): + """根据网格配置和机器配置生成拍摄序列(蛇形)""" + rows = gs.mission_config.get("rows", 2) + cols = gs.mission_config.get("cols", 3) + grid = gs.mission_config.get("grid", []) + machines = gs.machines_config + + def get_machine(row, col): + for m in machines: + if m.get("row") == row and m.get("col") == col: + return m + return None + + # 蛇形序列:行0从左到右正面→右到左背面,行1从右到左背面→左到右正面...交替 + sequence = [] + for r in range(rows): + # 检查该行是否有机器 + has_any = any(grid[r][c] for c in range(cols)) if r < len(grid) else False + if not has_any: + continue + + if r % 2 == 0: # 偶数行:正面从左到右,背面从右到左 + # 正面:从左到右 + for c in range(cols): + if r < len(grid) and c < len(grid[r]) and grid[r][c]: + m = get_machine(r, c) + if m and m.get("front"): + sequence.append({ + "machine_id": m["id"], + "row": r, "col": c, + "side": "front", + "row_dir": "lr", # 正面时该行的方向 + "row_dir_back": "rl" # 背面时该行的方向 + }) + # 背面:从右到左 + for c in range(cols - 1, -1, -1): + if r < len(grid) and c < len(grid[r]) and grid[r][c]: + m = get_machine(r, c) + if m and m.get("back"): + sequence.append({ + "machine_id": m["id"], + "row": r, "col": c, + "side": "back", + "row_dir": "lr", + "row_dir_back": "rl" + }) + else: # 奇数行:正面从右到左,背面从左到右(方向反转) + # 背面:从左到右(此行的背面在下一行的前面位置,但这里按用户描述:背面先行) + for c in range(cols): + if r < len(grid) and c < len(grid[r]) and grid[r][c]: + m = get_machine(r, c) + if m and m.get("back"): + sequence.append({ + "machine_id": m["id"], + "row": r, "col": c, + "side": "back", + "row_dir": "rl", + "row_dir_back": "lr" + }) + # 正面:从右到左 + for c in range(cols - 1, -1, -1): + if r < len(grid) and c < len(grid[r]) and grid[r][c]: + m = get_machine(r, c) + if m and m.get("front"): + sequence.append({ + "machine_id": m["id"], + "row": r, "col": c, + "side": "front", + "row_dir": "rl", + "row_dir_back": "lr" + }) + + return json + +# ========== 点位配置 API(独立于机器)========== +@app.route("/api/mission/positions", methods=["GET"]) +def api_mission_positions_list(): + """获取所有点位配置""" + positions = gs.mission_config.get("positions", []) + return jsonify({"ok": True, "positions": positions}) + +@app.route("/api/mission/positions", methods=["POST"]) +def api_mission_positions_save(): + """保存点位配置(单个点位:创建或更新)""" + data = request.json + row = int(data.get("row", 0)) + col = int(data.get("col", 0)) + side = data.get("side", "front") # "front" | "back" + coords = data.get("coords", [0, 0, 0]) + poses = data.get("poses", []) + + positions = gs.mission_config.setdefault("positions", []) + # 查找是否已有该点位 + key = (row, col, side) + for i, p in enumerate(positions): + if int(p.get("row", 0)) == row and int(p.get("col", 0)) == col and p.get("side") == side: + positions[i] = {"row": row, "col": col, "side": side, "coords": coords, "poses": poses} + save_json("mission_config.json", gs.mission_config) + return jsonify({"ok": True, "position": positions[i]}) + positions.append({"row": row, "col": col, "side": side, "coords": coords, "poses": poses}) + save_json("mission_config.json", gs.mission_config) + return jsonify({"ok": True, "position": positions[-1]}) + + + + +# ========== 机械臂控制 API ========== +@app.route("/api/arm/get_angles", methods=["GET"]) +def api_arm_get_angles(): + """获取当前关节角度""" + if not gs.arm_client: + return jsonify({"ok": False, "error": "未连接机械臂"}), 400 + ok, angles = gs.arm_client.get_angles() + return jsonify({"ok": ok, "angles": angles}) + +@app.route("/api/arm/set_angles", methods=["POST"]) +def api_arm_set_angles(): + """设置关节角度""" + data = request.json + angles = data.get("angles", []) + speed = data.get("speed", 500) + if not gs.arm_client: + return jsonify({"ok": False, "error": "未连接机械臂"}), 400 + ok = gs.arm_client.set_angles(angles, speed) + return jsonify({"ok": ok}) + +@app.route("/api/arm/set_angle", methods=["POST"]) +def api_arm_set_angle(): + """设置单个关节角度""" + data = request.json + joint = data.get("joint", "J1") + angle = data.get("angle", 0.0) + speed = data.get("speed", 500) + if not gs.arm_client: + return jsonify({"ok": False}), 400 + ok = gs.arm_client.set_angle(joint, angle, speed) + return jsonify({"ok": ok}) + +@app.route("/api/arm/jog", methods=["POST"]) +def api_arm_jog(): + """连续调节关节(用于方向键)""" + data = request.json + joint = data.get("joint", "J1") + direction = data.get("direction", 0) # -1 / 0 / 1 + speed = data.get("speed", 500) + if not gs.arm_client: + return jsonify({"ok": False}), 400 + ok = gs.arm_client.jog_angle(joint, direction, speed) + return jsonify({"ok": ok}) + +@app.route("/api/arm/get_coords", methods=["GET"]) +def api_arm_get_coords(): + """获取当前坐标""" + if not gs.arm_client: + return jsonify({"ok": False}), 400 + ok, coords = gs.arm_client.get_coords() + return jsonify({"ok": ok, "coords": coords}) + +@app.route("/api/arm/state_check", methods=["GET"]) +def api_arm_state_check(): + """检查机械臂状态""" + if not gs.arm_client: + return jsonify({"ok": False}), 400 + ok = gs.arm_client.state_check() + return jsonify({"ok": ok, "running": not ok}) + +@app.route("/api/arm/power_on", methods=["POST"]) +def api_arm_power_on(): + if not gs.arm_client: + return jsonify({"ok": False}), 400 + ok = gs.arm_client.power_on() + return jsonify({"ok": ok}) + +@app.route("/api/arm/state_on", methods=["POST"]) +def api_arm_state_on(): + if not gs.arm_client: + return jsonify({"ok": False}), 400 + ok = gs.arm_client.state_on() + return jsonify({"ok": ok}) + +@app.route("/api/arm/state_off", methods=["POST"]) +def api_arm_state_off(): + if not gs.arm_client: + return jsonify({"ok": False}), 400 + ok = gs.arm_client.state_off() + return jsonify({"ok": ok}) + + +# ========== 摄像头预览 API ========== +@app.route("/api/camera/preview") +def api_camera_preview(): + """MJPEG 视频流""" + if not gs.qr_scanner or not gs.qr_scanner._cap: + return "camera not opened", 400 + + def gen(): + while True: + frame = gs.qr_scanner.read_frame() + if frame is None: + break + # 编码为 JPEG + import cv2 + ret, buf = cv2.imencode(".jpg", frame) + if ret: + yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + + buf.tobytes() + b"\r\n") + + return Response(gen(), mimetype="multipart/x-mixed-replace; boundary=frame") + +@app.route("/api/camera/refresh") +def api_camera_refresh(): + """AGV 摄像头单帧 JPEG(polling 模式)""" + if not gs.qr_scanner or not gs.qr_scanner._cap: + return "camera not opened", 400 + import cv2 + frame = gs.qr_scanner.read_frame() + if frame is None: + return "", 400 + ret, buf = cv2.imencode(".jpg", frame) + if ret: + return Response(buf.tobytes(), mimetype="image/jpeg") + return "encode failed", 500 + +@app.route("/api/camera/capture") +def api_camera_capture(): + """拍摄一张照片""" + if not gs.qr_scanner or not gs.qr_scanner._cap: + return jsonify({"ok": False, "error": "摄像头未打开"}), 400 + import cv2 + frame = gs.qr_scanner.read_frame() + if frame is None: + return jsonify({"ok": False, "error": "读取帧失败"}), 400 + + photo_dir = os.path.join(DATA_DIR, "photos") + os.makedirs(photo_dir, exist_ok=True) + photo_path = os.path.join(photo_dir, f"capture_{int(time.time())}.jpg") + cv2.imwrite(photo_path, frame) + return jsonify({"ok": True, "path": photo_path}) + +@app.route("/api/camera/arm_refresh") +def api_arm_camera_refresh(): + """从机械臂拉一张 JPEG(流式读第一个完整帧,超时则降级)""" + import requests + try: + r = requests.get(ARM_CAMERA_CONFIG["url"], stream=True, timeout=5) + if r.status_code != 200: + return "", 404 + data = b"" + for chunk in r.iter_content(chunk_size=4096): + data += chunk + # 在累积数据中找 JPEG 完整帧 + s = data.find(b"\xff\xd8") + e = data.find(b"\xff\xd9", s + 2) if s >= 0 else -1 + if s >= 0 and e > s: + r.close() + return Response(data[s:e+2], mimetype="image/jpeg") + # 数据太长还没找到 JPEG 也直接返回(可能是空流) + if len(data) > 1024 * 1024: + r.close() + return "", 404 + r.close() + return "", 404 + except Exception as ex: + logger.error(f"arm_refresh 失败: {ex}") + return "", 404 + + +@app.route("/api/camera/qr_scan", methods=["GET"]) +def api_qr_scan(): + """扫描一次二维码""" + if not gs.qr_scanner: + return jsonify({"ok": False}), 400 + result = gs.qr_scanner.scan_once() + return jsonify({"ok": bool(result), "data": result}) + + +# ========== AGV 移动控制 API ========== +@app.route("/api/agv/move", methods=["POST"]) +def api_agv_move(): + """控制 AGV 移动(前进/后退/左转/右转/停止)""" + data = request.json + direction = data.get("direction", "stop") # forward / backward / left / right / stop + speed = data.get("speed", AGV_CONFIG.get("move_speed", 0.5)) + if not gs.agv_controller or not gs.agv_controller.is_connected(): + return jsonify({"ok": False, "error": "AGV 未连接"}), 400 + try: + if direction == "forward": + gs.agv_controller.move_forward(speed) + elif direction == "backward": + gs.agv_controller.move_backward(speed) + elif direction == "left": + gs.agv_controller.turn_left(speed) + elif direction == "right": + gs.agv_controller.turn_right(speed) + elif direction == "left_lateral": + gs.agv_controller.move_left_lateral(speed) + elif direction == "right_lateral": + gs.agv_controller.move_right_lateral(speed) + elif direction == "stop": + gs.agv_controller.stop() + else: + return jsonify({"ok": False, "error": "未知方向"}), 400 + return jsonify({"ok": True}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + +@app.route("/api/agv/position", methods=["GET"]) +def api_agv_position(): + """获取 AGV 当前位置""" + if not gs.agv_controller or not gs.agv_controller.is_connected(): + return jsonify({"ok": False, "error": "AGV 未连接"}), 400 + pos = gs.agv_controller.get_position() + if not pos: + return jsonify({"ok": False, "error": "AGV 未发布位置数据(/odom 无数据),请检查 AGV 传感器是否正常"}), 400 + battery = gs.agv_controller.get_battery() + return jsonify({"ok": True, "position": pos, "battery": battery}) + +@app.route("/api/agv/stop", methods=["POST"]) +def api_agv_stop(): + """立即停止 AGV""" + if gs.agv_controller: + gs.agv_controller.stop() + return jsonify({"ok": True}) + +@app.route("/api/agv/reset", methods=["POST"]) +def api_agv_reset(): + """撞物体后复位 - 停止运动并尝试重新上电""" + import time + if not gs.agv_controller: + return jsonify({"ok": False, "error": "AGV 控制器未初始化"}), 400 + try: + # 1. 先停止运动 + gs.agv_controller.stop() + time.sleep(0.5) + + # 2. 检查 AGV 对象是否存在 + agv = gs.agv_controller._agv + if not agv: + return jsonify({"ok": False, "error": "AGV 未连接,请重新连接"}), 400 + + # 3. 检查电源状态 + power_on = agv.is_power_on() + if not power_on: + # 撞物体后可能自动断电保护,尝试重新上电 + agv.power_on() + time.sleep(2) + power_on = agv.is_power_on() + if power_on: + gs.agv_controller._connected = True + return jsonify({"ok": True, "message": "复位成功,已重新上电"}) + else: + return jsonify({"ok": False, "error": "上电失败,请检查 AGV 状态"}), 500 + else: + # 电源正常,只需要停止 + return jsonify({"ok": True, "message": "复位成功,AGV 已停止"}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + +# ========== 任务执行 API ========== +@app.route("/api/mission/start", methods=["POST"]) +def api_mission_start(): + """开始执行任务""" + if gs.state == State.RUNNING: + return jsonify({"ok": False, "error": "任务已在运行中"}), 400 + + data = request.json or {} + mission_data = { + "map": gs.map_config, + "points": gs.points_config, + } + + def run(): + from config import AGV_CONFIG, UPLOAD_CONFIG + executor_config = { + "device": AGV_CONFIG.get("device", "/dev/agvpro_controller"), + "baudrate": AGV_CONFIG.get("baudrate", 1000000), + "arm": ARM_CONFIG, + "upload_url": UPLOAD_CONFIG["url"], + "upload_timeout": UPLOAD_CONFIG["timeout"], + "upload_retries": UPLOAD_CONFIG["max_retries"], + "camera_index": 0, + } + executor = MissionExecutor(executor_config) + + # 连接 + conn_results = executor.connect_all() + if not conn_results.get("arm") or not conn_results.get("camera"): + gs.mission_report = {"error": "连接失败", "details": conn_results} + gs.state = State.IDLE + return + + gs.state = State.RUNNING + report = executor.execute_mission(mission_data) + gs.mission_report = report + executor.disconnect_all() + gs.state = State.IDLE if report["failed"] == 0 else State.PAUSED + + thread = threading.Thread(target=run, daemon=True) + thread.start() + return jsonify({"ok": True, "message": "任务已启动"}) + +@app.route("/api/mission/stop", methods=["POST"]) +def api_mission_stop(): + """停止任务""" + if hasattr(MissionExecutor, "_instance"): + MissionExecutor._instance.stop() + gs.state = State.IDLE + return jsonify({"ok": True}) + +@app.route("/api/mission/pause", methods=["POST"]) +def api_mission_pause(): + gs.state = State.PAUSED + return jsonify({"ok": True}) + +@app.route("/api/mission/report", methods=["GET"]) +def api_mission_report(): + return jsonify({"report": gs.mission_report}) + +@app.route("/api/mission/state", methods=["GET"]) +def api_mission_state(): + return jsonify({"state": gs.state}) + + +# ========== 静态资源 ========== +@app.route("/photos/") +def photos(name): + return send_from_directory(os.path.join(DATA_DIR, "photos"), name) + + +# ========== 启动 ========== +if __name__ == "__main__": + logger.info("=" * 50) + logger.info("AGV 拍摄系统启动") + logger.info(f"监听 {SERVER_CONFIG['host']}:{SERVER_CONFIG['port']}") + logger.info("=" * 50) + # 启动时自动初始化 AGV 摄像头 + gs.qr_scanner = QRScanner(CAMERA_CONFIG["device_index"]) + gs.camera_opened = gs.qr_scanner.open() + logger.info(f"AGV 摄像头初始化: {'成功' if gs.camera_opened else '失败'}") + app.run( + host=SERVER_CONFIG["host"], + port=SERVER_CONFIG["port"], + debug=SERVER_CONFIG["debug"], + threaded=True + ) diff --git a/agv_app/config.py b/agv_app/config.py new file mode 100644 index 0000000..85b01b5 --- /dev/null +++ b/agv_app/config.py @@ -0,0 +1,87 @@ +""" +配置文件 - 所有可配置参数集中管理 +""" +import os + +# 基础路径(部署后对应 ~/work/agv_app) +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +# ========== AGV 参数 ========== +AGV_CONFIG = { + "device": "/dev/agvpro_controller", + "baudrate": 10000000, + "move_speed": 0.5, + "turn_speed": 0.5, +} + +# ========== 机械臂 TCP 客户端 ========== +ARM_CONFIG = { + "host": "192.168.110.164", + "port": 5002, + "timeout": 8, + "retry_times": 3, + "retry_interval": 1, +} + +# ========== 地图 ========== +MAP_CONFIG = { + "map_dir": "/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/", + "map_file": "map.yaml", +} + +# ========== 摄像头 ========== +CAMERA_CONFIG = { + "device_index": 4, # AGV 摄像头 video4(标准彩色摄像头,V4L2后端) + "backend": "v4l2", # 使用 V4L2 后端获取标准彩色格式(640x480) + "qr_detect_interval": 0.5, + "capture_delay": 0.5, +} + +# ========== 机械臂摄像头流 ========== +ARM_CAMERA_CONFIG = { + "url": "http://192.168.110.164:5003/api/camera/preview", +} + +# ========== HTTP 上传 ========== +UPLOAD_CONFIG = { + "url": "https://ts.zhijian168.com/prod-api/file/uploadImage", + "timeout": 30, + "max_retries": 3, +} + +# ========== Flask 服务器 ========== +SERVER_CONFIG = { + "host": "0.0.0.0", + "port": 5000, + "secret_key": "agv630_secret_key_2024", + "debug": False, +} + +# ========== 任务配置存储路径 ========== +DATA_DIR = os.path.join(BASE_DIR, "data") +os.makedirs(DATA_DIR, exist_ok=True) + +# ========== 关节角度范围限制 ========== +JOINT_LIMITS = { + "J1": (-180.0, 180.0), + "J2": (-270.0, 90.0), + "J3": (-150.0, 150.0), + "J4": (-260.0, 80.0), + "J5": (-168.0, 168.0), + "J6": (-174.0, 174.0), +} + +# ========== 机械臂默认速度 ========== +DEFAULT_ARM_SPEED = 500 + +# ========== 状态定义 ========== +class State: + SETTING = "setting" + RUNNING = "running" + PAUSED = "paused" + IDLE = "idle" + +class PhotoType: + FRONT = "front" + BACK = "back" + NAMEPLATE = "nameplate" \ No newline at end of file diff --git a/agv_app/data/machines_config.json b/agv_app/data/machines_config.json new file mode 100644 index 0000000..bdb5f16 --- /dev/null +++ b/agv_app/data/machines_config.json @@ -0,0 +1,296 @@ +[ + { + "id": "m_0_2", + "row": 0, + "col": 2, + "front": { + "coords": [ + 1.2421705407118802, + 0.0025490140048510445, + 0.00150923641 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_1_2", + "row": 1, + "col": 2, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_2_0", + "row": 2, + "col": 0, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_2_2", + "row": 2, + "col": 2, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_3_0", + "row": 3, + "col": 0, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_3_2", + "row": 3, + "col": 2, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_3_1", + "row": 3, + "col": 1, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_4_4", + "row": 4, + "col": 4, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_4_1", + "row": 4, + "col": 1, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_4_0", + "row": 4, + "col": 0, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_3_4", + "row": 3, + "col": 4, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_3_3", + "row": 3, + "col": 3, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_0_1", + "row": 0, + "col": 1, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + }, + { + "id": "m_0_0", + "row": 0, + "col": 0, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + } +] \ No newline at end of file diff --git a/agv_app/data/map_config.json b/agv_app/data/map_config.json new file mode 100644 index 0000000..7ccc11d --- /dev/null +++ b/agv_app/data/map_config.json @@ -0,0 +1,5 @@ +{ + "map_dir": "/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/", + "map_file": "map.yaml", + "map_yaml": "/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/map.yaml" +} \ No newline at end of file diff --git a/agv_app/data/mission_config.json b/agv_app/data/mission_config.json new file mode 100644 index 0000000..1e1f374 --- /dev/null +++ b/agv_app/data/mission_config.json @@ -0,0 +1,128 @@ +{ + "rows": 2, + "cols": 2, + "grid": [], + "positions": [ + { + "row": 0, + "col": 0, + "side": "front", + "coords": [ + 0.616485726055098, + -0.002587517923224651, + -0.003483980050000001 + ], + "poses": [] + }, + { + "row": 3, + "col": 1, + "side": "back", + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + { + "row": 1, + "col": 1, + "side": "front", + "coords": [ + -0.27906358987415997, + 0.00411087876725537, + -0.00749475593 + ], + "poses": [] + }, + { + "row": 0, + "col": 1, + "side": "front", + "coords": [ + 0.616485726055098, + -0.002587517923224651, + -0.003483980050000001 + ], + "poses": [] + }, + { + "row": 0, + "col": 2, + "side": "front", + "coords": [ + -0.27906358987415997, + 0.00411087876725537, + -0.00749475593 + ], + "poses": [] + }, + { + "row": 2, + "col": 1, + "side": "shoot", + "coords": [ + -1.898244121263206, + -0.014324627152337432, + 0.004533442980000002 + ], + "poses": [] + }, + { + "row": 2, + "col": 0, + "side": "shoot", + "coords": [ + -0.9528404539697249, + -0.01004755255507813, + 0.005515614170000002 + ], + "poses": [] + }, + { + "row": 0, + "col": 1, + "side": "shoot", + "coords": [ + -0.9528404539697249, + -0.01004755255507813, + 0.005515614170000002 + ], + "poses": [] + }, + { + "row": 1, + "col": 1, + "side": "shoot", + "coords": [ + -0.9528404539697249, + -0.01004755255507813, + 0.005515614170000002 + ], + "poses": [] + }, + { + "row": 0, + "col": 0, + "side": "shoot", + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + { + "row": 1, + "col": 0, + "side": "shoot", + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + } + ] +} \ No newline at end of file diff --git a/agv_app/data/points_config.json b/agv_app/data/points_config.json new file mode 100644 index 0000000..ac9106e --- /dev/null +++ b/agv_app/data/points_config.json @@ -0,0 +1,55 @@ +[ + { + "coords": [ + -0.2489133152442747, + -0.9566827357283122, + 1.3165501267 + ], + "id": "p_1778482526", + "name": "point_1", + "photo_mode": "front", + "poses": [ + { + "arm_angles": [], + "id": "pose_1778483465", + "name": "姿态1", + "photo_type": "front", + "speed": 500 + } + ], + "sequence": [ + "front", + "back" + ] + }, + { + "coords": [ + -0.13938025759948866, + -0.5310313938681763, + 1.3225773811300001 + ], + "id": "p_1778482605", + "name": "point_2", + "photo_mode": "front", + "poses": [], + "sequence": [ + "front", + "back" + ] + }, + { + "coords": [ + -0.5498454634407133, + 0.4294772846745445, + 2.083953415929999 + ], + "id": "p_1778483433", + "name": "point_3", + "photo_mode": "front", + "poses": [], + "sequence": [ + "front", + "back" + ] + } +] \ No newline at end of file diff --git a/agv_app/requirements.txt b/agv_app/requirements.txt new file mode 100644 index 0000000..a0b4d27 --- /dev/null +++ b/agv_app/requirements.txt @@ -0,0 +1,7 @@ +flask>=2.0 +flask-cors>=3.0 +pymycobot>=4.0.0 +opencv-python>=4.5 +pyzbar>=0.1.8 +requests>=2.25 +numpy>=1.20 diff --git a/agv_app/setting.html b/agv_app/setting.html new file mode 100644 index 0000000..1ed76f2 --- /dev/null +++ b/agv_app/setting.html @@ -0,0 +1,385 @@ + + + + + + 设置 - AGV 拍摄系统 + + + +
+
+ + +
+ + +
+ + + + +
+ +
+ +
+
+

地图配置

+
+
+ + +
+
+ + +
+
+ + +
+
+

{% raw %}{{ mapMsg }}{% endraw %}

+
+
+

地图可视化

+
+ + +
+ +
+
+
+
+
+
+ + +
+ + +
+

① 网格配置 (M×N)

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
第{% raw %}{{ c }}{% endraw %}列
+ + + + + + +
点位行 1
+
+ {% raw %}{{ getPointAt(0, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(0, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %} + +
+ + + + + +
机器行 {% raw %}{{ missionConfig.rows }}{% endraw %}
+
+ + +
+ + +
点位行 {% raw %}{{ missionConfig.rows+1 }}{% endraw %}
+
+ {% raw %}{{ getPointAt(missionConfig.rows, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(missionConfig.rows, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %} + +
+
+

点击「点位行」配置拍摄坐标;点击「机器行」切换有无机器
中间点位同时服务于上下两台机器(上机器背面 / 下机器正面),删除机器不影响点位配置

+
+
+ + +
+

② 点位配置 — 第{% raw %}{{ selectedMachine.row+1 }}{% endraw %}行 第{% raw %}{{ selectedMachine.col+1 }}{% endraw %}列

+ + +
+

📷 正面点位

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+

正面姿态 ({% raw %}{{ selectedMachine.front.poses.length }}{% endraw %} 个)

+
+ {% raw %}{{ pose.name }}{% endraw %} + 角度: {% raw %}{{ formatAngles(pose.arm_angles) }}{% endraw %} + +
+
+
+ + +
+
+ + +
+

📷 背面点位

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+

背面姿态 ({% raw %}{{ selectedMachine.back.poses.length }}{% endraw %} 个)

+
+ {% raw %}{{ pose.name }}{% endraw %} + 角度: {% raw %}{{ formatAngles(pose.arm_angles) }}{% endraw %} + +
+
+
+ + +
+
+ +
+ + +
+
+ + +
+

③ 🐍 蛇形拍摄序列预览

+
+
+ {% raw %}{{ idx+1 }}{% endraw %} + + 第{% raw %}{{ step.row+1 }}{% endraw %}行 第{% raw %}{{ step.col+1 }}{% endraw %}列 + {% raw %}{{ step.side === 'front' ? '正面' : '背面' }}{% endraw %} + +
+
+
+ +
+
+
+ + + + + + +
+
+

🤖 机械臂控制

+
+ ⚠️ 机械臂未连接,请先在首页连接设备 +
+
+
+ +
+
+

关节角度控制

+
+
+ +
{% raw %}{{ currentAngles[j-1] ? currentAngles[j-1].toFixed(1) : '—' }}{% endraw %}°
+
+ + + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+

🚗 AGV 移动控制

+
+ ⚠️ AGV 未连接,请先在首页连接设备 +
+
+
+ +
+
+ 🔋 电压: {% raw %}{{ agvBattery !== null ? agvBattery + 'V' : '—' }}{% endraw %} + 📍 位置: X={% raw %}{{ agvPosition[0] ? agvPosition[0].toFixed(2) : '?' }}{% endraw %} Y={% raw %}{{ agvPosition[1] ? agvPosition[1].toFixed(2) : '?' }}{% endraw %} + +
+
+
+
+ +
+
+
+ + + +
+
+
+ +
+
+
+
+ + +
+
+
+ +
+ + {% raw %}{{ (agvSpeed * 100).toFixed(0) }}{% endraw %}% +
+
+
+
+ + +
+
+
+
+
+
+ + + + + diff --git a/agv_app/setting.js b/agv_app/setting.js new file mode 100644 index 0000000..4bc8c6d --- /dev/null +++ b/agv_app/setting.js @@ -0,0 +1,587 @@ +const { createApp } = Vue +const API = '' + +createApp({ + data() { + return { + tab: 'map', + // 任务配置 + missionConfig: { rows: 3, cols: 3, grid: [], machines: [] }, + selectedMachine: null, + sequence: [], + poseForm: { name: '', photo_type: 'front', description: '' }, + // 地图 + mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' }, + mapMsg: '', + mapLoaded: false, + mapImageUrl: '', + mapMeta: null, + // 点位 + points: [], + newPointName: '', + newPointMode: 'front', + newPointSequence: ['front', 'back'], + // 机型(姿态组) + models: [], + selectedModelId: null, + newModelName: '', + newModelSerial: '', + // 机械臂 + armConnected: false, + currentAngles: [], + angleInputs: [], + previewUrl: API + '/api/camera/preview', + jogIntervals: {}, + // AGV + cameraOpened: false, + agvConnected: false, + agvBattery: null, + agvPosition: null, + agvSpeed: 0.5, + agvMoveInterval: null, + agvCameraUrl: API + '/api/camera/refresh', + agvCameraTimer: null, + } + }, + mounted() { + this.refresh() + this.refreshAngles() + }, + watch: { + tab(val) { + if (val === 'agv') { + this.agvCameraTimer = setInterval(() => { + this.agvCameraUrl = API + '/api/camera/refresh?t=' + Date.now() + }, 1000) + } else { + if (this.agvCameraTimer) { + clearInterval(this.agvCameraTimer) + this.agvCameraTimer = null + } + } + } + }, + beforeUnmount() { + Object.values(this.jogIntervals).forEach(i => clearInterval(i)) + if (this.agvCameraTimer) clearInterval(this.agvCameraTimer) + }, + methods: { + async refresh() { + try { + const res = await fetch(API + '/api/status') + const data = await res.json() + this.agvConnected = data.agv_connected + this.armConnected = data.arm_connected + this.cameraOpened = data.camera_opened + this.mapLoaded = data.map_loaded + if (data.map_loaded) { + this.mapImageUrl = API + '/api/map/image?t=' + Date.now() + try { + const metaRes = await fetch(API + '/api/map/meta') + const meta = await metaRes.json() + if (meta.ok) this.mapMeta = meta + } catch (e) {} + } + } catch (e) {} + await this.loadAllPoints() + await this.loadAllModels() + await this.loadAllMachines() + await this.loadMissionConfig() + }, + // === 地图 === + async loadMap() { + const res = await fetch(API + '/api/map/load', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.mapForm) + }) + const data = await res.json() + this.mapMsg = data.ok ? '✅ 地图加载成功' : '❌ ' + (data.error || '加载失败') + this.mapLoaded = data.ok + if (data.ok) { + this.mapImageUrl = API + '/api/map/image?t=' + Date.now() + try { + const metaRes = await fetch(API + '/api/map/meta') + const meta = await metaRes.json() + if (meta.ok) this.mapMeta = meta + } catch (e) {} + } + }, + onMapError() { + this.mapMsg = '❌ 地图图像加载失败' + }, + getMapX(coords) { + if (!coords || !this.mapMeta) return 50 + const [x, y, yaw] = coords + const { resolution, origin, width } = this.mapMeta + const px = (x - origin[0]) / (resolution * width) * 100 + return Math.max(0, Math.min(100, px)) + }, + getMapY(coords) { + if (!coords || !this.mapMeta) return 50 + const [x, y, yaw] = coords + const { resolution, origin, height } = this.mapMeta + const py = (y - origin[1]) / (resolution * height) * 100 + return Math.max(0, Math.min(100, 100 - py)) + }, + async saveMap() { + await fetch(API + '/api/map/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.mapForm) + }) + this.mapMsg = '✅ 地图配置已保存' + }, + // === 点位 === + async loadAllPoints() { + const res = await fetch(API + '/api/points/list') + const data = await res.json() + this.points = data.points || [] + }, + async addPoint() { + const res = await fetch(API + '/api/points/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: this.newPointName || 'point_' + (this.points.length + 1), + photo_mode: this.newPointMode, + sequence: this.newPointSequence + }) + }) + const data = await res.json() + if (data.ok) { + await this.loadAllPoints() + this.newPointName = '' + } + }, + async deletePoint(id) { + if (!confirm('确定删除该点位?')) return + await fetch(API + '/api/points/delete/' + id, { method: 'DELETE' }) + await this.loadAllPoints() + }, + async saveAllPoints() { + await fetch(API + '/api/points/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ points: this.points }) + }) + alert('点位已保存') + }, + getPoint(id) { + return this.points.find(p => p.id === id) + }, + formatAngles(angles) { + if (!angles) return '—' + return angles.map(a => (a || 0).toFixed(1) + '°').join(' / ') + }, + // === 机型管理 === + async loadAllModels() { + const res = await fetch(API + '/api/models/list') + const data = await res.json() + this.models = data.models || [] + this.models.forEach(m => { + if (!this.poseForm[m.id]) { + this.poseForm[m.id] = { name: '', photo_type: 'front', description: '' } + } + }) + }, + async addModel() { + const res = await fetch(API + '/api/models/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: this.newModelName || 'model_' + (this.models.length + 1), + serial_prefix: this.newModelSerial, + description: '' + }) + }) + const data = await res.json() + if (data.ok) { + await this.loadAllModels() + this.newModelName = '' + this.newModelSerial = '' + } + }, + async deleteModel(modelId) { + if (!confirm('确定删除该机型?其下所有姿态将被删除!')) return + await fetch(API + '/api/models/delete/' + modelId, { method: 'DELETE' }) + await this.loadAllModels() + }, + // === 姿态管理(属于机型)=== + async addPose(modelId) { + const form = this.poseForm[modelId] + if (!form) return + await fetch(API + '/api/models/poses/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model_id: modelId, + name: form.name || '姿态' + ((this.getModel(modelId)?.poses?.length || 0) + 1), + photo_type: form.photo_type, + arm_angles: this.currentAngles, + speed: 500, + description: form.description || '' + }) + }) + await this.loadAllModels() + form.name = '' + form.description = '' + }, + async deletePose(modelId, poseId) { + if (!confirm('确定删除该姿态?')) return + await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' }) + await this.loadAllModels() + }, + getModel(id) { + return this.models.find(m => m.id === id) + }, + // === 任务配置 === + async loadMissionConfig() { + try { + const res = await fetch(API + '/api/mission/config') + const data = await res.json() + if (data.ok && data.config) { + this.missionConfig.rows = data.config.rows || 3 + this.missionConfig.cols = data.config.cols || 3 + this.missionConfig.grid = data.config.grid || [] + } + } catch (e) { console.error('加载任务配置失败', e) } + }, + async generateGrid() { + try { + const res = await fetch(API + '/api/mission/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + rows: this.missionConfig.rows, + cols: this.missionConfig.cols, + grid: [] + }) + }) + const data = await res.json() + if (data.ok) { + this.missionConfig.grid = data.config.grid || [] + alert('✅ 网格已生成 (' + this.missionConfig.rows + '×' + this.missionConfig.cols + ')') + } else { + alert('❌ 网格生成失败') + } + } catch (e) { alert('请求失败: ' + e.message) } + }, + async saveMissionConfig() { + try { + const res = await fetch(API + '/api/mission/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + rows: this.missionConfig.rows, + cols: this.missionConfig.cols, + grid: this.missionConfig.grid + }) + }) + const data = await res.json() + if (data.ok) { + alert('✅ 网格配置已保存') + } + } catch (e) { alert('保存失败: ' + e.message) } + }, + async loadAllMachines() { + try { + const res = await fetch(API + '/api/mission/machines') + const data = await res.json() + this.missionConfig.machines = data.machines || [] + } catch (e) { console.error('加载机器列表失败', e) } + }, + getMachineAt(ri, ci) { + if (!this.missionConfig.machines) return null + return this.missionConfig.machines.find(m => m.row === ri && m.col === ci) || null + }, + getPositionAt(ri, ci) { + if (!this.missionConfig.machines) return null + const machine = this.getMachineAt(ri, ci) + if (!machine) return null + if (ri === 0) return machine.front + const prevMachine = this.getMachineAt(ri - 1, ci) + return prevMachine ? prevMachine.back : machine.front + }, + onCellClick(ri, ci) { + const m = this.getMachineAt(ri, ci) + if (!m) { + // 无机器 → 创建机器记录并选中 + this.createMachine(ri, ci).then(ok => { + if (ok) { + const created = this.getMachineAt(ri, ci) + if (created) this.selectMachine(created) + } + }) + } else { + // 有机器 → 选中 + this.selectMachine(m) + } + }, + async createMachine(ri, ci) { + try { + const machineId = 'm_' + ri + '_' + ci + const res = await fetch(API + '/api/mission/machines/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: machineId, + row: ri, + col: ci, + front: { coords: [0, 0, 0], poses: [] }, + back: { coords: [0, 0, 0], poses: [] } + }) + }) + const data = await res.json() + if (!data.ok && data.error !== '该位置已有机器') { + alert('创建机器失败: ' + (data.error || '未知错误')) + return false + } + await this.loadAllMachines() + return true + } catch (e) { alert('创建机器失败: ' + e.message); return false } + }, + selectMachine(machine) { + if (!machine.front) machine.front = { coords: [0, 0, 0], poses: [] } + else if (!Array.isArray(machine.front.coords)) machine.front.coords = [0, 0, 0] + if (!machine.back) machine.back = { coords: [0, 0, 0], poses: [] } + else if (!Array.isArray(machine.back.coords)) machine.back.coords = [0, 0, 0] + this.selectedMachine = machine + }, + clearSelection() { + this.selectedMachine = null + }, + async deleteMachine(machineId) { + if (!confirm('确定删除此机器?')) return + try { + await fetch(API + '/api/mission/machines/' + machineId, { method: 'DELETE' }) + this.selectedMachine = null + await this.loadAllMachines() + } catch (e) { alert('删除失败: ' + e.message) } + }, + async saveMachineCoords() { + if (!this.selectedMachine) return + try { + const res = await fetch(API + '/api/mission/machines/' + this.selectedMachine.id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + front: this.selectedMachine.front, + back: this.selectedMachine.back + }) + }) + if (res.ok) { + this.mapMsg = '✅ 机器坐标已保存' + setTimeout(() => this.mapMsg = '', 2000) + } else { + alert('保存失败: ' + res.status) + } + } catch (e) { alert('保存失败: ' + e.message) } + }, + async readPosition(side) { + if (!this.agvConnected) { alert('AGV 未连接'); return } + try { + const res = await fetch(API + '/api/agv/position') + const data = await res.json() + if (data.ok && data.position) { + const [x, y, theta] = data.position + if (side === 'front') { + this.selectedMachine.front.coords = [x, y, theta] + } else { + this.selectedMachine.back.coords = [x, y, theta] + } + } else { + alert('读取位置失败: ' + (data.error || '未知错误')) + } + } catch (e) { alert('读取位置失败: ' + e.message) } + }, + async addPoseToMachine(machineId, side) { + const name = this.poseForm.name || '姿态' + (((this.selectedMachine && this.selectedMachine[side] && this.selectedMachine[side].poses) || []).length + 1) + try { + const res = await fetch(API + '/api/mission/poses/' + machineId + '/' + side, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name, + arm_angles: this.currentAngles.length === 6 ? this.currentAngles : [0, 0, 0, 0, 0, 0], + speed: 500, + description: '' + }) + }) + const data = await res.json() + if (data.ok) { + this.poseForm.name = '' + await this.loadAllMachines() + // 重新选中当前机器以刷新姿态列表 + const updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col) + if (updated) this.selectMachine(updated) + } else { + alert('添加姿态失败: ' + (data.error || '未知错误')) + } + } catch (e) { alert('添加姿态失败: ' + e.message) } + }, + async deletePose(machineId, side, poseId) { + if (!confirm('确定删除此姿态?')) return + try { + await fetch(API + '/api/mission/poses/' + machineId + '/' + side + '/' + poseId, { method: 'DELETE' }) + await this.loadAllMachines() + if (this.selectedMachine) { + const updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col) + if (updated) this.selectMachine(updated) + } + } catch (e) { alert('删除姿态失败: ' + e.message) } + }, + async capturePosition(ri, ci, side) { + if (!this.agvConnected) { alert('请先连接AGV'); return } + let machine = this.getMachineAt(ri, ci) + if (!machine) { + try { + const machineId = 'm_' + ri + '_' + ci + const res = await fetch(API + '/api/mission/machines/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: machineId, + row: ri, + col: ci, + front: { coords: [0, 0, 0], poses: [] }, + back: { coords: [0, 0, 0], poses: [] } + }) + }) + if (!res.ok) throw new Error('创建失败') + await this.loadAllMachines() + machine = this.getMachineAt(ri, ci) + } catch (e) { alert('创建机器失败: ' + e.message); return } + } + try { + const res = await fetch(API + '/api/agv/position') + const pos = await res.json() + let x = 0, y = 0, theta = 0 + if (pos.ok && pos.position && Array.isArray(pos.position)) { + x = pos.position[0] || 0 + y = pos.position[1] || 0 + theta = pos.position[2] || 0 + } else { + alert('读取位置失败: ' + (pos.error || '未知错误')) + return + } + if (!machine) { machine = this.getMachineAt(ri, ci) } + if (!machine) { alert('机器记录不存在'); return } + if (side === 'front') { machine.front.coords = [x, y, theta] } else { machine.back.coords = [x, y, theta] } + await fetch(API + '/api/mission/machines/' + machine.id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(machine) + }) + alert((side === 'front' ? '正面' : '背面') + '点位已更新: (' + x.toFixed(2) + ',' + y.toFixed(2) + ',' + theta.toFixed(2) + ')') + } catch (e) { alert('读取位置失败: ' + e.message) } + }, + async refreshSequence() { + try { + const res = await fetch(API + '/api/mission/generate_sequence') + const data = await res.json() + if (data.ok) { + this.sequence = data.sequence || [] + } + } catch (e) { console.error('刷新序列失败', e) } + }, + // === 机械臂 === + async refreshAngles() { + if (!this.armConnected) return + try { + const res = await fetch(API + '/api/arm/get_angles') + const data = await res.json() + if (data.ok && data.angles) { + this.currentAngles = data.angles + this.angleInputs = [...data.angles] + } + } catch (e) {} + }, + async setAngle(idx, val) { + await fetch(API + '/api/arm/set_angle', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ joint: 'J' + (idx + 1), angle: val }) + }) + }, + async applyAngles() { + await fetch(API + '/api/arm/set_angles', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ angles: this.angleInputs, speed: 500 }) + }) + }, + jogStart(idx, dir) { + const joint = 'J' + (idx + 1) + fetch(API + '/api/arm/jog', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ joint, direction: dir }) + }) + this.jogIntervals[idx] = setInterval(() => this.refreshAngles(), 200) + }, + jogStop(idx) { + clearInterval(this.jogIntervals[idx]) + const joint = 'J' + (idx + 1) + fetch(API + '/api/arm/jog', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ joint, direction: 0 }) + }) + setTimeout(() => this.refreshAngles(), 300) + }, + onPreviewError(e) { + e.target.style.display = 'none' + }, + // === AGV 控制 === + async refreshAgvPosition() { + if (!this.agvConnected) return + try { + const res = await fetch(API + '/api/agv/position') + const data = await res.json() + if (data.ok) { + this.agvPosition = data.position + this.agvBattery = data.battery + } + } catch (e) {} + }, + agvMoveStart(dir) { + if (!this.agvConnected) return + fetch(API + '/api/agv/move', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ direction: dir, speed: this.agvSpeed }) + }) + }, + agvMoveStop() { + fetch(API + '/api/agv/move', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ direction: 'stop' }) + }) + }, + async agvStop() { + await fetch(API + '/api/agv/stop', { method: 'POST' }) + }, + async agvResetCollision() { + if (!this.agvConnected) { + alert('AGV 未连接') + return + } + if (!confirm('确定执行撞物体后复位?')) return + try { + const res = await fetch(API + '/api/agv/reset', { method: 'POST' }) + const data = await res.json() + if (data.ok) { + alert('✅ ' + data.message) + await this.refresh() + await this.refreshAgvPosition() + } else { + alert('❌ 复位失败: ' + (data.error || '未知错误')) + } + } catch (e) { + alert('❌ 复位请求失败: ' + e.message) + } + }, + } +}).mount('#app') diff --git a/agv_app/start.sh b/agv_app/start.sh new file mode 100644 index 0000000..ad9d646 --- /dev/null +++ b/agv_app/start.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# 启动 AGV 拍摄系统 + +cd ~/work/agv_app +python3 app.py diff --git a/agv_app/start_all.sh b/agv_app/start_all.sh new file mode 100755 index 0000000..9ee8f09 --- /dev/null +++ b/agv_app/start_all.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# AGV 拍摄系统完整启动脚本 - ROS2 + Flask +# 使用方法: ./start_all.sh + +echo "=== 停止旧进程 ===" +pkill -f "ros2 launch agv_pro_bringup" 2>/dev/null +pkill -f "python.*app.py" 2>/dev/null +sleep 2 + +echo "=== 启动 ROS2 Bringup ===" +# Source ROS2 环境 +source /opt/ros/humble/setup.bash +cd /home/elephant/agv_pro_ros2 +source install/setup.bash + +# 启动 ROS2 bringup (后台运行) +nohup ros2 launch agv_pro_bringup agv_pro_bringup.launch.py port_name:=/dev/agvpro_controller > /tmp/ros2_bringup.log 2>&1 & +ROS2_PID=$! +echo "ROS2 bringup started, PID: $ROS2_PID" + +# 等待 ROS2 初始化 (AGV节点需要连接串口) +echo "等待 ROS2 初始化..." +sleep 5 + +# 检查 ROS2 节点是否启动 +if source /opt/ros/humble/setup.bash && source ~/agv_pro_ros2/install/setup.bash && ros2 node list 2>/dev/null | grep -q agv_pro_node; then + echo "✅ ROS2 AGV 节点已启动" +else + echo "⚠️ ROS2 节点启动可能失败,请检查日志: /tmp/ros2_bringup.log" +fi + +echo "=== 启动 Flask ===" +cd /home/elephant/work/agv_app +nohup python3 app.py > /tmp/agv_flask.log 2>&1 & +FLASK_PID=$! +echo "Flask started, PID: $FLASK_PID" + +sleep 2 +echo "" +echo "=== 启动完成 ===" +echo "ROS2 log: /tmp/ros2_bringup.log" +echo "Flask log: /tmp/agv_flask.log" +echo "" +echo "检查状态:" +echo " ros2 node list" +echo " curl http://localhost:5000/api/status" diff --git a/agv_app/start_flask.sh b/agv_app/start_flask.sh new file mode 100755 index 0000000..8e50f0b --- /dev/null +++ b/agv_app/start_flask.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Flask 启动脚本 - 杀掉旧进程并重启 + +pkill -f "python.*app.py" 2>/dev/null +sleep 1 + +cd /home/elephant/work/agv_app +nohup python3 app.py > /tmp/agv_flask.log 2>&1 & +echo "Flask started, PID: $!" diff --git a/agv_app/static/css/style.css b/agv_app/static/css/style.css new file mode 100644 index 0000000..af9e83a --- /dev/null +++ b/agv_app/static/css/style.css @@ -0,0 +1,722 @@ +/* ========== 全局样式 ========== */ +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; + background: #0f1923; + color: #e8eaed; + font-size: 14px; + min-height: 100vh; +} + +a { color: #4fc3f7; text-decoration: none; } +a:hover { text-decoration: underline; } + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px 16px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +/* ========== 顶部栏 ========== */ +.topbar { + background: #1a2332; + border-bottom: 1px solid #2a3441; + padding: 0 20px; + display: flex; + align-items: center; + height: 56px; + gap: 32px; + position: sticky; + top: 0; + z-index: 100; +} + +.logo { font-size: 18px; font-weight: bold; color: #4fc3f7; } + +.nav { display: flex; gap: 4px; } +.nav-link { + padding: 8px 16px; + border-radius: 6px; + color: #9aa0a6; + transition: all 0.2s; +} +.nav-link:hover { background: #2a3441; color: #e8eaed; text-decoration: none; } +.nav-link.active { background: #263238; color: #4fc3f7; } + +.status-bar { margin-left: auto; display: flex; gap: 12px; } +.status-item { + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: bold; +} +.status-item.setting { background: #1b3a2f; color: #4caf50; } +.status-item.running { background: #2a2a1b; color: #ffeb3b; } +.status-item.paused { background: #3a2a1a; color: #ff9800; } +.status-item.idle { background: #2a2a2a; color: #9aa0a6; } + +/* ========== 卡片 ========== */ +.card { + background: #1a2332; + border-radius: 12px; + padding: 20px; + border: 1px solid #2a3441; +} +.card h2 { font-size: 16px; margin-bottom: 16px; color: #4fc3f7; } + +/* ========== 状态卡片 ========== */ +.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; } +.status-card { + background: #0f1923; + border-radius: 8px; + padding: 16px; + text-align: center; + border: 1px solid #2a3441; +} +.status-card.ok { border-color: #2e7d32; background: #0d1f14; } +.status-card.error { border-color: #c62828; background: #1f0d0d; } +.status-icon { font-size: 24px; margin-bottom: 8px; } +.status-label { font-size: 12px; color: #9aa0a6; margin-bottom: 4px; } +.status-value { font-size: 14px; font-weight: bold; } + +/* ========== 按钮 ========== */ +.btn { + padding: 8px 16px; + border-radius: 6px; + border: none; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; + background: #263238; + color: #e8eaed; + font-family: inherit; +} +.btn:hover:not(:disabled) { background: #37474f; } +.btn:disabled { opacity: 0.5; cursor: not-allowed; } + +.btn-primary { background: #0277bd; color: #fff; } +.btn-primary:hover:not(:disabled) { background: #0288d1; } +.btn-secondary { background: #37474f; } +.btn-danger { background: #d32f2f; color: #fff; } +.btn-danger:hover:not(:disabled) { background: #f44336; } +.btn-success { background: #2e7d32; color: #fff; } +.btn-success:hover:not(:disabled) { background: #388e3c; } +.btn-warning { background: #e65100; color: #fff; } +.btn-error { background: #c62828; color: #fff; } +.btn-large { padding: 12px 24px; font-size: 16px; } +.btn-small { padding: 4px 10px; font-size: 12px; } +.btn-icon { background: none; border: none; cursor: pointer; font-size: 14px; padding: 4px; } +.btn-row { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; } + +/* ========== 表单 ========== */ +.form-group { margin-bottom: 12px; } +.form-group label { display: block; font-size: 12px; color: #9aa0a6; margin-bottom: 4px; } +.form-group input, +.form-group select { + width: 100%; + padding: 8px 12px; + background: #0f1923; + border: 1px solid #2a3441; + border-radius: 6px; + color: #e8eaed; + font-size: 14px; + font-family: inherit; +} +.form-group input:focus, +.form-group select:focus { outline: none; border-color: #4fc3f7; } +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + +/* ========== Tabs ========== */ +.tabs { + background: #1a2332; + border-bottom: 1px solid #2a3441; + padding: 0 20px; + display: flex; + gap: 4px; +} +.tab { + padding: 12px 20px; + background: none; + border: none; + color: #9aa0a6; + cursor: pointer; + font-size: 14px; + border-bottom: 2px solid transparent; + font-family: inherit; +} +.tab.active { color: #4fc3f7; border-bottom-color: #4fc3f7; } +.tab:hover { color: #e8eaed; } + +/* ========== 摄像头预览 ========== */ +.camera-preview { + width: 100%; + max-width: 480px; + border-radius: 8px; + overflow: hidden; + margin: 0 auto 16px; + background: #000; +} +.camera-preview img, +.camera-full img { + width: 100%; + display: block; + aspect-ratio: 16/9; + object-fit: cover; +} +.camera-full { + width: 100%; + border-radius: 8px; + overflow: hidden; + background: #000; +} + +/* ========== 关节控制 ========== */ +.joints-panel { margin-top: 16px; } +.joints-panel h3 { margin-bottom: 12px; font-size: 14px; } +.joint-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.joint-control { + background: #0f1923; + border-radius: 8px; + padding: 12px; + text-align: center; + border: 1px solid #2a3441; +} +.joint-control label { font-size: 12px; color: #4fc3f7; font-weight: bold; } +.joint-value { font-size: 18px; font-weight: bold; color: #fff; margin: 4px 0; } +.joint-buttons { display: flex; align-items: center; gap: 4px; justify-content: center; } +.joint-buttons button { + width: 32px; + height: 32px; + border-radius: 4px; + border: 1px solid #2a3441; + background: #263238; + color: #e8eaed; + cursor: pointer; + font-size: 14px; +} +.joint-buttons input { + width: 60px; + padding: 4px; + text-align: center; + background: #0f1923; + border: 1px solid #2a3441; + border-radius: 4px; + color: #e8eaed; + font-size: 12px; +} + +/* ========== 点位列表 ========== */ +.point-item { + background: #0f1923; + border: 1px solid #2a3441; + border-radius: 8px; + padding: 12px; + margin-bottom: 12px; +} +.point-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } +.point-name { font-weight: bold; font-size: 15px; } +.point-coords { font-size: 12px; color: #9aa0a6; margin-bottom: 8px; } +.badge { + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + background: #263238; + color: #4fc3f7; +} +.pose-list { margin-top: 8px; } +.pose-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid #2a3441; + font-size: 13px; +} +.angles { color: #9aa0a6; font-size: 11px; font-family: monospace; } +.pose-add { + display: flex; + gap: 8px; + align-items: center; + margin-top: 8px; +} +.pose-add input { flex: 1; padding: 6px 10px; background: #1a2332; border: 1px solid #2a3441; border-radius: 4px; color: #e8eaed; font-size: 13px; } +.pose-add select { padding: 6px; background: #1a2332; border: 1px solid #2a3441; border-radius: 4px; color: #e8eaed; } + +.empty-hint { color: #9aa0a6; text-align: center; padding: 20px; } +.hint { font-size: 12px; color: #9aa0a6; margin-top: 8px; } +.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 12px; } +.alert-error { background: #1f0d0d; border: 1px solid #c62828; color: #ef5350; } +.checkbox-group { display: flex; gap: 16px; } +.checkbox-group label { display: flex; align-items: center; gap: 6px; cursor: pointer; color: #e8eaed; } + +/* ========== 运行页面 ========== */ +.running-header { display: flex; align-items: center; gap: 20px; margin-bottom: 16px; } +.running-status { + font-size: 18px; + font-weight: bold; + display: flex; + align-items: center; + gap: 8px; +} +.running-status.idle { color: #9aa0a6; } +.running-status.running { color: #4caf50; } +.running-status.paused { color: #ff9800; } +.pulse { + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + animation: pulse 1.5s infinite; +} +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } +} +.running-progress { flex: 1; display: flex; align-items: center; gap: 12px; } +.progress-bar { flex: 1; height: 8px; background: #2a3441; border-radius: 4px; overflow: hidden; } +.progress-fill { height: 100%; background: #4fc3f7; border-radius: 4px; transition: width 0.3s; } + +/* ========== 报告 ========== */ +.report-summary { display: flex; gap: 16px; margin-bottom: 16px; } +.stat { padding: 8px 16px; border-radius: 8px; background: #0f1923; border: 1px solid #2a3441; } +.stat.ok { border-color: #2e7d32; color: #4caf50; } +.stat.error { border-color: #c62828; color: #ef5350; } +.report-item { padding: 8px 12px; background: #0f1923; border-radius: 6px; margin-bottom: 8px; border: 1px solid #2a3441; } +.report-point { display: flex; align-items: center; gap: 8px; font-weight: bold; } +.report-status { font-size: 16px; } +.report-pose { font-size: 12px; color: #9aa0a6; padding-left: 24px; margin-top: 4px; } + +/* ========== 响应式 ========== */ +@media (max-width: 768px) { + .container { grid-template-columns: 1fr; } + .grid-3 { grid-template-columns: 1fr; } + .form-row { grid-template-columns: 1fr; } + .joint-grid { grid-template-columns: repeat(2, 1fr); } + .form-row { grid-template-columns: 1fr; } +} + +/* AGV 移动控制面板 */ +.agv-status-bar { + display: flex; + gap: 16px; + align-items: center; + padding: 10px 14px; + background: #0f1923; + border-radius: 8px; + margin-bottom: 16px; + font-size: 13px; + color: #9aa0a6; + flex-wrap: wrap; +} +.agv-status-bar strong { color: #e8eaed; } + +.agv-control-panel { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + max-width: 280px; + margin: 0 auto; +} +.agv-dir-row { + display: grid; + grid-template-columns: 80px 80px 80px; + gap: 6px; + width: 100%; +} +.agv-dir-placeholder { width: 80px; height: 44px; } +.agv-btn { + height: 44px; + border-radius: 8px; + border: 1px solid #2a3441; + background: #263238; + color: #e8eaed; + cursor: pointer; + font-size: 13px; + font-family: inherit; + transition: background 0.15s; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + user-select: none; +} +.agv-btn:active, .agv-btn:focus { outline: none; } +.agv-btn-up { background: #1b3a2f; border-color: #2e7d32; color: #4caf50; } +.agv-btn-down { background: #3a1b1b; border-color: #7d2e2e; color: #f44336; } +.agv-btn-left { background: #1b2d3a; border-color: #1565c0; color: #42a5f5; } +.agv-btn-right { background: #2d2a1b; border-color: #7d6e2e; color: #ffc107; } +.agv-btn-stop { background: #37474f; border-color: #546e7a; } +.agv-btn-up:active { background: #1e4d38; } +.agv-btn-down:active { background: #4d2020; } +.agv-btn-left:active { background: #1e3a4d; } +.agv-btn-right:active { background: #3d3820; } +.agv-btn-stop:active { background: #455a64; } +.agv-btn-lateral { + background: #2d1b4a; + border-color: #7c4dff; + color: #b388ff; + font-size: 13px; + min-width: 120px; +} +.agv-btn-lateral:active { background: #3d2560; } +.agv-lateral-row { + display: flex; + gap: 12px; + justify-content: center; + margin-top: 8px; + max-width: 280px; + width: 100%; +} + +.speed-control { + display: flex; + align-items: center; + gap: 10px; +} +.speed-value { + min-width: 44px; + text-align: right; + font-weight: bold; + color: #4fc3f7; +} + +/* 双摄像头预览布局 */ +.camera-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-top: 12px; +} +.camera-box { + background: #111; + border-radius: 8px; + overflow: hidden; +} +.camera-label { + padding: 8px 12px; + font-size: 13px; + color: #aaa; + background: #1a1a1a; + border-bottom: 1px solid #333; +} +.camera-img { + width: 100%; + display: block; + aspect-ratio: 4/3; + object-fit: cover; +} +.camera-placeholder { + width: 100%; + aspect-ratio: 4/3; + display: flex; + align-items: center; + justify-content: center; + color: #666; + font-size: 14px; +} +/* ========== 地图标记 ========== */ +.map-marker { + position: absolute; + transform: translate(-50%, -100%); + font-size: 20px; + cursor: pointer; + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); + z-index: 10; +} +.map-marker:hover { + transform: translate(-50%, -100%) scale(1.2); +} + +/* ========== 任务配置 M×N 网格 ========== */ +.mission-grid-wrap { + margin-top: 12px; + overflow-x: auto; +} +.mission-grid { + display: grid; + gap: 4px; + grid-template-columns: 80px repeat(var(--cols,4), 90px); +} +.grid-cell { + min-width: 80px; + min-height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + font-size: 12px; + cursor: pointer; + border: 1px solid #2a3441; + background: #0f1923; + transition: background 0.15s, border-color 0.15s; +} +.grid-cell.active { + background: #1b3a2f; + border-color: #2e7d32; + color: #4caf50; +} +.grid-cell.active:hover { + background: #234; +} +.grid-cell.selected { + border-color: #4fc3f7 !important; + box-shadow: 0 0 0 2px #4fc3f7; +} +.grid-header { + background: transparent; + border-color: transparent; + cursor: default; + font-weight: bold; + color: #9aa0a6; + font-size: 12px; +} + +/* 机器配置表单 */ +.machine-form { + background: #0f1923; + border: 1px solid #2a3441; + border-radius: 8px; + padding: 16px; + margin-top: 12px; +} +.machine-form h3 { + font-size: 14px; + color: #4fc3f7; + margin-bottom: 10px; +} +.machine-form h4 { + font-size: 13px; + color: #9aa0a6; + margin: 8px 0 6px; +} + +/* 姿态列表 */ +.pose-list { + margin-top: 8px; +} +.pose-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid #2a3441; + font-size: 13px; +} +.pose-name { + font-weight: bold; + min-width: 80px; +} +.pose-angles { + color: #9aa0a6; + font-size: 11px; + font-family: monospace; + flex: 1; +} +.pose-add { + display: flex; + gap: 8px; + align-items: center; + margin-top: 8px; +} +.pose-add input { + flex: 1; + padding: 6px 10px; + background: #1a2332; + border: 1px solid #2a3441; + border-radius: 4px; + color: #e8eaed; + font-size: 13px; +} + +/* 蛇形序列预览 */ +.sequence-preview { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 320px; + overflow-y: auto; +} +.sequence-step { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 12px; + background: #0f1923; + border-radius: 6px; + border: 1px solid #2a3441; + font-size: 13px; +} +.step-index { + background: #263238; + color: #4fc3f7; + border-radius: 10px; + min-width: 28px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: bold; +} +.step-info { + flex: 1; +} +.step-side { + padding: 2px 8px; + border-radius: 8px; + font-size: 11px; + font-weight: bold; +} +.step-side:contains('正面') { + background: #1b3a2f; + color: #4caf50; +} +.step-side:contains('背面') { + background: #3a1b2f; + color: #ce93d8; +} + +/* 网格单元格点位配置 */ +.cell-machine { + font-size: 11px; + font-weight: bold; + color: #2c3e50; +} +.cell-points { + margin-top: 2px; + font-size: 9px; +} +.point-row { + display: flex; + align-items: center; + gap: 2px; + padding: 1px 2px; + background: #f8f9fa; + border-radius: 3px; + cursor: pointer; + margin: 1px 0; +} +.point-row:hover { + background: #e9ecef; +} +.point-label { + color: #666; + min-width: 24px; +} +.point-coords { + color: #0366d6; + font-family: monospace; + font-size: 8px; + flex: 1; +} +.btn-icon-small { + background: none; + border: none; + cursor: pointer; + font-size: 10px; + padding: 1px 3px; + border-radius: 3px; +} +.btn-icon-small:hover { + background: #ddd; +} + +/* ========== 任务配置 弹窗 + 网格增强样式 ========== */ + +/* 弹窗遮罩 */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} +.modal-box { + background: #1a1f2e; + border: 1px solid #2a3a50; + border-radius: 12px; + padding: 20px 24px; + min-width: 380px; + max-width: 500px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); +} +.modal-box h3 { margin: 0 0 8px; color: #e0e6f0; font-size: 16px; } + +/* 点位行单元格 */ +.point-cell { cursor: pointer; flex-direction: column; gap: 2px; } +.point-cell:hover { border-color: #4fc3f7; background: #162030; } +.point-cell.point-filled { background: #0d2535; border-color: #1565c0; } +.point-coords { font-size: 10px; color: #64b5f6; font-family: monospace; } +.point-empty { font-size: 10px; color: #455a64; } + +/* 机器行单元格 */ +.machine-cell { cursor: pointer; } +.machine-cell:hover { border-color: #4caf50; background: #1b3a2f; } +.machine-cell.active { background: #1b3a2f; border-color: #2e7d32; color: #4caf50; } +.machine-icon { font-size: 18px; } +.machine-empty { font-size: 16px; color: #455a64; } +/* ========== 任务配置 弹窗 + 网格增强样式 ========== */ + +/* 弹窗遮罩 */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} +.modal-box { + background: #1a1f2e; + border: 1px solid #2a3a50; + border-radius: 12px; + padding: 20px 24px; + min-width: 380px; + max-width: 500px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); +} +.modal-box h3 { margin: 0 0 8px; color: #e0e6f0; font-size: 16px; } + +/* 点位行单元格 */ +.point-cell { cursor: pointer; flex-direction: column; gap: 2px; } +.point-cell:hover { border-color: #4fc3f7; background: #162030; } +.point-cell.point-filled { background: #0d2535; border-color: #1565c0; } +.point-coords { font-size: 10px; color: #64b5f6; font-family: monospace; } +.point-empty { font-size: 10px; color: #455a64; } + +/* 机器行单元格 */ +.machine-cell { cursor: pointer; } +.machine-cell:hover { border-color: #4caf50; background: #1b3a2f; } +.machine-cell.active { background: #1b3a2f; border-color: #2e7d32; color: #4caf50; } +.machine-icon { font-size: 18px; } +.machine-empty { font-size: 16px; color: #455a64; } +/* 点位编辑弹窗 */ +.modal-overlay .modal-box { min-width: 420px; } +.modal-overlay .form-row { gap: 8px; } +.modal-overlay .btn-row { gap: 8px; flex-wrap: wrap; } + +/* 地图坐标点覆盖层 */ +.map-container { position: relative; } +.map-overlay { + position: absolute; top: 0; left: 0; right: 0; bottom: 0; + pointer-events: none; z-index: 10; +} +.map-dot { + position: absolute; + transform: translate(-50%, -50%); +} +.point-dot { + width: 10px; height: 10px; + background: #f39c12; + border-radius: 50%; + border: 2px solid #fff; + box-shadow: 0 0 6px rgba(243,156,18,0.9); +} diff --git a/agv_app/static/js/app.js b/agv_app/static/js/app.js new file mode 100644 index 0000000..8cc7260 --- /dev/null +++ b/agv_app/static/js/app.js @@ -0,0 +1,121 @@ +const { createApp } = Vue + +const API = '' + +createApp({ + delimiters: ['[[', ']]'], + + data() { + return { + connecting: false, + agvConnected: false, + armConnected: false, + cameraOpened: false, + armCameraOpened: false, + mapLoaded: false, + mapConfig: {}, + pointsCount: 0, + currentState: 'idle', + // 摄像头轮询 + agvCameraSrc: '/api/camera/refresh?t=' + Date.now(), + armCameraSrc: '/api/camera/arm_refresh?t=' + Date.now(), + agvCameraError: false, + armCameraError: false, + reconnectingDevice: null + } + }, + computed: { + allReady() { + return this.agvConnected && this.armConnected && this.cameraOpened && this.mapLoaded + }, + statusClass() { + return this.currentState + }, + statusText() { + const map = { idle: '空闲', setting: '设置模式', running: '运行中', paused: '已暂停' } + return map[this.currentState] || '未知' + } + }, + mounted() { + this.refresh() + setInterval(this.refreshStatus, 3000) + this.refreshCams() + setInterval(() => this.refreshCams(), 2000) + }, + methods: { + refreshCams() { + this.agvCameraSrc = '/api/camera/refresh?t=' + Date.now() + this.armCameraSrc = '/api/camera/arm_refresh?t=' + Date.now() + }, + async refresh() { + await this.refreshStatus() + await this.loadPoints() + }, + async refreshStatus() { + try { + const res = await fetch(API + '/api/status') + const data = await res.json() + this.agvConnected = data.agv_connected + this.armConnected = data.arm_connected + this.cameraOpened = data.camera_opened + this.armCameraOpened = data.arm_camera_opened + this.mapLoaded = data.map_loaded + this.currentState = data.state || 'idle' + if (data.map_loaded && data.map) { + this.mapConfig = data.map + } + } catch (e) { + console.error(e) + } + }, + async loadPoints() { + try { + const res = await fetch(API + '/api/points/list') + const data = await res.json() + this.pointsCount = data.points ? data.points.length : 0 + } catch (e) {} + }, + async connectAll() { + this.connecting = true + try { + const res = await fetch(API + '/api/system/connect', { method: 'POST' }) + const data = await res.json() + if (data.errors && data.errors.length) { + alert('部分连接失败:\n' + data.errors.join('\n')) + } + await this.refreshStatus() + } finally { + this.connecting = false + } + }, + async disconnectAll() { + await fetch(API + '/api/system/disconnect', { method: 'POST' }) + await this.refreshStatus() + }, + async connectDevice(device) { + if (this.connecting) return + this.reconnectingDevice = device + try { + const res = await fetch(API + '/api/device/connect', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({device}) + }) + const data = await res.json() + if (!data.ok && data.error) { + alert(data.device + ' 重连失败: ' + data.error) + } + await this.refreshStatus() + } finally { + this.reconnectingDevice = null + } + }, + async goRunning() { + if (!this.allReady) { + alert('请先连接所有设备并加载地图') + } else { + window.location.href = '/running' + } + } + } +}).mount('#app') diff --git a/agv_app/static/js/running.js b/agv_app/static/js/running.js new file mode 100644 index 0000000..bcb3e9a --- /dev/null +++ b/agv_app/static/js/running.js @@ -0,0 +1,80 @@ +const { createApp } = Vue +const API = '' + +createApp({ + delimiters: ['[[', ']]'], + + data() { + return { + missionState: 'idle', + currentPoint: 0, + totalPoints: 0, + report: null, + previewUrl: API + '/api/camera/preview', + polling: null + } + }, + computed: { + missionStateText() { + const map = { idle: '空闲', running: '任务运行中', paused: '已暂停', completed: '已完成' } + return map[this.missionState] || '未知' + }, + progressPercent() { + if (!this.totalPoints) return 0 + return Math.round((this.currentPoint / this.totalPoints) * 100) + } + }, + mounted() { + this.poll() + }, + beforeUnmount() { + if (this.polling) clearInterval(this.polling) + }, + methods: { + poll() { + this.refresh() + this.polling = setInterval(this.refresh, 2000) + }, + async refresh() { + try { + const res = await fetch(API + '/api/mission/state') + const data = await res.json() + this.missionState = data.state || 'idle' + + if (this.missionState === 'running') { + const reportRes = await fetch(API + '/api/mission/report') + const reportData = await reportRes.json() + if (reportData.report) { + this.totalPoints = reportData.report.total_points || 0 + this.currentPoint = reportData.report.details?.length || 0 + this.report = reportData.report + } + } else if (this.missionState === 'idle') { + const reportRes = await fetch(API + '/api/mission/report') + const reportData = await reportRes.json() + if (reportData.report) { + this.report = reportData.report + this.totalPoints = reportData.report.total_points || 0 + this.currentPoint = reportData.report.details?.length || 0 + } + } + } catch (e) {} + }, + async startMission() { + if (this.missionState !== 'idle') return + await fetch(API + '/api/mission/start', { method: 'POST' }) + this.missionState = 'running' + }, + async pauseMission() { + await fetch(API + '/api/mission/pause', { method: 'POST' }) + this.missionState = 'paused' + }, + async stopMission() { + await fetch(API + '/api/mission/stop', { method: 'POST' }) + this.missionState = 'idle' + }, + onPreviewError(e) { + e.target.style.display = 'none' + } + } +}).mount('#app') diff --git a/agv_app/static/js/setting.js b/agv_app/static/js/setting.js new file mode 100644 index 0000000..d401e53 --- /dev/null +++ b/agv_app/static/js/setting.js @@ -0,0 +1,887 @@ +const { createApp } = Vue +const API = '' + +const app = createApp({ + data() { + return { + tab: 'map', + // 任务配置 + missionConfig: { rows: 3, cols: 3, grid: [], machines: [], positions: [] }, + // 点位编辑弹窗 + editingPoint: null, // 当前编辑的点位 {pointRow, col} — pointRow是点位行号(0~rows) + pointEditor: { x: 0, y: 0, yaw: 0 }, + selectedMachine: null, + sequence: [], + poseForm: { name: '', photo_type: 'front', description: '' }, + // 地图 + mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' }, + mapMsg: '', + mapLoaded: false, + mapImageUrl: '', + mapMeta: null, + mapVersion: 0, // 地图点位版本号,用于强制重新渲染 + // 点位 + points: [], + newPointName: '', + newPointMode: 'front', + newPointSequence: ['front', 'back'], + // 机型(姿态组) + models: [], + selectedModelId: null, + newModelName: '', + newModelDesc: '', + newModelNotes: '', + newPoseForm: {}, // 机型配置:新建姿态的表单 + // 机械臂 + armConnected: false, + currentAngles: [], + angleInputs: [], + previewUrl: API + '/api/camera/preview', + jogIntervals: {}, + // AGV + cameraOpened: false, + agvConnected: false, + agvBattery: null, + agvPosition: null, + agvSpeed: 0.5, + agvMoveInterval: null, + agvCameraUrl: API + '/api/camera/refresh', + agvCameraTimer: null, + } + }, + mounted() { + this.refresh() + this.refreshAngles() + }, + watch: { + // 监听点位数据变化,自动刷新地图 + 'missionConfig.positions'() { + this.mapVersion++ + }, + tab(val) { + if (val === 'agv') { + this.agvCameraTimer = setInterval(() => { + this.agvCameraUrl = API + '/api/camera/refresh?t=' + Date.now() + }, 1000) + } else { + if (this.agvCameraTimer) { + clearInterval(this.agvCameraTimer) + this.agvCameraTimer = null + } + } + } + }, + beforeUnmount() { + Object.values(this.jogIntervals).forEach(i => clearInterval(i)) + if (this.agvCameraTimer) clearInterval(this.agvCameraTimer) + }, + methods: { + async refresh() { + try { + const res = await fetch(API + '/api/status') + const data = await res.json() + this.agvConnected = data.agv_connected + this.armConnected = data.arm_connected + this.cameraOpened = data.camera_opened + this.mapLoaded = data.map_loaded + if (data.map_loaded) { + this.mapImageUrl = API + '/api/map/image?t=' + Date.now() + try { + const metaRes = await fetch(API + '/api/map/meta') + const meta = await metaRes.json() + if (meta.ok) this.mapMeta = meta + } catch (e) {} + } + } catch (e) {} + await this.loadAllPoints() + await this.loadAllModels() + await this.loadAllMachines() + await this.loadMissionConfig() + }, + // === 地图 === + async loadMap() { + const res = await fetch(API + '/api/map/load', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.mapForm) + }) + const data = await res.json() + this.mapMsg = data.ok ? '✅ 地图加载成功' : '❌ ' + (data.error || '加载失败') + this.mapLoaded = data.ok + if (data.ok) { + this.mapImageUrl = API + '/api/map/image?t=' + Date.now() + try { + const metaRes = await fetch(API + '/api/map/meta') + const meta = await metaRes.json() + if (meta.ok) this.mapMeta = meta + } catch (e) {} + } + }, + onMapError() { + this.mapMsg = '❌ 地图图像加载失败' + }, + getMapX(coords) { + if (!coords || !this.mapMeta) { + console.log('[getMapX] mapMeta not loaded, returning default 50'); + return 50 + } + const [x, y, yaw] = coords + const { resolution, origin, width } = this.mapMeta + const px = (x - origin[0]) / (resolution * width) * 100 + const result = Math.max(0, Math.min(100, px)); + console.log('[getMapX]', coords, '→ px%:', result); + return result + }, + getMapY(coords) { + if (!coords || !this.mapMeta) { + console.log('[getMapY] mapMeta not loaded, returning default 50'); + return 50 + } + const [x, y, yaw] = coords + const { resolution, origin, height } = this.mapMeta + const py = (y - origin[1]) / (resolution * height) * 100 + const result = Math.max(0, Math.min(100, 100 - py)); + console.log('[getMapY]', coords, '→ py%:', result); + return result + }, + async saveMap() { + await fetch(API + '/api/map/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.mapForm) + }) + this.mapMsg = '✅ 地图配置已保存' + }, + // === 点位 === + async loadAllPoints() { + const res = await fetch(API + '/api/points/list') + const data = await res.json() + this.points = data.points || [] + }, + async addPoint() { + const res = await fetch(API + '/api/points/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: this.newPointName || 'point_' + (this.points.length + 1), + photo_mode: this.newPointMode, + sequence: this.newPointSequence + }) + }) + const data = await res.json() + if (data.ok) { + await this.loadAllPoints() + this.newPointName = '' + } + }, + async deletePoint(id) { + if (!confirm('确定删除该点位?')) return + await fetch(API + '/api/points/delete/' + id, { method: 'DELETE' }) + await this.loadAllPoints() + }, + async saveAllPoints() { + await fetch(API + '/api/points/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ points: this.points }) + }) + alert('点位已保存') + }, + getPoint(id) { + return this.points.find(p => p.id === id) + }, + formatAngles(angles) { + if (!angles) return '—' + return angles.map(a => (a || 0).toFixed(1) + '°').join(' / ') + }, + // === 机型管理 === + async loadAllModels() { + const res = await fetch(API + '/api/models/list') + const data = await res.json() + this.models = data.models || [] + this.models.forEach(m => { + if (!this.poseForm[m.id]) { + this.poseForm[m.id] = { name: '', photo_type: 'front', description: '' } + } + }) + }, + async addModel() { + const res = await fetch(API + '/api/models/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: this.newModelName || '型号_' + (this.models.length + 1), + description: this.newModelDesc || '', + notes: this.newModelNotes || '', + serial_prefix: this.newModelSerial || '' + }) + }) + const data = await res.json() + if (data.ok) { + await this.loadAllModels() + this.newModelName = '' + this.newModelDesc = '' + this.newModelNotes = '' + this.newModelSerial = '' + } + }, + async deleteModel(modelId) { + if (!confirm('确定删除该机型?其下所有姿态将被删除!')) return + await fetch(API + '/api/models/delete/' + modelId, { method: 'DELETE' }) + await this.loadAllModels() + }, + // === 姿态管理(属于机型)=== + async addPose(modelId, photoType, customName) { + const form = this.poseForm[modelId] + const type = photoType || form?.photo_type || 'front' + const name = customName || form?.name || type + '_' + ((this.getModel(modelId)?.poses?.filter(p => p.photo_type === type).length || 0) + 1) + await fetch(API + '/api/models/poses/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model_id: modelId, + name: name, + photo_type: type, + arm_angles: this.currentAngles && this.currentAngles.length === 6 ? this.currentAngles : [0, 0, 0, 0, 0, 0], + speed: 500, + description: form?.description || '' + }) + }) + await this.loadAllModels() + if (form) { + form.name = '' + form.description = '' + } + }, + async deletePose(modelId, poseId) { + if (!confirm('确定删除该姿态?')) return + await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' }) + await this.loadAllModels() + }, + async updatePoseAngle(modelId, poseId, jointIndex, event) { + const value = parseFloat(event.target.value) + if (isNaN(value)) return + // Find the pose and update its angle + const model = this.getModel(modelId) + if (!model) return + const pose = model.poses.find(p => p.id === poseId) + if (!pose) return + if (!pose.arm_angles) pose.arm_angles = [0, 0, 0, 0, 0, 0] + pose.arm_angles[jointIndex] = value + // Save to backend + await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ arm_angles: pose.arm_angles }) + }) + }, + getModel(id) { + return this.models.find(m => m.id === id) + }, + // === 任务配置 === + async loadMissionConfig() { + try { + const res = await fetch(API + '/api/mission/config') + const data = await res.json() + if (data.ok && data.config) { + this.missionConfig.rows = data.config.rows || 3 + this.missionConfig.cols = data.config.cols || 3 + this.missionConfig.grid = data.config.grid || [] + this.missionConfig.machines = data.machines || [] + this.missionConfig.positions = data.config.positions || [] + this.mapVersion++ + } + } catch (e) { console.error('加载任务配置失败', e) } + }, + async generateGrid() { + try { + const res = await fetch(API + '/api/mission/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + rows: this.missionConfig.rows, + cols: this.missionConfig.cols, + grid: [] + }) + }) + const data = await res.json() + if (data.ok) { + this.missionConfig.grid = data.config.grid || [] + alert('✅ 网格已生成 (' + this.missionConfig.rows + '×' + this.missionConfig.cols + ')') + } else { + alert('❌ 网格生成失败') + } + } catch (e) { alert('请求失败: ' + e.message) } + }, + async saveMissionConfig() { + try { + const res = await fetch(API + '/api/mission/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + rows: this.missionConfig.rows, + cols: this.missionConfig.cols, + grid: this.missionConfig.grid + }) + }) + const data = await res.json() + if (data.ok) { + alert('✅ 网格配置已保存') + } + } catch (e) { alert('保存失败: ' + e.message) } + }, + async loadAllMachines() { + try { + const res = await fetch(API + '/api/mission/machines') + const data = await res.json() + this.missionConfig.machines = data.machines || [] + } catch (e) { console.error('加载机器列表失败', e) } + }, + + // ========== 点位行模型(独立于机器) ========== + + /** + * 获取指定点位行的数据 + * @param {number} pointRow - 点位行号,范围 0 ~ missionConfig.rows + * - pointRow=0 → 第1台机器的正面拍摄点 + * - pointRow=rows → 最后1台机器的背面拍摄点 + * - 中间 pointRow=i → 上面机器(i)的背面 + 下面机器(i+1)的正面 + * @param {number} col - 列号(0-based) + * @returns {object|null} 点位对象 { coords: [x,y,z], ... } 或 null + */ + getPointAt(pointRow, col) { + var positions = this.missionConfig.positions || [] + // 在独立 positions 数组中查找 (pointRow, col) + for (var i = 0; i < positions.length; i++) { + var p = positions[i] + if (parseInt(p.row) === pointRow && parseInt(p.col) === col) { + // 优先找 shoot,其次找 front/back + if (p.side === 'shoot') return p + // 再看有没有同一(row,col)的shoot + } + } + // 再找同(row,col)的shoot类型 + for (var i = 0; i < positions.length; i++) { + var p = positions[i] + if (parseInt(p.row) === pointRow && parseInt(p.col) === col && p.side === 'shoot') { + return p + } + } + // 兼容旧数据:尝试从机器对象的 front/back 获取 + var machineAbove = this.getMachineAt(pointRow - 1, col) // 上面的机器 + var machineBelow = this.getMachineAt(pointRow, col) // 下面的机器 + if (pointRow === 0 && machineBelow) { + // 第一个点位行 → 下面机器的正面 + return machineBelow.front || { coords: [0, 0, 0], poses: [] } + } + if (pointRow === this.missionConfig.rows && machineAbove) { + // 最后一个点位行 → 上面机器的背面 + return machineAbove.back || { coords: [0, 0, 0], poses: [] } + } + // 中间点位行:优先返回上面机器的背面 + if (machineAbove && machineAbove.back) { + return machineAbove.back + } + if (machineBelow && machineBelow.front) { + return machineBelow.front + } + return null + }, + + /** + * 获取点位的归属描述(用于弹窗标题) + * @param {number} pointRow - 点位行号 + * @param {number} col - 列号 + * @returns {string} 如 "第2列 · 机器2背面/机器3正面" + */ + getPointOwnerLabel(pointRow, col) { + var rows = this.missionConfig.rows + var labels = [] + if (pointRow === 0) { + // 第一行点位:下面机器的正面 + var m = this.getMachineAt(0, col) + if (m) labels.push('机器' + (m.row + 1) + '正面') + } else if (pointRow === rows) { + // 最后行点位:上面机器的背面 + var m2 = this.getMachineAt(rows - 1, col) + if (m2) labels.push('机器' + (m2.row + 1) + '背面') + } else { + // 中间点位行:上面机器的背面 + 下面机器的正面 + var mAbove = this.getMachineAt(pointRow - 1, col) + var mBelow = this.getMachineAt(pointRow, col) + if (mAbove) labels.push('机器' + (mAbove.row + 1) + '背面') + if (mBelow) labels.push('机器' + (mBelow.row + 1) + '正面') + } + if (labels.length === 0) return '第' + (col + 1) + '列 · 无归属' + return '第' + (col + 1) + '列 · ' + labels.join('/') + }, + + /** + * 检查点位是否可以清空 + * 只有当上下两台机器都不需要这个点位时才能清空 + */ + canClearPoint(pointRow, col) { + var rows = this.missionConfig.rows + // 如果上面有机器且机器存在 → 不能清空(上面机器需要此点位拍背面) + if (pointRow > 0 && pointRow <= rows) { + var mAbove = this.getMachineAt(pointRow - 1, col) + if (mAbove) return false + } + // 如果下面有机器且机器存在 → 不能清空(下面机器需要此点位拍正面) + if (pointRow >= 0 && pointRow < rows) { + var mBelow = this.getMachineAt(pointRow, col) + if (mBelow) return false + } + return true + }, + + getMachineAt(ri, ci) { + if (!this.missionConfig.machines) return null + return this.missionConfig.machines.find(function(m) { return m.row === ri && m.col === ci }) || null + }, + + // 打开点位编辑弹窗(基于点位行号) + openPointEdit(pointRow, col) { + var existing = this.getPointAt(pointRow, col) + if (existing && existing.coords) { + this.pointEditor.x = existing.coords[0] !== undefined ? existing.coords[0] : 0 + this.pointEditor.y = existing.coords[1] !== undefined ? existing.coords[1] : 0 + this.pointEditor.yaw = existing.coords[2] !== undefined ? existing.coords[2] : 0 + } else { + this.pointEditor.x = 0 + this.pointEditor.y = 0 + this.pointEditor.yaw = 0 + } + this.editingPoint = { pointRow: pointRow, col: col } + }, + + // 关闭点位编辑弹窗 + closePointEdit() { + this.editingPoint = null + }, + + // 从AGV读取当前坐标到点位编辑器 + async loadPointFromAgv() { + if (!this.agvConnected) { alert('请先连接AGV'); return } + try { + const res = await fetch(API + '/api/agv/position') + const pos = await res.json() + if (pos.ok && pos.position && Array.isArray(pos.position)) { + this.pointEditor.x = pos.position[0] ?? 0 + this.pointEditor.y = pos.position[1] ?? 0 + this.pointEditor.yaw = pos.position[2] ?? 0 + alert('✅ 已读取AGV位置: (' + this.pointEditor.x.toFixed(2) + ', ' + this.pointEditor.y.toFixed(2) + ', ' + this.pointEditor.yaw.toFixed(2) + ')') + } else if (pos.ok && (!pos.position || !Array.isArray(pos.position))) { + alert('⚠️ AGV 未发布位置数据,请检查 AGV 传感器是否正常') + } else { + alert('读取AGV位置失败: ' + (pos.error || '未知错误')) + } + } catch (e) { alert('读取AGV位置失败: ' + e.message) } + }, + + // 保存点位配置到独立 positions 数组 + async savePoint() { + if (!this.editingPoint) return + var pointRow = this.editingPoint.pointRow + var col = this.editingPoint.col + var coords = [this.pointEditor.x, this.pointEditor.y, this.pointEditor.yaw] + try { + // 保存到后端独立点位表(使用 point_row 标识点位行) + var saveRes = await fetch(API + '/api/mission/positions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + row: pointRow, + col: col, + side: 'shoot', // 统一用 'shoot' 表示拍摄点位 + coords: coords, + poses: [] + }) + }) + var saveData = await saveRes.json() + if (!saveData.ok) { + alert('保存点位失败: ' + (saveData.error || '未知错误')) + return + } + + // 同步更新关联机器的坐标(如果有的话) + this.syncPointToMachines(pointRow, col, coords) + + await this.loadMissionConfig() + alert('✅ 点位坐标已保存: (' + coords[0].toFixed(2) + ', ' + coords[1].toFixed(2) + ', ' + coords[2].toFixed(2) + ')') + this.closePointEdit() + } catch (e) { alert('保存点位失败: ' + e.message) } + }, + + // 将点位坐标同步到关联的机器对象 + syncPointToMachines(pointRow, col, coords) { + var rows = this.missionConfig.rows + // pointRow=0 → 同步到下面机器(0,col)的front + if (pointRow === 0) { + var m0 = this.getMachineAt(0, col) + if (m0) { + this.updateMachineSide(m0, 'front', coords) + } + return + } + // pointRow=rows → 同步到上面机器(rows-1,col)的back + if (pointRow === rows) { + var mLast = this.getMachineAt(rows - 1, col) + if (mLast) { + this.updateMachineSide(mLast, 'back', coords) + } + return + } + // 中间点位行 → 同步到上面机器的back + 下面机器的front + var mAbove = this.getMachineAt(pointRow - 1, col) + var mBelow = this.getMachineAt(pointRow, col) + if (mAbove) { + this.updateMachineSide(mAbove, 'back', coords) + } + if (mBelow) { + this.updateMachineSide(mBelow, 'front', coords) + } + }, + + // 更新机器某侧的坐标 + async updateMachineSide(machine, side, coords) { + try { + var update = {} + update[side] = { + coords: coords, + poses: (machine[side] && machine[side].poses) ? machine[side].poses : [] + } + await fetch(API + '/api/mission/machines/' + machine.id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(update) + }) + } catch (e) { console.error('同步机器坐标失败', e) } + }, + + // 清空点位(带保护检查) + async clearPoint() { + if (!this.editingPoint) return + var pointRow = this.editingPoint.pointRow + var col = this.editingPoint.col + + // 检查是否可以清空 + if (!this.canClearPoint(pointRow, col)) { + var ownerLabel = this.getPointOwnerLabel(pointRow, col) + alert('⚠️ 无法清空!此点位服务于: ' + ownerLabel + '\n必须先移除相关机器才能清空此点位。') + return + } + + if (!confirm('确定清空此点位坐标?')) return + + try { + // 发送清空请求(坐标归零或删除记录) + await fetch(API + '/api/mission/positions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + row: pointRow, + col: col, + side: 'shoot', + coords: [0, 0, 0], + poses: [] + }) + }) + this.pointEditor.x = 0 + this.pointEditor.y = 0 + this.pointEditor.yaw = 0 + await this.loadMissionConfig() + } catch (e) { alert('清空点位失败: ' + e.message) } + }, + + onCellClick(ri, ci) { + var m = this.getMachineAt(ri, ci) + if (!m) { + // 无机器 → 创建机器记录 + this.createMachine(ri, ci).then(function(ok) { + if (ok) { + var created = this.getMachineAt(ri, ci) + if (created) this.selectMachine(created) + } + }.bind(this)) + } else { + // 有机器 → 切换为无机器(删除) + this.deleteMachine(m.id) + } + }, + async createMachine(ri, ci) { + try { + var machineId = 'm_' + ri + '_' + ci + var res = await fetch(API + '/api/mission/machines/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: machineId, + row: ri, + col: ci, + front: { coords: [0, 0, 0], poses: [] }, + back: { coords: [0, 0, 0], poses: [] } + }) + }) + var data = await res.json() + if (!data.ok && data.error !== '该位置已有机器') { + alert('创建机器失败: ' + (data.error || '未知错误')) + return false + } + await this.loadAllMachines() + return true + } catch (e) { alert('创建机器失败: ' + e.message); return false } + }, + selectMachine(machine) { + if (!machine.front) machine.front = { coords: [0, 0, 0], poses: [] } + else if (!Array.isArray(machine.front.coords)) machine.front.coords = [0, 0, 0] + if (!machine.back) machine.back = { coords: [0, 0, 0], poses: [] } + else if (!Array.isArray(machine.back.coords)) machine.back.coords = [0, 0, 0] + this.selectedMachine = machine + }, + clearSelection() { + this.selectedMachine = null + }, + async deleteMachine(machineId) { + if (!confirm('确定删除此机器?\n\n注意:删除后其上方/下方的点位仍可继续使用。')) return + try { + await fetch(API + '/api/mission/machines/' + machineId, { method: 'DELETE' }) + this.selectedMachine = null + await this.loadAllMachines() + await this.loadMissionConfig() // 重新加载点位,确保数据同步 + } catch (e) { alert('删除失败: ' + e.message) } + }, + async saveMachineCoords() { + if (!this.selectedMachine) return + try { + var res = await fetch(API + '/api/mission/machines/' + this.selectedMachine.id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + front: this.selectedMachine.front, + back: this.selectedMachine.back + }) + }) + if (res.ok) { + this.mapMsg = '✅ 机器坐标已保存' + setTimeout(function() { this.mapMsg = '' }.bind(this), 2000) + } else { + alert('保存失败: ' + res.status) + } + } catch (e) { alert('保存失败: ' + e.message) } + }, + async readPosition(side) { + if (!this.agvConnected) { alert('AGV 未连接'); return } + try { + var res = await fetch(API + '/api/agv/position') + var data = await res.json() + if (data.ok && data.position && Array.isArray(data.position)) { + var x = data.position[0] + var y = data.position[1] + var theta = data.position[2] + if (side === 'front') { + this.selectedMachine.front.coords = [x, y, theta] + } else { + this.selectedMachine.back.coords = [x, y, theta] + } + } else if (data.ok && (!data.position || !Array.isArray(data.position))) { + alert('⚠️ AGV 未发布位置数据,请检查 AGV 传感器是否正常') + } else { + alert('读取位置失败: ' + (data.error || '未知错误')) + } + } catch (e) { alert('读取位置失败: ' + e.message) } + }, + async addPoseToMachine(machineId, side) { + var name = this.poseForm.name || '姿态' + (((this.selectedMachine && this.selectedMachine[side] && this.selectedMachine[side].poses) || []).length + 1) + try { + var res = await fetch(API + '/api/mission/poses/' + machineId + '/' + side, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name, + arm_angles: this.currentAngles.length === 6 ? this.currentAngles : [0, 0, 0, 0, 0, 0], + speed: 500, + description: '' + }) + }) + var data = await res.json() + if (data.ok) { + this.poseForm.name = '' + await this.loadAllMachines() + var updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col) + if (updated) this.selectMachine(updated) + } else { + alert('添加姿态失败: ' + (data.error || '未知错误')) + } + } catch (e) { alert('添加姿态失败: ' + e.message) } + }, + async deletePose(machineId, side, poseId) { + if (!confirm('确定删除此姿态?')) return + try { + await fetch(API + '/api/mission/poses/' + machineId + '/' + side + '/' + poseId, { method: 'DELETE' }) + await this.loadAllMachines() + if (this.selectedMachine) { + var updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col) + if (updated) this.selectMachine(updated) + } + } catch (e) { alert('删除姿态失败: ' + e.message) } + }, + async capturePosition(ri, ci, side) { + if (!this.agvConnected) { alert('请先连接AGV'); return } + var machine = this.getMachineAt(ri, ci) + if (!machine) { + try { + var machineId = 'm_' + ri + '_' + ci + var res = await fetch(API + '/api/mission/machines/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: machineId, + row: ri, + col: ci, + front: { coords: [0, 0, 0], poses: [] }, + back: { coords: [0, 0, 0], poses: [] } + }) + }) + if (!res.ok) throw new Error('创建失败') + await this.loadAllMachines() + machine = this.getMachineAt(ri, ci) + } catch (e) { alert('创建机器失败: ' + e.message); return } + } + try { + var res = await fetch(API + '/api/agv/position') + var pos = await res.json() + var x = 0, y = 0, theta = 0 + if (pos.ok && pos.position && Array.isArray(pos.position)) { + x = pos.position[0] || 0 + y = pos.position[1] || 0 + theta = pos.position[2] || 0 + } else if (pos.ok && (!pos.position || !Array.isArray(pos.position))) { + alert('⚠️ AGV 未发布位置数据,请检查 AGV 传感器是否正常') + return + } else { + alert('读取位置失败: ' + (pos.error || '未知错误')) + return + } + if (!machine) { machine = this.getMachineAt(ri, ci) } + if (!machine) { alert('机器记录不存在'); return } + if (side === 'front') { machine.front.coords = [x, y, theta] } else { machine.back.coords = [x, y, theta] } + await fetch(API + '/api/mission/machines/' + machine.id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(machine) + }) + alert((side === 'front' ? '正面' : '背面') + '点位已更新: (' + x.toFixed(2) + ',' + y.toFixed(2) + ',' + theta.toFixed(2) + ')') + } catch (e) { alert('读取位置失败: ' + e.message) } + }, + async refreshSequence() { + try { + var res = await fetch(API + '/api/mission/generate_sequence') + var data = await res.json() + if (data.ok) { + this.sequence = data.sequence || [] + } + } catch (e) { console.error('刷新序列失败', e) } + }, + // === 机械臂 === + async refreshAngles() { + if (!this.armConnected) return + try { + var res = await fetch(API + '/api/arm/get_angles') + var data = await res.json() + if (data.ok && data.angles) { + this.currentAngles = data.angles + this.angleInputs = [...data.angles] + } + } catch (e) {} + }, + async setAngle(idx, val) { + await fetch(API + '/api/arm/set_angle', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ joint: 'J' + (idx + 1), angle: val }) + }) + }, + async applyAngles() { + await fetch(API + '/api/arm/set_angles', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ angles: this.angleInputs, speed: 500 }) + }) + }, + jogStart(idx, dir) { + var joint = 'J' + (idx + 1) + fetch(API + '/api/arm/jog', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ joint, direction: dir }) + }) + this.jogIntervals[idx] = setInterval(function() { this.refreshAngles() }.bind(this), 200) + }, + jogStop(idx) { + clearInterval(this.jogIntervals[idx]) + var joint = 'J' + (idx + 1) + fetch(API + '/api/arm/jog', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ joint, direction: 0 }) + }) + setTimeout(function() { this.refreshAngles() }.bind(this), 300) + }, + onPreviewError(e) { + e.target.style.display = 'none' + }, + // === AGV 控制 === + async refreshAgvPosition() { + if (!this.agvConnected) return + try { + var res = await fetch(API + '/api/agv/position') + var data = await res.json() + if (data.ok) { + this.agvPosition = data.position + this.agvBattery = data.battery + } + } catch (e) {} + }, + agvMoveStart(dir) { + if (!this.agvConnected) return + fetch(API + '/api/agv/move', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ direction: dir, speed: this.agvSpeed }) + }) + }, + agvMoveStop() { + fetch(API + '/api/agv/move', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ direction: 'stop' }) + }) + }, + async agvStop() { + await fetch(API + '/api/agv/stop', { method: 'POST' }) + }, + async agvResetCollision() { + if (!this.agvConnected) { + alert('AGV 未连接') + return + } + if (!confirm('确定执行撞物体后复位?')) return + try { + var res = await fetch(API + '/api/agv/reset', { method: 'POST' }) + var data = await res.json() + if (data.ok) { + alert('✅ ' + data.message) + await this.refresh() + await this.refreshAgvPosition() + } else { + alert('❌ 复位失败: ' + (data.error || '未知错误')) + } + } catch (e) { + alert('❌ 复位请求失败: ' + e.message) + } + }, + } +}) +const vm = app.mount('#app') +window.vm = vm // 暴露组件实例 diff --git a/agv_app/static/js/vue3.global.prod.js b/agv_app/static/js/vue3.global.prod.js new file mode 100644 index 0000000..43f0ad2 --- /dev/null +++ b/agv_app/static/js/vue3.global.prod.js @@ -0,0 +1,13 @@ +/** +* vue v3.5.33 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/var Vue=function(e){"use strict";var t,n,r;let i,l,s,o,a,c,u,h,d,p,f,g,m;function y(e){let t=Object.create(null);for(let n of e.split(","))t[n]=1;return e=>e in t}let b={},_=[],S=()=>{},x=()=>!1,C=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&(e.charCodeAt(2)>122||97>e.charCodeAt(2)),k=e=>e.startsWith("onUpdate:"),T=Object.assign,w=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},N=Object.prototype.hasOwnProperty,A=(e,t)=>N.call(e,t),E=Array.isArray,I=e=>"function"==typeof e,R=e=>"string"==typeof e,O=e=>"symbol"==typeof e,M=e=>null!==e&&"object"==typeof e,P=e=>(M(e)||I(e))&&I(e.then)&&I(e.catch),F=Object.prototype.toString,L=e=>R(e)&&"NaN"!==e&&"-"!==e[0]&&""+parseInt(e,10)===e,$=y(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),D=y("bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"),V=e=>{let t=Object.create(null);return n=>t[n]||(t[n]=e(n))},B=/-\w/g,j=V(e=>e.replace(B,e=>e.slice(1).toUpperCase())),U=/\B([A-Z])/g,H=V(e=>e.replace(U,"-$1").toLowerCase()),q=V(e=>e.charAt(0).toUpperCase()+e.slice(1)),W=V(e=>e?`on${q(e)}`:""),K=(e,t)=>!Object.is(e,t),z=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},G=e=>{let t=parseFloat(e);return isNaN(t)?e:t},X=e=>{let t=R(e)?Number(e):NaN;return isNaN(t)?e:t},Q=()=>i||(i="u">typeof globalThis?globalThis:"u">typeof self?self:"u">typeof window?window:"u">typeof global?global:{}),Z=y("Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol");function Y(e){if(E(e)){let t={};for(let n=0;n{if(e){let n=e.split(et);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function ei(e){let t="";if(R(e))t=e;else if(E(e))for(let n=0;neu(e,t))}let ed=e=>!!(e&&!0===e.__v_isRef),ep=e=>R(e)?e:null==e?"":E(e)||M(e)&&(e.toString===F||!I(e.toString))?ed(e)?ep(e.value):JSON.stringify(e,ef,2):String(e),ef=(e,t)=>{let n;if(ed(t))return ef(e,t.value);if("[object Map]"===(n=t,F.call(n)))return{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[eg(t,r)+" =>"]=n,e),{})};{let e;if("[object Set]"===(e=t,F.call(e)))return{[`Set(${t.size})`]:[...t.values()].map(e=>eg(e))};else{if(O(t))return eg(t);let e;if(M(t)&&!E(t)&&"[object Object]"!==(e=t,F.call(e)))return String(t)}}return t},eg=(e,t="")=>{var n;return O(e)?`Symbol(${null!=(n=e.description)?n:t})`:e};class em{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.__v_skip=!0,this.parent=l,!e&&l&&(this.index=(l.scopes||(l.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){let e,t;if(this._isPaused=!0,this.scopes)for(e=0,t=this.scopes.length;e0&&0==--this._on){if(l===this)l=this.prevScope;else{let e=l;for(;e;){if(e.prevScope===this){e.prevScope=this.prevScope;break}e=e.prevScope}}this.prevScope=void 0}}stop(e){if(this._active){let t,n;for(t=0,this._active=!1,n=this.effects.length;t0)){if(a){let e=a;for(a=void 0;e;){let t=e.next;e.next=void 0,e.flags&=-9,e=t}}for(;o;){let t=o;for(o=void 0;t;){let n=t.next;if(t.next=void 0,t.flags&=-9,1&t.flags)try{t.trigger()}catch(t){e||(e=t)}t=n}}if(e)throw e}}function ex(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function eC(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;-1===r.version?(r===n&&(n=e),ew(r),function(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function ek(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(eT(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function eT(e){if(4&e.flags&&!(16&e.flags)||(e.flags&=-17,e.globalVersion===eO)||(e.globalVersion=eO,!e.isSSR&&128&e.flags&&(!e.deps&&!e._dirty||!ek(e))))return;e.flags|=2;let t=e.dep,n=s,r=eN;s=e,eN=!0;try{ex(e);let n=e.fn(e._value);(0===t.version||K(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(e){throw t.version++,e}finally{s=n,eN=r,eC(e),e.flags&=-3}}function ew(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)ew(e,!0)}t||--n.sc||!n.map||n.map.delete(n.key)}let eN=!0,eA=[];function eE(){eA.push(eN),eN=!1}function eI(){let e=eA.pop();eN=void 0===e||e}function eR(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=s;s=void 0;try{t()}finally{s=e}}}let eO=0;class eM{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class eP{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!s||!eN||s===this.computed)return;let t=this.activeLink;if(void 0===t||t.sub!==s)t=this.activeLink=new eM(s,this),s.deps?(t.prevDep=s.depsTail,s.depsTail.nextDep=t,s.depsTail=t):s.deps=s.depsTail=t,function e(t){if(t.dep.sc++,4&t.sub.flags){let n=t.dep.computed;if(n&&!t.dep.subs){n.flags|=20;for(let t=n.deps;t;t=t.nextDep)e(t)}let r=t.dep.subs;r!==t&&(t.prevSub=r,r&&(r.nextSub=t)),t.dep.subs=t}}(t);else if(-1===t.version&&(t.version=this.version,t.nextDep)){let e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=s.depsTail,t.nextDep=void 0,s.depsTail.nextDep=t,s.depsTail=t,s.deps===t&&(s.deps=e)}return t}trigger(e){this.version++,eO++,this.notify(e)}notify(e){eb++;try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{eS()}}}let eF=new WeakMap,eL=Symbol(""),e$=Symbol(""),eD=Symbol("");function eV(e,t,n){if(eN&&s){let t=eF.get(e);t||eF.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new eP),r.map=t,r.key=n),r.track()}}function eB(e,t,n,r,i,l){let s=eF.get(e);if(!s)return void eO++;let o=e=>{e&&e.trigger()};if(eb++,"clear"===t)s.forEach(o);else{let i=E(e),l=i&&L(n);if(i&&"length"===n){let e=Number(r);s.forEach((t,n)=>{("length"===n||n===eD||!O(n)&&n>=e)&&o(t)})}else switch((void 0!==n||s.has(void 0))&&o(s.get(n)),l&&o(s.get(eD)),t){case"add":if(i)l&&o(s.get("length"));else{let t;o(s.get(eL));"[object Map]"===(t=e,F.call(t))&&o(s.get(e$))}break;case"delete":if(!i){let t;o(s.get(eL));"[object Map]"===(t=e,F.call(t))&&o(s.get(e$))}break;case"set":let a;"[object Map]"===(a=e,F.call(a))&&o(s.get(eL))}}eS()}function ej(e){let t=tm(e);return t===e?t:(eV(t,"iterate",eD),tf(e)?t:t.map(ty))}function eU(e){return eV(e=tm(e),"iterate",eD),e}function eH(e,t){return tp(e)?td(e)?tb(ty(t)):tb(t):ty(t)}let eq={__proto__:null,[Symbol.iterator](){return eW(this,Symbol.iterator,e=>eH(this,e))},concat(...e){return ej(this).concat(...e.map(e=>E(e)?ej(e):e))},entries(){return eW(this,"entries",e=>(e[1]=eH(this,e[1]),e))},every(e,t){return ez(this,"every",e,t,void 0,arguments)},filter(e,t){return ez(this,"filter",e,t,e=>e.map(e=>eH(this,e)),arguments)},find(e,t){return ez(this,"find",e,t,e=>eH(this,e),arguments)},findIndex(e,t){return ez(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return ez(this,"findLast",e,t,e=>eH(this,e),arguments)},findLastIndex(e,t){return ez(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return ez(this,"forEach",e,t,void 0,arguments)},includes(...e){return eG(this,"includes",e)},indexOf(...e){return eG(this,"indexOf",e)},join(e){return ej(this).join(e)},lastIndexOf(...e){return eG(this,"lastIndexOf",e)},map(e,t){return ez(this,"map",e,t,void 0,arguments)},pop(){return eX(this,"pop")},push(...e){return eX(this,"push",e)},reduce(e,...t){return eJ(this,"reduce",e,t)},reduceRight(e,...t){return eJ(this,"reduceRight",e,t)},shift(){return eX(this,"shift")},some(e,t){return ez(this,"some",e,t,void 0,arguments)},splice(...e){return eX(this,"splice",e)},toReversed(){return ej(this).toReversed()},toSorted(e){return ej(this).toSorted(e)},toSpliced(...e){return ej(this).toSpliced(...e)},unshift(...e){return eX(this,"unshift",e)},values(){return eW(this,"values",e=>eH(this,e))}};function eW(e,t,n){let r=eU(e),i=r[t]();return r===e||tf(e)||(i._next=i.next,i.next=()=>{let e=i._next();return e.done||(e.value=n(e.value)),e}),i}let eK=Array.prototype;function ez(e,t,n,r,i,l){let s=eU(e),o=s!==e&&!tf(e),a=s[t];if(a!==eK[t]){let t=a.apply(e,l);return o?ty(t):t}let c=n;s!==e&&(o?c=function(t,r){return n.call(this,eH(e,t),r,e)}:n.length>2&&(c=function(t,r){return n.call(this,t,r,e)}));let u=a.call(s,c,r);return o&&i?i(u):u}function eJ(e,t,n,r){let i=eU(e),l=i!==e&&!tf(e),s=n,o=!1;i!==e&&(l?(o=0===r.length,s=function(t,r,i){return o&&(o=!1,t=eH(e,t)),n.call(this,t,eH(e,r),i,e)}):n.length>3&&(s=function(t,r,i){return n.call(this,t,r,i,e)}));let a=i[t](s,...r);return o?eH(e,a):a}function eG(e,t,n){let r=tm(e);eV(r,"iterate",eD);let i=r[t](...n);return(-1===i||!1===i)&&tg(n[0])?(n[0]=tm(n[0]),r[t](...n)):i}function eX(e,t,n=[]){eE(),eb++;let r=tm(e)[t].apply(e,n);return eS(),eI(),r}let eQ=y("__proto__,__v_isRef,__isVue"),eZ=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>"arguments"!==e&&"caller"!==e).map(e=>Symbol[e]).filter(O));function eY(e){O(e)||(e=String(e));let t=tm(this);return eV(t,"has",e),t.hasOwnProperty(e)}class e0{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if("__v_skip"===t)return e.__v_skip;let r=this._isReadonly,i=this._isShallow;if("__v_isReactive"===t)return!r;if("__v_isReadonly"===t)return r;if("__v_isShallow"===t)return i;if("__v_raw"===t)return n===(r?i?to:ts:i?tl:ti).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;let l=E(e);if(!r){let e;if(l&&(e=eq[t]))return e;if("hasOwnProperty"===t)return eY}let s=Reflect.get(e,t,t_(e)?e:n);if((O(t)?eZ.has(t):eQ(t))||(r||eV(e,"get",t),i))return s;if(t_(s)){let e=l&&L(t)?s:s.value;return r&&M(e)?tu(e):e}return M(s)?r?tu(s):ta(s):s}}class e1 extends e0{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t],l=E(e)&&L(t);if(!this._isShallow){let e=tp(i);if(tf(n)||tp(n)||(i=tm(i),n=tm(n)),!l&&t_(i)&&!t_(n))if(e)return!0;else return i.value=n,!0}let s=l?Number(t)e;function e9(e){return function(){return"delete"!==e&&("clear"===e?void 0:this)}}function e7(e,t){let n,r=(T(n={get(n){let r=this.__v_raw,i=tm(r),l=tm(n);e||(K(n,l)&&eV(i,"get",n),eV(i,"get",l));let{has:s}=Reflect.getPrototypeOf(i),o=t?e5:e?tb:ty;return s.call(i,n)?o(r.get(n)):s.call(i,l)?o(r.get(l)):void(r!==i&&r.get(n))},get size(){let t=this.__v_raw;return e||eV(tm(t),"iterate",eL),t.size},has(t){let n=this.__v_raw,r=tm(n),i=tm(t);return e||(K(t,i)&&eV(r,"has",t),eV(r,"has",i)),t===i?n.has(t):n.has(t)||n.has(i)},forEach(n,r){let i=this,l=i.__v_raw,s=tm(l),o=t?e5:e?tb:ty;return e||eV(s,"iterate",eL),l.forEach((e,t)=>n.call(r,o(e),o(t),i))}},e?{add:e9("add"),set:e9("set"),delete:e9("delete"),clear:e9("clear")}:{add(e){let n=tm(this),r=Reflect.getPrototypeOf(n),i=tm(e),l=t||tf(e)||tp(e)?e:i;return r.has.call(n,l)||K(e,l)&&r.has.call(n,e)||K(i,l)&&r.has.call(n,i)||(n.add(l),eB(n,"add",l,l)),this},set(e,n){t||tf(n)||tp(n)||(n=tm(n));let r=tm(this),{has:i,get:l}=Reflect.getPrototypeOf(r),s=i.call(r,e);s||(e=tm(e),s=i.call(r,e));let o=l.call(r,e);return r.set(e,n),s?K(n,o)&&eB(r,"set",e,n):eB(r,"add",e,n),this},delete(e){let t=tm(this),{has:n,get:r}=Reflect.getPrototypeOf(t),i=n.call(t,e);i||(e=tm(e),i=n.call(t,e)),r&&r.call(t,e);let l=t.delete(e);return i&&eB(t,"delete",e,void 0),l},clear(){let e=tm(this),t=0!==e.size,n=e.clear();return t&&eB(e,"clear",void 0,void 0),n}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=function(...n){let i,l=this.__v_raw,s=tm(l),o="[object Map]"===(i=s,F.call(i)),a="entries"===r||r===Symbol.iterator&&o,c=l[r](...n),u=t?e5:e?tb:ty;return e||eV(s,"iterate","keys"===r&&o?e$:eL),T(Object.create(c),{next(){let{value:e,done:t}=c.next();return t?{value:e,done:t}:{value:a?[u(e[0]),u(e[1])]:u(e),done:t}}})}}),n);return(t,n,i)=>"__v_isReactive"===n?!e:"__v_isReadonly"===n?e:"__v_raw"===n?t:Reflect.get(A(r,n)&&n in t?r:t,n,i)}let te={get:e7(!1,!1)},tt={get:e7(!1,!0)},tn={get:e7(!0,!1)},tr={get:e7(!0,!0)},ti=new WeakMap,tl=new WeakMap,ts=new WeakMap,to=new WeakMap;function ta(e){return tp(e)?e:th(e,!1,e3,te,ti)}function tc(e){return th(e,!1,e4,tt,tl)}function tu(e){return th(e,!0,e6,tn,ts)}function th(e,t,n,r,i){var l;let s;if(!M(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;let o=(l=e).__v_skip||!Object.isExtensible(l)?0:function(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}((s=l,F.call(s)).slice(8,-1));if(0===o)return e;let a=i.get(e);if(a)return a;let c=new Proxy(e,2===o?r:n);return i.set(e,c),c}function td(e){return tp(e)?td(e.__v_raw):!!(e&&e.__v_isReactive)}function tp(e){return!!(e&&e.__v_isReadonly)}function tf(e){return!!(e&&e.__v_isShallow)}function tg(e){return!!e&&!!e.__v_raw}function tm(e){let t=e&&e.__v_raw;return t?tm(t):e}function tv(e){return!A(e,"__v_skip")&&Object.isExtensible(e)&&J(e,"__v_skip",!0),e}let ty=e=>M(e)?ta(e):e,tb=e=>M(e)?tu(e):e;function t_(e){return!!e&&!0===e.__v_isRef}function tS(e){return tC(e,!1)}function tx(e){return tC(e,!0)}function tC(e,t){return t_(e)?e:new tk(e,t)}class tk{constructor(e,t){this.dep=new eP,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:tm(e),this._value=t?e:ty(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){let t=this._rawValue,n=this.__v_isShallow||tf(e)||tp(e);K(e=n?e:tm(e),t)&&(this._rawValue=e,this._value=n?e:ty(e),this.dep.trigger())}}function tT(e){return t_(e)?e.value:e}let tw={get:(e,t,n)=>"__v_raw"===t?e:tT(Reflect.get(e,t,n)),set:(e,t,n,r)=>{let i=e[t];return t_(i)&&!t_(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function tN(e){return td(e)?e:new Proxy(e,tw)}class tA{constructor(e){this.__v_isRef=!0,this._value=void 0;const t=this.dep=new eP,{get:n,set:r}=e(t.track.bind(t),t.trigger.bind(t));this._get=n,this._set=r}get value(){return this._value=this._get()}set value(e){this._set(e)}}function tE(e){return new tA(e)}class tI{constructor(e,t,n){this._object=e,this._defaultValue=n,this.__v_isRef=!0,this._value=void 0,this._key=O(t)?t:String(t),this._raw=tm(e);let r=!0,i=e;if(!E(e)||O(this._key)||!L(this._key))do r=!tg(i)||tf(i);while(r&&(i=i.__v_raw));this._shallow=r}get value(){let e=this._object[this._key];return this._shallow&&(e=tT(e)),this._value=void 0===e?this._defaultValue:e}set value(e){if(this._shallow&&t_(this._raw[this._key])){let t=this._object[this._key];if(t_(t)){t.value=e;return}}this._object[this._key]=e}get dep(){var e,t;let n;return e=this._raw,t=this._key,(n=eF.get(e))&&n.get(t)}}class tR{constructor(e){this._getter=e,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}class tO{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new eP(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=eO-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(8&this.flags)&&s!==this)return e_(this,!0),!0}get value(){let e=this.dep.track();return eT(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}}let tM={},tP=new WeakMap;function tF(e,t=!1,n=g){if(n){let t=tP.get(n);t||tP.set(n,t=[]),t.push(e)}}function tL(e,t=1/0,n){if(t<=0||!M(e)||e.__v_skip||((n=n||new Map).get(e)||0)>=t)return e;if(n.set(e,t),t--,t_(e))tL(e.value,t,n);else if(E(e))for(let r=0;r{tL(e,t,n)});else{let r;if("[object Object]"===(r=e,F.call(r))){for(let r in e)tL(e[r],t,n);for(let r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&tL(e[r],t,n)}}}return e}function t$(e,t,n,r){try{return r?e(...r):e()}catch(e){tV(e,t,n)}}function tD(e,t,n,r){if(I(e)){let i=t$(e,t,n,r);return i&&P(i)&&i.catch(e=>{tV(e,t,n)}),i}if(E(e)){let i=[];for(let l=0;l=tY(n)?tB.push(e):tB.splice(function(e){let t=tj+1,n=tB.length;for(;t>>1,i=tB[r],l=tY(i);ltY(e)-tY(t));if(tU.length=0,tH)return void tH.push(...e);for(tq=0,tH=e;tqnull==e.id?2&e.flags?-1:1/0:e.id,t0=null,t1=null;function t2(e){let t=t0;return t0=e,t1=e&&e.type.__scopeId||null,t}function t3(e,t=t0,n){if(!t||e._n)return e;let r=(...n)=>{let i;r._d&&il(-1);let l=t2(t);try{i=e(...n)}finally{t2(l),r._d&&il(1)}return i};return r._n=!0,r._c=!0,r._d=!0,r}function t6(e,t,n,r){let i=e.dirs,l=t&&t.dirs;for(let s=0;s1)return n&&I(t)?t.call(r&&r.proxy):t}}let t5=Symbol.for("v-scx");function t9(e,t){return t7(e,null,{flush:"sync"})}function t7(e,t,n=b){let{flush:r}=n,i=T({},n),s=iw;i.call=(e,t,n)=>tD(e,s,t,n);let o=!1;return"post"===r?i.scheduler=e=>{rq(e,s&&s.suspense)}:"sync"!==r&&(o=!0,i.scheduler=(e,t)=>{t?e():tJ(e)}),i.augmentJob=e=>{t&&(e.flags|=4),o&&(e.flags|=2,s&&(e.id=s.uid,e.i=s))},function(e,t,n=b){let r,i,s,o,{immediate:a,deep:c,once:u,scheduler:h,augmentJob:d,call:p}=n,f=e=>c?e:tf(e)||!1===c||0===c?tL(e,1):tL(e),m=!1,y=!1;if(t_(e)?(i=()=>e.value,m=tf(e)):td(e)?(i=()=>f(e),m=!0):E(e)?(y=!0,m=e.some(e=>td(e)||tf(e)),i=()=>e.map(e=>t_(e)?e.value:td(e)?f(e):I(e)?p?p(e,2):e():void 0)):i=I(e)?t?p?()=>p(e,2):e:()=>{if(s){eE();try{s()}finally{eI()}}let t=g;g=r;try{return p?p(e,3,[o]):e(o)}finally{g=t}}:S,t&&c){let e=i,t=!0===c?1/0:c;i=()=>tL(e(),t)}let _=l,x=()=>{r.stop(),_&&_.active&&w(_.effects,r)};if(u&&t){let e=t;t=(...t)=>{e(...t),x()}}let C=y?Array(e.length).fill(tM):tM,k=e=>{if(1&r.flags&&(r.dirty||e))if(t){let e=r.run();if(c||m||(y?e.some((e,t)=>K(e,C[t])):K(e,C))){s&&s();let n=g;g=r;try{let n=[e,C===tM?void 0:y&&C[0]===tM?[]:C,o];C=e,p?p(t,3,n):t(...n)}finally{g=n}}}else r.run()};return d&&d(k),(r=new ey(i)).scheduler=h?()=>h(k,!1):k,o=e=>tF(e,!1,r),s=r.onStop=()=>{let e=tP.get(r);if(e){if(p)p(e,4);else for(let t of e)t();tP.delete(r)}},t?a?k(!0):C=r.run():h?h(k.bind(null,!0),!0):r.run(),x.pause=r.pause.bind(r),x.resume=r.resume.bind(r),x.stop=x,x}(e,t,i)}function ne(e,t,n){let r,i=this.proxy,l=R(e)?e.includes(".")?nt(i,e):()=>i[e]:e.bind(i,i);I(t)?r=t:(r=t.handler,n=t);let s=iA(this),o=t7(l,r.bind(i),n);return s(),o}function nt(e,t){let n=t.split(".");return()=>{let t=e;for(let e=0;ee&&(e.disabled||""===e.disabled),nl=e=>"u">typeof SVGElement&&e instanceof SVGElement,ns=e=>"function"==typeof MathMLElement&&e instanceof MathMLElement,no=(e,t)=>{let n=e&&e.to;return R(n)?t?t(n):null:n};function na(e,t,n,{o:{insert:r},m:i},l=2){0===l&&r(e.targetAnchor,t,n);let{el:s,anchor:o,shapeFlag:a,children:c,props:u}=e,h=2===l;if(h&&r(s,t,n),!nn.has(e)&&(!h||ni(u))&&16&a)for(let e=0;e{e.isMounted=!0}),n2(()=>{e.isUnmounting=!0}),e}let nf=[Function,Array],ng={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:nf,onEnter:nf,onAfterEnter:nf,onEnterCancelled:nf,onBeforeLeave:nf,onLeave:nf,onAfterLeave:nf,onLeaveCancelled:nf,onBeforeAppear:nf,onAppear:nf,onAfterAppear:nf,onAppearCancelled:nf},nm=e=>{let t=e.subTree;return t.component?nm(t.component):t};function nv(e){let t=e[0];if(e.length>1){for(let n of e)if(n.type!==r5){t=n;break}}return t}let ny={name:"BaseTransition",props:ng,setup(e,{slots:t}){let n=iN(),r=np();return()=>{let i=t.default&&nk(t.default(),!0),l=i&&i.length?nv(i):n.subTree?iy():void 0;if(!l)return;let s=tm(e),{mode:o}=s;if(r.isLeaving)return nS(l);let a=nx(l);if(!a)return nS(l);let c=n_(a,s,r,n,e=>c=e);a.type!==r5&&nC(a,c);let u=n.subTree&&nx(n.subTree);if(u&&u.type!==r5&&!ic(u,a)&&nm(n).type!==r5){let e=n_(u,s,r,n);if(nC(u,e),"out-in"===o&&a.type!==r5)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,8&n.job.flags||n.update(),delete e.afterLeave,u=void 0},nS(l);"in-out"===o&&a.type!==r5?e.delayLeave=(e,t,n)=>{nb(r,u)[String(u.key)]=u,e[nh]=()=>{t(),e[nh]=void 0,delete c.delayedLeave,u=void 0},c.delayedLeave=()=>{n(),delete c.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return l}}};function nb(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function n_(e,t,n,r,i){let{appear:l,mode:s,persisted:o=!1,onBeforeEnter:a,onEnter:c,onAfterEnter:u,onEnterCancelled:h,onBeforeLeave:d,onLeave:p,onAfterLeave:f,onLeaveCancelled:g,onBeforeAppear:m,onAppear:y,onAfterAppear:b,onAppearCancelled:_}=t,S=String(e.key),x=nb(n,e),C=(e,t)=>{e&&tD(e,r,9,t)},k=(e,t)=>{let n=t[1];C(e,t),E(e)?e.every(e=>e.length<=1)&&n():e.length<=1&&n()},T={mode:s,persisted:o,beforeEnter(t){let r=a;if(!n.isMounted)if(!l)return;else r=m||a;t[nh]&&t[nh](!0);let i=x[S];i&&ic(e,i)&&i.el[nh]&&i.el[nh](),C(r,[t])},enter(t){if(x[S]===e)return;let r=c,i=u,s=h;if(!n.isMounted)if(!l)return;else r=y||c,i=b||u,s=_||h;let o=!1;t[nd]=e=>{o||(o=!0,e?C(s,[t]):C(i,[t]),T.delayedLeave&&T.delayedLeave(),t[nd]=void 0)};let a=t[nd].bind(null,!1);r?k(r,[t,a]):a()},leave(t,r){let i=String(e.key);if(t[nd]&&t[nd](!0),n.isUnmounting)return r();C(d,[t]);let l=!1;t[nh]=n=>{l||(l=!0,r(),n?C(g,[t]):C(f,[t]),t[nh]=void 0,x[i]===e&&delete x[i])};let s=t[nh].bind(null,!1);x[i]=e,p?k(p,[t,s]):s()},clone(e){let l=n_(e,t,n,r,i);return i&&i(l),l}};return T}function nS(e){if(nH(e))return(e=im(e)).children=null,e}function nx(e){if(!nH(e))return e.type.__isTeleport&&e.children?nv(e.children):e;if(e.component)return e.component.subTree;let{shapeFlag:t,children:n}=e;if(n){if(16&t)return n[0];if(32&t&&I(n.default))return n.default()}}function nC(e,t){6&e.shapeFlag&&e.component?(e.transition=t,nC(e.component.subTree,t)):128&e.shapeFlag?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function nk(e,t=!1,n){let r=[],i=0;for(let l=0;l1)for(let e=0;enE(e,t&&(E(t)?t[l]:t),n,r,i));if(nj(r)&&!i){512&r.shapeFlag&&r.type.__asyncResolved&&r.component.subTree.component&&nE(e,t,n,r.component.subTree);return}let l=4&r.shapeFlag?i$(r.component):r.el,s=i?null:l,{i:o,r:a}=e,c=t&&t.r,u=o.refs===b?o.refs={}:o.refs,h=o.setupState,d=tm(h),p=h===b?x:e=>!nN(u,e)&&A(d,e),f=(e,t)=>!(t&&nN(u,t));if(null!=c&&c!==a&&(nI(t),R(c)?(u[c]=null,p(c)&&(h[c]=null)):t_(c)&&(f(c,t.k)&&(c.value=null),t.k&&(u[t.k]=null))),I(a))t$(a,o,12,[s,u]);else{let t=R(a),r=t_(a);if(t||r){let o=()=>{if(e.f){let n=t?p(a)?h[a]:u[a]:f()||!e.k?a.value:u[e.k];if(i)E(n)&&w(n,l);else if(E(n))n.includes(l)||n.push(l);else if(t)u[a]=[l],p(a)&&(h[a]=u[a]);else{let t=[l];f(a,e.k)&&(a.value=t),e.k&&(u[e.k]=t)}}else t?(u[a]=s,p(a)&&(h[a]=s)):r&&(f(a,e.k)&&(a.value=s),e.k&&(u[e.k]=s))};if(s){let t=()=>{o(),nA.delete(e)};t.id=-1,nA.set(e,t),rq(t,n)}else nI(e),o()}}}function nI(e){let t=nA.get(e);t&&(t.flags|=8,nA.delete(e))}let nR=!1,nO=()=>{nR||(console.error("Hydration completed but contains mismatches."),nR=!0)},nM=e=>{if(1===e.nodeType){if(e.namespaceURI.includes("svg")&&"foreignObject"!==e.tagName)return"svg";if(e.namespaceURI.includes("MathML"))return"mathml"}},nP=e=>8===e.nodeType;function nF(e){let{mt:t,p:n,o:{patchProp:r,createText:i,nextSibling:l,parentNode:s,remove:o,insert:a,createComment:c}}=e,u=(n,r,o,c,b,_=!1)=>{_=_||!!r.dynamicChildren;let S=nP(n)&&"["===n.data,x=()=>f(n,r,o,c,b,S),{type:C,ref:k,shapeFlag:T,patchFlag:w}=r,N=n.nodeType;r.el=n,-2===w&&(_=!1,r.dynamicChildren=null);let A=null;switch(C){case r8:3!==N?""===r.children?(a(r.el=i(""),s(n),n),A=n):A=x():(n.data!==r.children&&(nO(),n.data=r.children),A=l(n));break;case r5:y(n)?(A=l(n),m(r.el=n.content.firstChild,n,o)):A=8!==N||S?x():l(n);break;case r9:if(S&&(N=(n=l(n)).nodeType),1===N||3===N){A=n;let e=!r.children.length;for(let t=0;t{s=s||!!t.dynamicChildren;let{type:a,props:c,patchFlag:u,shapeFlag:h,dirs:p,transition:f}=t,g="input"===a||"option"===a;if(g||-1!==u){let a;p&&t6(t,null,n,"created");let b=!1;if(y(e)){b=rG(null,f)&&n&&n.vnode.props&&n.vnode.props.appear;let r=e.content.firstChild;if(b){let e=r.getAttribute("class");e&&(r.$cls=e),f.beforeEnter(r)}m(r,e,n),t.el=e=r}if(16&h&&!(c&&(c.innerHTML||c.textContent))){let r=d(e.firstChild,t,e,n,i,l,s);for(;r;){nD(e,1)||nO();let t=r;r=r.nextSibling,o(t)}}else if(8&h){let n=t.children;` +`===n[0]&&("PRE"===e.tagName||"TEXTAREA"===e.tagName)&&(n=n.slice(1));let{textContent:r}=e;r!==n&&r!==n.replace(/\r\n|\r/g,` +`)&&(nD(e,0)||nO(),e.textContent=t.children)}if(c){if(g||!s||48&u){let t=e.tagName.includes("-");for(let i in c)(g&&(i.endsWith("value")||"indeterminate"===i)||C(i)&&!$(i)||"."===i[0]||t&&!$(i))&&r(e,i,null,c[i],void 0,n)}else if(c.onClick)r(e,"onClick",null,c.onClick,void 0,n);else if(4&u&&td(c.style))for(let e in c.style)c.style[e]}(a=c&&c.onVnodeBeforeMount)&&iC(a,n,t),p&&t6(t,null,n,"beforeMount"),((a=c&&c.onVnodeMounted)||p||b)&&r3(()=>{a&&iC(a,n,t),b&&f.enter(e),p&&t6(t,null,n,"mounted")},i)}return e.nextSibling},d=(e,t,r,s,o,c,h)=>{h=h||!!t.dynamicChildren;let d=t.children,p=d.length;for(let t=0;t{let{slotScopeIds:u}=t;u&&(i=i?i.concat(u):u);let h=s(e),p=d(l(e),t,h,n,r,i,o);return p&&nP(p)&&"]"===p.data?l(t.anchor=p):(nO(),a(t.anchor=c("]"),h,p),p)},f=(e,t,r,i,a,c)=>{if(nD(e.parentElement,1)||nO(),t.el=null,c){let t=g(e);for(;;){let n=l(e);if(n&&n!==t)o(n);else break}}let u=l(e),h=s(e);return o(e),n(null,t,h,u,r,i,nM(h),a),r&&(r.vnode.el=t.el,rR(r,t.el)),u},g=(e,t="[",n="]")=>{let r=0;for(;e;)if((e=l(e))&&nP(e)&&(e.data===t&&r++,e.data===n))if(0===r)return l(e);else r--;return e},m=(e,t,n)=>{let r=t.parentNode;r&&r.replaceChild(e,t);let i=n;for(;i;)i.vnode.el===t&&(i.vnode.el=i.subTree.el=e),i=i.parent},y=e=>1===e.nodeType&&"TEMPLATE"===e.tagName;return[(e,t)=>{if(!t.hasChildNodes()){n(null,e,t),tZ(),t._vnode=e;return}u(t.firstChild,e,null,null,null),tZ(),t._vnode=e},u]}let nL="data-allow-mismatch",n$={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function nD(e,t){if(0===t||1===t)for(;e&&!e.hasAttribute(nL);)e=e.parentElement;let n=e&&e.getAttribute(nL);if(null==n)return!1;{if(""===n)return!0;let e=n.split(",");return!!(0===t&&e.includes("children"))||e.includes(n$[t])}}let nV=Q().requestIdleCallback||(e=>setTimeout(e,1)),nB=Q().cancelIdleCallback||(e=>clearTimeout(e)),nj=e=>!!e.type.__asyncLoader;function nU(e,t){let{ref:n,props:r,children:i,ce:l}=t.vnode,s=ip(e,r,i);return s.ref=n,s.ce=l,delete t.vnode.ce,s}let nH=e=>e.type.__isKeepAlive;function nq(e,t){let n;if(E(e))return e.some(e=>nq(e,t));if(R(e))return e.split(",").includes(t);return"[object RegExp]"===(n=e,F.call(n))&&(e.lastIndex=0,e.test(t))}function nW(e,t){nz(e,"a",t)}function nK(e,t){nz(e,"da",t)}function nz(e,t,n=iw){let r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(nX(t,r,n),n){let e=n.parent;for(;e&&e.parent;)nH(e.parent.vnode)&&function(e,t,n,r){let i=nX(t,e,r,!0);n3(()=>{w(r[t],i)},n)}(r,t,n,e),e=e.parent}}function nJ(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function nG(e){return 128&e.shapeFlag?e.ssContent:e}function nX(e,t,n=iw,r=!1){if(n){let i=n[e]||(n[e]=[]),l=t.__weh||(t.__weh=(...r)=>{eE();let i=iA(n),l=tD(t,n,e,r);return i(),eI(),l});return r?i.unshift(l):i.push(l),l}}let nQ=e=>(t,n=iw)=>{iR&&"sp"!==e||nX(e,(...e)=>t(...e),n)},nZ=nQ("bm"),nY=nQ("m"),n0=nQ("bu"),n1=nQ("u"),n2=nQ("bum"),n3=nQ("um"),n6=nQ("sp"),n4=nQ("rtg"),n8=nQ("rtc");function n5(e,t=iw){nX("ec",e,t)}let n9="components",n7=Symbol.for("v-ndc");function re(e,t,n=!0,r=!1){let i=t0||iw;if(i){let n=i.type;if(e===n9){let e=iD(n,!1);if(e&&(e===t||e===j(t)||e===q(j(t))))return n}let l=rt(i[e]||n[e],t)||rt(i.appContext[e],t);return!l&&r?n:l}}function rt(e,t){return e&&(e[t]||e[j(t)]||e[q(j(t))])}let rn=e=>e?iI(e)?i$(e):rn(e.parent):null,rr=T(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>rn(e.parent),$root:e=>rn(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>rh(e),$forceUpdate:e=>e.f||(e.f=()=>{tJ(e.update)}),$nextTick:e=>e.n||(e.n=tz.bind(e.proxy)),$watch:e=>ne.bind(e)}),ri=(e,t)=>e!==b&&!e.__isScriptSetup&&A(e,t),rl={get({_:e},t){let n,r;if("__v_skip"===t)return!0;let{ctx:i,setupState:l,data:s,props:o,accessCache:a,type:c,appContext:u}=e;if("$"!==t[0]){let e=a[t];if(void 0!==e)switch(e){case 1:return l[t];case 2:return s[t];case 4:return i[t];case 3:return o[t]}else{if(ri(l,t))return a[t]=1,l[t];if(s!==b&&A(s,t))return a[t]=2,s[t];if(A(o,t))return a[t]=3,o[t];if(i!==b&&A(i,t))return a[t]=4,i[t];rc&&(a[t]=0)}}let h=rr[t];return h?("$attrs"===t&&eV(e.attrs,"get",""),h(e)):(n=c.__cssModules)&&(n=n[t])?n:i!==b&&A(i,t)?(a[t]=4,i[t]):A(r=u.config.globalProperties,t)?r[t]:void 0},set({_:e},t,n){let{data:r,setupState:i,ctx:l}=e;return ri(i,t)?(i[t]=n,!0):r!==b&&A(r,t)?(r[t]=n,!0):!A(e.props,t)&&!("$"===t[0]&&t.slice(1)in e)&&(l[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:i,props:l,type:s}},o){let a;return!!(n[o]||e!==b&&"$"!==o[0]&&A(e,o)||ri(t,o)||A(l,o)||A(r,o)||A(rr,o)||A(i.config.globalProperties,o)||(a=s.__cssModules)&&a[o])},defineProperty(e,t,n){return null!=n.get?e._.accessCache[t]=0:A(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}},rs=T({},rl,{get(e,t){if(t!==Symbol.unscopables)return rl.get(e,t,e)},has:(e,t)=>"_"!==t[0]&&!Z(t)});function ro(e){let t=iN();return t.setupContext||(t.setupContext=iL(t))}function ra(e){return E(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}let rc=!0;function ru(e,t,n){tD(E(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}function rh(e){let t,n=e.type,{mixins:r,extends:i}=n,{mixins:l,optionsCache:s,config:{optionMergeStrategies:o}}=e.appContext,a=s.get(n);return a?t=a:l.length||r||i?(t={},l.length&&l.forEach(e=>rd(t,e,o,!0)),rd(t,n,o)):t=n,M(n)&&s.set(n,t),t}function rd(e,t,n,r=!1){let{mixins:i,extends:l}=t;for(let s in l&&rd(e,l,n,!0),i&&i.forEach(t=>rd(e,t,n,!0)),t)if(r&&"expose"===s);else{let r=rp[s]||n&&n[s];e[s]=r?r(e[s],t[s]):t[s]}return e}let rp={data:rf,props:ry,emits:ry,methods:rv,computed:rv,beforeCreate:rm,created:rm,beforeMount:rm,mounted:rm,beforeUpdate:rm,updated:rm,beforeDestroy:rm,beforeUnmount:rm,destroyed:rm,unmounted:rm,activated:rm,deactivated:rm,errorCaptured:rm,serverPrefetch:rm,components:rv,directives:rv,watch:function(e,t){if(!e)return t;if(!t)return e;let n=T(Object.create(null),e);for(let r in t)n[r]=rm(e[r],t[r]);return n},provide:rf,inject:function(e,t){return rv(rg(e),rg(t))}};function rf(e,t){return t?e?function(){return T(I(e)?e.call(this,this):e,I(t)?t.call(this,this):t)}:t:e}function rg(e){if(E(e)){let t={};for(let n=0;n"modelValue"===t||"model-value"===t?e.modelModifiers:e[`${t}Modifiers`]||e[`${j(t)}Modifiers`]||e[`${H(t)}Modifiers`];function rC(e,t,...n){let r;if(e.isUnmounted)return;let i=e.vnode.props||b,l=n,s=t.startsWith("update:"),o=s&&rx(i,t.slice(7));o&&(o.trim&&(l=n.map(e=>R(e)?e.trim():e)),o.number&&(l=n.map(G)));let a=i[r=W(t)]||i[r=W(j(t))];!a&&s&&(a=i[r=W(H(t))]),a&&tD(a,e,6,l);let c=i[r+"Once"];if(c){if(e.emitted){if(e.emitted[r])return}else e.emitted={};e.emitted[r]=!0,tD(c,e,6,l)}}let rk=new WeakMap;function rT(e,t){return!!e&&!!C(t)&&(A(e,(t=t.slice(2).replace(/Once$/,""))[0].toLowerCase()+t.slice(1))||A(e,H(t))||A(e,t))}function rw(e){let t,n,{type:r,vnode:i,proxy:l,withProxy:s,propsOptions:[o],slots:a,attrs:c,emit:u,render:h,renderCache:d,props:p,data:f,setupState:g,ctx:m,inheritAttrs:y}=e,b=t2(e);try{if(4&i.shapeFlag){let e=s||l;t=ib(h.call(e,e,d,p,g,f,m)),n=c}else t=ib(r.length>1?r(p,{attrs:c,slots:a,emit:u}):r(p,null)),n=r.props?c:rN(c)}catch(n){r7.length=0,tV(n,e,1),t=ip(r5)}let _=t;if(n&&!1!==y){let e=Object.keys(n),{shapeFlag:t}=_;e.length&&7&t&&(o&&e.some(k)&&(n=rA(n,o)),_=im(_,n,!1,!0))}return i.dirs&&((_=im(_,null,!1,!0)).dirs=_.dirs?_.dirs.concat(i.dirs):i.dirs),i.transition&&nC(_,i.transition),t=_,t2(b),t}let rN=e=>{let t;for(let n in e)("class"===n||"style"===n||C(n))&&((t||(t={}))[n]=e[n]);return t},rA=(e,t)=>{let n={};for(let r in e)k(r)&&r.slice(9)in t||(n[r]=e[r]);return n};function rE(e,t,n){let r=Object.keys(t);if(r.length!==Object.keys(e).length)return!0;for(let i=0;iObject.getPrototypeOf(e)===rO;function rP(e,t,n,r){let i,[l,s]=e.propsOptions,o=!1;if(t)for(let a in t){let c;if($(a))continue;let u=t[a];l&&A(l,c=j(a))?s&&s.includes(c)?(i||(i={}))[c]=u:n[c]=u:rT(e.emitsOptions,a)||a in r&&u===r[a]||(r[a]=u,o=!0)}if(s){let t=tm(n),r=i||b;for(let i=0;i"_"===e||"_ctx"===e||"$stable"===e,rV=e=>E(e)?e.map(ib):[ib(e)],rB=(e,t,n)=>{if(t._n)return t;let r=t3((...e)=>rV(t(...e)),n);return r._c=!1,r},rj=(e,t,n)=>{let r=e._ctx;for(let n in e){if(rD(n))continue;let i=e[n];if(I(i))t[n]=rB(n,i,r);else if(null!=i){let e=rV(i);t[n]=()=>e}}},rU=(e,t)=>{let n=rV(t);e.slots.default=()=>n},rH=(e,t,n)=>{for(let r in t)(n||!rD(r))&&(e[r]=t[r])},rq=r3;function rW(e){return rK(e,nF)}function rK(e,t){var n;let r,i;Q().__VUE__=!0;let{insert:l,remove:s,patchProp:o,createElement:a,createText:c,createComment:h,setText:d,setElementText:p,parentNode:f,nextSibling:g,setScopeId:m=S,insertStaticContent:y}=e,x=(e,t,n,r=null,i=null,l=null,s,o=null,a=!!t.dynamicChildren)=>{if(e===t)return;e&&!ic(e,t)&&(r=es(e),et(e,i,l,!0),e=null),-2===t.patchFlag&&(a=!1,t.dynamicChildren=null);let{type:c,ref:u,shapeFlag:h}=t;switch(c){case r8:C(e,t,n,r);break;case r5:k(e,t,n,r);break;case r9:null==e&&w(t,n,r,s);break;case r4:B(e,t,n,r,i,l,s,o,a);break;default:1&h?N(e,t,n,r,i,l,s,o,a):6&h?U(e,t,n,r,i,l,s,o,a):64&h?c.process(e,t,n,r,i,l,s,o,a,ec):128&h&&c.process(e,t,n,r,i,l,s,o,a,ec)}null!=u&&i?nE(u,e&&e.ref,l,t||e,!t):null==u&&e&&null!=e.ref&&nE(e.ref,null,l,e,!0)},C=(e,t,n,r)=>{if(null==e)l(t.el=c(t.children),n,r);else{let n=t.el=e.el;t.children!==e.children&&d(n,t.children)}},k=(e,t,n,r)=>{null==e?l(t.el=h(t.children||""),n,r):t.el=e.el},w=(e,t,n,r)=>{[e.el,e.anchor]=y(e.children,t,n,r,e.el,e.anchor)},N=(e,t,n,r,i,l,s,o,a)=>{if("svg"===t.type?s="svg":"math"===t.type&&(s="mathml"),null==e)R(t,n,r,i,l,s,o,a);else{let n=e.el&&e.el._isVueCE?e.el:null;try{n&&n._beginPatch(),L(e,t,i,l,s,o,a)}finally{n&&n._endPatch()}}},R=(e,t,n,r,i,s,c,u)=>{let h,d,{props:f,shapeFlag:g,transition:m,dirs:y}=e;if(h=e.el=a(e.type,s,f&&f.is,f),8&g?p(h,e.children):16&g&&F(e.children,h,null,r,i,rz(e,s),c,u),y&&t6(e,null,r,"created"),O(h,e,e.scopeId,c,r),f){for(let e in f)"value"===e||$(e)||o(h,e,null,f[e],s,r);"value"in f&&o(h,"value",null,f.value,s),(d=f.onVnodeBeforeMount)&&iC(d,r,e)}y&&t6(e,null,r,"beforeMount");let b=rG(i,m);b&&m.beforeEnter(h),l(h,t,n),((d=f&&f.onVnodeMounted)||b||y)&&rq(()=>{d&&iC(d,r,e),b&&m.enter(h),y&&t6(e,null,r,"mounted")},i)},O=(e,t,n,r,i)=>{if(n&&m(e,n),r)for(let t=0;t{for(let c=a;c{let a,c=t.el=e.el,{patchFlag:u,dynamicChildren:h,dirs:d}=t;u|=16&e.patchFlag;let f=e.props||b,g=t.props||b;if(n&&rJ(n,!1),(a=g.onVnodeBeforeUpdate)&&iC(a,n,t,e),d&&t6(t,e,n,"beforeUpdate"),n&&rJ(n,!0),(f.innerHTML&&null==g.innerHTML||f.textContent&&null==g.textContent)&&p(c,""),h?D(e.dynamicChildren,h,c,n,r,rz(t,i),l):s||X(e,t,c,null,n,r,rz(t,i),l,!1),u>0){if(16&u)V(c,f,g,n,i);else if(2&u&&f.class!==g.class&&o(c,"class",null,g.class,i),4&u&&o(c,"style",f.style,g.style,i),8&u){let e=t.dynamicProps;for(let t=0;t{a&&iC(a,n,t,e),d&&t6(t,e,n,"updated")},r)},D=(e,t,n,r,i,l,s)=>{for(let o=0;o{if(t!==n){if(t!==b)for(let l in t)$(l)||l in n||o(e,l,t[l],null,i,r);for(let l in n){if($(l))continue;let s=n[l],a=t[l];s!==a&&"value"!==l&&o(e,l,a,s,i,r)}"value"in n&&o(e,"value",t.value,n.value,i)}},B=(e,t,n,r,i,s,o,a,u)=>{let h=t.el=e?e.el:c(""),d=t.anchor=e?e.anchor:c(""),{patchFlag:p,dynamicChildren:f,slotScopeIds:g}=t;g&&(a=a?a.concat(g):g),null==e?(l(h,n,r),l(d,n,r),F(t.children||[],n,d,i,s,o,a,u)):p>0&&64&p&&f&&e.dynamicChildren&&e.dynamicChildren.length===f.length?(D(e.dynamicChildren,f,n,i,s,o,a),(null!=t.key||i&&t===i.subTree)&&rX(e,t,!0)):X(e,t,n,d,i,s,o,a,u)},U=(e,t,n,r,i,l,s,o,a)=>{t.slotScopeIds=o,null==e?512&t.shapeFlag?i.ctx.activate(t,n,r,s,a):q(t,n,r,i,l,s,a):W(e,t,a)},q=(e,t,n,r,i,l,s)=>{var o,a,c;let h,d,p,f=(o=e,a=r,c=i,h=o.type,d=(a?a.appContext:o.appContext)||ik,(p={uid:iT++,vnode:o,type:h,parent:a,appContext:d,root:null,next:null,subTree:null,effect:null,update:null,job:null,scope:new em(!0),render:null,proxy:null,exposed:null,exposeProxy:null,withProxy:null,provides:a?a.provides:Object.create(d.provides),ids:a?a.ids:["",0,0],accessCache:null,renderCache:[],components:null,directives:null,propsOptions:function e(t,n,r=!1){let i=r?rL:n.propsCache,l=i.get(t);if(l)return l;let s=t.props,o={},a=[],c=!1;if(!I(t)){let i=t=>{c=!0;let[r,i]=e(t,n,!0);T(o,r),i&&a.push(...i)};!r&&n.mixins.length&&n.mixins.forEach(i),t.extends&&i(t.extends),t.mixins&&t.mixins.forEach(i)}if(!s&&!c)return M(t)&&i.set(t,_),_;if(E(s))for(let e=0;e{let r=e(t,n,!0);r&&(a=!0,T(o,r))};!r&&n.mixins.length&&n.mixins.forEach(i),t.extends&&i(t.extends),t.mixins&&t.mixins.forEach(i)}return s||a?(E(s)?s.forEach(e=>o[e]=null):T(o,s),M(t)&&i.set(t,o),o):(M(t)&&i.set(t,null),null)}(h,d),emit:null,emitted:null,propsDefaults:b,inheritAttrs:h.inheritAttrs,ctx:b,data:b,props:b,attrs:b,slots:b,refs:b,setupState:b,setupContext:null,suspense:c,suspenseId:c?c.pendingId:0,asyncDep:null,asyncResolved:!1,isMounted:!1,isUnmounted:!1,isDeactivated:!1,bc:null,c:null,bm:null,m:null,bu:null,u:null,um:null,bum:null,da:null,a:null,rtg:null,rtc:null,ec:null,sp:null}).ctx={_:p},p.root=a?a.root:p,p.emit=rC.bind(null,p),o.ce&&o.ce(p),e.component=p);if(nH(e)&&(f.ctx.renderer=ec),function(e,t=!1,n=!1){t&&u(t);let{props:r,children:i}=e.vnode,l=iI(e);!function(e,t,n,r=!1){let i={},l=Object.create(rO);for(let n in e.propsDefaults=Object.create(null),rP(e,t,i,l),e.propsOptions[0])n in i||(i[n]=void 0);n?e.props=r?i:tc(i):e.type.props?e.props=i:e.props=l,e.attrs=l}(e,r,l,t);var s=n||t;let o=e.slots=Object.create(rO);if(32&e.vnode.shapeFlag){let e=i._;e?(rH(o,i,s),s&&J(o,"_",e,!0)):rj(i,o)}else i&&rU(e,i);l&&function(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,rl);let{setup:r}=n;if(r){eE();let n=e.setupContext=r.length>1?iL(e):null,i=iA(e),l=t$(r,e,0,[e.props,n]),s=P(l);if(eI(),i(),(s||e.sp)&&!nj(e)&&nw(e),s){if(l.then(iE,iE),t)return l.then(n=>{iO(e,n,t)}).catch(t=>{tV(t,e,0)});e.asyncDep=l}else iO(e,l,t)}else iP(e,t)}(e,t),t&&u(!1)}(f,!1,s),f.asyncDep){if(i&&i.registerDep(f,K,s),!e.el){let r=f.subTree=ip(r5);k(null,r,t,n),e.placeholder=r.el}}else K(f,e,t,n,i,l,s)},W=(e,t,n)=>{let r=t.component=e.component;if(function(e,t,n){let{props:r,children:i,component:l}=e,{props:s,children:o,patchFlag:a}=t,c=l.emitsOptions;if(t.dirs||t.transition)return!0;if(!n||!(a>=0))return(!!i||!!o)&&(!o||!o.$stable)||r!==s&&(r?!s||rE(r,s,c):!!s);if(1024&a)return!0;if(16&a)return r?rE(r,s,c):!!s;if(8&a){let e=t.dynamicProps;for(let t=0;t{e.scope.on();let a=e.effect=new ey(()=>{if(e.isMounted){let t,{next:n,bu:r,u:i,parent:a,vnode:u}=e;{let t=function e(t){let n=t.subTree.component;if(n)if(n.asyncDep&&!n.asyncResolved)return n;else return e(n)}(e);if(t){n&&(n.el=u.el,G(e,n,o)),t.asyncDep.then(()=>{rq(()=>{e.isUnmounted||c()},l)});return}}let h=n;rJ(e,!1),n?(n.el=u.el,G(e,n,o)):n=u,r&&z(r),(t=n.props&&n.props.onVnodeBeforeUpdate)&&iC(t,a,n,u),rJ(e,!0);let d=rw(e),p=e.subTree;e.subTree=d,x(p,d,f(p.el),es(p),e,l,s),n.el=d.el,null===h&&rR(e,d.el),i&&rq(i,l),(t=n.props&&n.props.onVnodeUpdated)&&rq(()=>iC(t,a,n,u),l)}else{let o,{el:a,props:c}=t,{bm:u,m:h,parent:d,root:p,type:f}=e,g=nj(t);if(rJ(e,!1),u&&z(u),!g&&(o=c&&c.onVnodeBeforeMount)&&iC(o,d,t),rJ(e,!0),a&&i){let t=()=>{e.subTree=rw(e),i(a,e.subTree,e,l,null)};g&&f.__asyncHydrate?f.__asyncHydrate(a,e,t):t()}else{p.ce&&p.ce._hasShadowRoot()&&p.ce._injectChildStyle(f,e.parent?e.parent.type:void 0);let i=e.subTree=rw(e);x(null,i,n,r,e,l,s),t.el=i.el}if(h&&rq(h,l),!g&&(o=c&&c.onVnodeMounted)){let e=t;rq(()=>iC(o,d,e),l)}(256&t.shapeFlag||d&&nj(d.vnode)&&256&d.vnode.shapeFlag)&&e.a&&rq(e.a,l),e.isMounted=!0,t=n=r=null}});e.scope.off();let c=e.update=a.run.bind(a),u=e.job=a.runIfDirty.bind(a);u.i=e,u.id=e.uid,a.scheduler=()=>tJ(u),rJ(e,!0),c()},G=(e,t,n)=>{t.component=e;let r=e.vnode.props;e.vnode=t,e.next=null,function(e,t,n,r){let{props:i,attrs:l,vnode:{patchFlag:s}}=e,o=tm(i),[a]=e.propsOptions,c=!1;if((r||s>0)&&!(16&s)){if(8&s){let n=e.vnode.dynamicProps;for(let r=0;r{let{vnode:r,slots:i}=e,l=!0,s=b;if(32&r.shapeFlag){let e=t._;e?n&&1===e?l=!1:rH(i,t,n):(l=!t.$stable,rj(t,i)),s=t}else t&&(rU(e,t),s={default:1});if(l)for(let e in i)rD(e)||null!=s[e]||delete i[e]})(e,t.children,n),eE(),tQ(e),eI()},X=(e,t,n,r,i,l,s,o,a=!1)=>{let c=e&&e.children,u=e?e.shapeFlag:0,h=t.children,{patchFlag:d,shapeFlag:f}=t;if(d>0){if(128&d)return void Y(c,h,n,r,i,l,s,o,a);else if(256&d)return void Z(c,h,n,r,i,l,s,o,a)}8&f?(16&u&&el(c,i,l),h!==c&&p(n,h)):16&u?16&f?Y(c,h,n,r,i,l,s,o,a):el(c,i,l,!0):(8&u&&p(n,""),16&f&&F(h,n,r,i,l,s,o,a))},Z=(e,t,n,r,i,l,s,o,a)=>{let c;e=e||_,t=t||_;let u=e.length,h=t.length,d=Math.min(u,h);for(c=0;ch?el(e,i,l,!0,!1,d):F(t,n,r,i,l,s,o,a,d)},Y=(e,t,n,r,i,l,s,o,a)=>{let c=0,u=t.length,h=e.length-1,d=u-1;for(;c<=h&&c<=d;){let r=e[c],u=t[c]=a?i_(t[c]):ib(t[c]);if(ic(r,u))x(r,u,n,null,i,l,s,o,a);else break;c++}for(;c<=h&&c<=d;){let r=e[h],c=t[d]=a?i_(t[d]):ib(t[d]);if(ic(r,c))x(r,c,n,null,i,l,s,o,a);else break;h--,d--}if(c>h){if(c<=d){let e=d+1,h=ed)for(;c<=h;)et(e[c],i,l,!0),c++;else{let p,f=c,g=c,m=new Map;for(c=g;c<=d;c++){let e=t[c]=a?i_(t[c]):ib(t[c]);null!=e.key&&m.set(e.key,c)}let y=0,b=d-g+1,S=!1,C=0,k=Array(b);for(c=0;c=b){et(u,i,l,!0);continue}if(null!=u.key)r=m.get(u.key);else for(p=g;p<=d;p++)if(0===k[p-g]&&ic(u,t[p])){r=p;break}void 0===r?et(u,i,l,!0):(k[r-g]=c+1,r>=C?C=r:S=!0,x(u,t[r],n,null,i,l,s,o,a),y++)}let T=S?function(e){let t,n,r,i,l,s=e.slice(),o=[0],a=e.length;for(t=0;t>1]]0&&(s[t]=o[r-1]),o[r]=t)}}for(r=o.length,i=o[r-1];r-- >0;)o[r]=i,i=s[i];return o}(k):_;for(p=T.length-1,c=b-1;c>=0;c--){let e=g+c,h=t[e],d=t[e+1],f=e+1{let{el:o,type:a,transition:c,children:u,shapeFlag:h}=e;if(6&h)return void ee(e.component.subTree,t,n,r);if(128&h)return void e.suspense.move(t,n,r);if(64&h)return void a.move(e,t,n,ec);if(a===r4){l(o,t,n);for(let e=0;e{let i;for(;e&&e!==t;)i=g(e),l(e,n,r),e=i;l(t,n,r)})(e,t,n);if(2!==r&&1&h&&c)if(0===r)c.beforeEnter(o),l(o,t,n),rq(()=>c.enter(o),i);else{let{leave:r,delayLeave:i,afterLeave:a}=c,u=()=>{e.ctx.isUnmounted?s(o):l(o,t,n)},h=()=>{o._isLeaving&&o[nh](!0),r(o,()=>{u(),a&&a()})};i?i(o,u,h):h()}else l(o,t,n)},et=(e,t,n,r=!1,i=!1)=>{let l,{type:s,props:o,ref:a,children:c,dynamicChildren:u,shapeFlag:h,patchFlag:d,dirs:p,cacheIndex:f,memo:g}=e;if(-2===d&&(i=!1),null!=a&&(eE(),nE(a,null,n,e,!0),eI()),null!=f&&(t.renderCache[f]=void 0),256&h)return void t.ctx.deactivate(e);let m=1&h&&p,y=!nj(e);if(y&&(l=o&&o.onVnodeBeforeUnmount)&&iC(l,t,e),6&h)ei(e.component,n,r);else{if(128&h)return void e.suspense.unmount(n,r);m&&t6(e,null,t,"beforeUnmount"),64&h?e.type.remove(e,t,n,ec,r):u&&!u.hasOnce&&(s!==r4||d>0&&64&d)?el(u,t,n,!1,!0):(s===r4&&384&d||!i&&16&h)&&el(c,t,n),r&&en(e)}let b=null!=g&&null==f;(y&&(l=o&&o.onVnodeUnmounted)||m||b)&&rq(()=>{l&&iC(l,t,e),m&&t6(e,null,t,"unmounted"),b&&(e.el=null)},n)},en=e=>{let{type:t,el:n,anchor:r,transition:i}=e;if(t===r4)return void er(n,r);if(t===r9)return void(({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=g(e),s(e),e=n;s(t)})(e);let l=()=>{s(n),i&&!i.persisted&&i.afterLeave&&i.afterLeave()};if(1&e.shapeFlag&&i&&!i.persisted){let{leave:t,delayLeave:r}=i,s=()=>t(n,l);r?r(e.el,l,s):s()}else l()},er=(e,t)=>{let n;for(;e!==t;)n=g(e),s(e),e=n;s(t)},ei=(e,t,n)=>{let{bum:r,scope:i,job:l,subTree:s,um:o,m:a,a:c}=e;rQ(a),rQ(c),r&&z(r),i.stop(),l&&(l.flags|=8,et(s,e,t,n)),o&&rq(o,t),rq(()=>{e.isUnmounted=!0},t)},el=(e,t,n,r=!1,i=!1,l=0)=>{for(let s=l;s{if(6&e.shapeFlag)return es(e.component.subTree);if(128&e.shapeFlag)return e.suspense.next();let t=g(e.anchor||e.el),n=t&&t[nr];return n?g(n):t},eo=!1,ea=(e,t,n)=>{let r;null==e?t._vnode&&(et(t._vnode,null,null,!0),r=t._vnode.component):x(t._vnode||null,e,t,null,null,null,n),t._vnode=e,eo||(eo=!0,tQ(r),tZ(),eo=!1)},ec={p:x,um:et,m:ee,r:en,mt:q,mc:F,pc:X,pbc:D,n:es,o:e};return t&&([r,i]=t(ec)),{render:ea,hydrate:r,createApp:(n=r,function(e,t=null){I(e)||(e=T({},e)),null==t||M(t)||(t=null);let r=rb(),i=new WeakSet,l=[],s=!1,o=r.app={_uid:r_++,_component:e,_props:t,_container:null,_context:r,_instance:null,version:iU,get config(){return r.config},set config(v){},use:(e,...t)=>(i.has(e)||(e&&I(e.install)?(i.add(e),e.install(o,...t)):I(e)&&(i.add(e),e(o,...t))),o),mixin:e=>(r.mixins.includes(e)||r.mixins.push(e),o),component:(e,t)=>t?(r.components[e]=t,o):r.components[e],directive:(e,t)=>t?(r.directives[e]=t,o):r.directives[e],mount(i,l,a){if(!s){let c=o._ceVNode||ip(e,t);return c.appContext=r,!0===a?a="svg":!1===a&&(a=void 0),l&&n?n(c,i):ea(c,i,a),s=!0,o._container=i,i.__vue_app__=o,i$(c.component)}},onUnmount(e){l.push(e)},unmount(){s&&(tD(l,o._instance,16),ea(null,o._container),delete o._container.__vue_app__)},provide:(e,t)=>(r.provides[e]=t,o),runWithContext(e){let t=rS;rS=o;try{return e()}finally{rS=t}}};return o})}}function rz({type:e,props:t},n){return"svg"===n&&"foreignObject"===e||"mathml"===n&&"annotation-xml"===e&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function rJ({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function rG(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function rX(e,t,n=!1){let r=e.children,i=t.children;if(E(r)&&E(i))for(let e=0;ee.__isSuspense,rY=0;function r0(e,t){let n=e.props&&e.props[t];I(n)&&n()}function r1(e,t,n,r,i,l,s,o,a,c,u=!1){var h;let d,p,{p:f,m:g,um:m,n:y,o:{parentNode:b,remove:_}}=c,S=null!=(d=(h=e).props&&h.props.suspensible)&&!1!==d;S&&t&&t.pendingBranch&&(p=t.pendingId,t.deps++);let x=e.props?X(e.props.timeout):void 0,C=l,k={vnode:e,parent:t,parentComponent:n,namespace:s,container:r,hiddenContainer:i,deps:0,pendingId:rY++,timeout:"number"==typeof x?x:-1,activeBranch:null,isFallbackMountPending:!1,pendingBranch:null,isInFallback:!u,isHydrating:u,isUnmounted:!1,effects:[],resolve(e=!1,n=!1){let{vnode:r,activeBranch:i,pendingBranch:s,pendingId:o,effects:a,parentComponent:c,container:u,isInFallback:h}=k,d=!1;k.isHydrating?k.isHydrating=!1:!e&&((d=i&&s.transition&&"out-in"===s.transition.mode)&&(i.transition.afterLeave=()=>{o===k.pendingId&&(g(s,u,l===C?y(i):l,0),tX(a),h&&r.ssFallback&&(r.ssFallback.el=null))}),i&&!k.isFallbackMountPending&&(b(i.el)===u&&(l=y(i)),m(i,c,k,!0),!d&&h&&r.ssFallback&&rq(()=>r.ssFallback.el=null,k)),d||g(s,u,l,0)),k.isFallbackMountPending=!1,r6(k,s),k.pendingBranch=null,k.isInFallback=!1;let f=k.parent,_=!1;for(;f;){if(f.pendingBranch){f.effects.push(...a),_=!0;break}f=f.parent}_||d||tX(a),k.effects=[],S&&t&&t.pendingBranch&&p===t.pendingId&&(t.deps--,0!==t.deps||n||t.resolve()),r0(r,"onResolve")},fallback(e){if(!k.pendingBranch)return;let{vnode:t,activeBranch:n,parentComponent:r,container:i,namespace:l}=k;r0(t,"onFallback");let s=y(n),c=()=>{k.isFallbackMountPending=!1,k.isInFallback&&(f(null,e,i,s,r,null,l,o,a),r6(k,e))},u=e.transition&&"out-in"===e.transition.mode;u&&(k.isFallbackMountPending=!0,n.transition.afterLeave=c),k.isInFallback=!0,m(n,r,null,!0),u||c()},move(e,t,n){k.activeBranch&&g(k.activeBranch,e,t,n),k.container=e},next:()=>k.activeBranch&&y(k.activeBranch),registerDep(e,t,n){let r=!!k.pendingBranch;r&&k.deps++;let i=e.vnode.el;e.asyncDep.catch(t=>{tV(t,e,0)}).then(l=>{if(e.isUnmounted||k.isUnmounted||k.pendingId!==e.suspenseId)return;iE(),e.asyncResolved=!0;let{vnode:o}=e;iO(e,l,!1),i&&(o.el=i);let a=!i&&e.subTree.el;t(e,o,b(i||e.subTree.el),i?null:y(e.subTree),k,s,n),a&&(o.placeholder=null,_(a)),rR(e,o.el),r&&0==--k.deps&&k.resolve()})},unmount(e,t){k.isUnmounted=!0,k.activeBranch&&m(k.activeBranch,n,e,t),k.pendingBranch&&m(k.pendingBranch,n,e,t)}};return k}function r2(e){let t;if(I(e)){let n=ii&&e._c;n&&(e._d=!1,it()),e=e(),n&&(e._d=!0,t=ie,ir())}return E(e)&&(e=function(e){let t;for(let n=0;nt!==e)),e}function r3(e,t){t&&t.pendingBranch?E(e)?t.effects.push(...e):t.effects.push(e):tX(e)}function r6(e,t){e.activeBranch=t;let{vnode:n,parentComponent:r}=e,i=t.el;for(;!i&&t.component;)i=(t=t.component.subTree).el;n.el=i,r&&r.subTree===n&&(r.vnode.el=i,rR(r,i))}let r4=Symbol.for("v-fgt"),r8=Symbol.for("v-txt"),r5=Symbol.for("v-cmt"),r9=Symbol.for("v-stc"),r7=[],ie=null;function it(e=!1){r7.push(ie=e?null:[])}function ir(){r7.pop(),ie=r7[r7.length-1]||null}let ii=1;function il(e,t=!1){ii+=e,e<0&&ie&&t&&(ie.hasOnce=!0)}function is(e){return e.dynamicChildren=ii>0?ie||_:null,ir(),ii>0&&ie&&ie.push(e),e}function io(e,t,n,r,i){return is(ip(e,t,n,r,i,!0))}function ia(e){return!!e&&!0===e.__v_isVNode}function ic(e,t){return e.type===t.type&&e.key===t.key}let iu=({key:e})=>null!=e?e:null,ih=({ref:e,ref_key:t,ref_for:n})=>("number"==typeof e&&(e=""+e),null!=e?R(e)||t_(e)||I(e)?{i:t0,r:e,k:t,f:!!n}:e:null);function id(e,t=null,n=null,r=0,i=null,l=+(e!==r4),s=!1,o=!1){let a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&iu(t),ref:t&&ih(t),scopeId:t1,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:l,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:t0};return o?(iS(a,n),128&l&&e.normalize(a)):n&&(a.shapeFlag|=R(n)?8:16),ii>0&&!s&&ie&&(a.patchFlag>0||6&l)&&32!==a.patchFlag&&ie.push(a),a}let ip=function(e,t=null,n=null,r=0,i=null,l=!1){var s;if(e&&e!==n7||(e=r5),ia(e)){let r=im(e,t,!0);return n&&iS(r,n),ii>0&&!l&&ie&&(6&r.shapeFlag?ie[ie.indexOf(e)]=r:ie.push(r)),r.patchFlag=-2,r}if(I(s=e)&&"__vccOpts"in s&&(e=e.__vccOpts),t){let{class:e,style:n}=t=ig(t);e&&!R(e)&&(t.class=ei(e)),M(n)&&(tg(n)&&!E(n)&&(n=T({},n)),t.style=Y(n))}let o=R(e)?1:rZ(e)?128:e.__isTeleport?64:M(e)?4:2*!!I(e);return id(e,t,n,r,i,o,l,!0)};function ig(e){return e?tg(e)||rM(e)?T({},e):e:null}function im(e,t,n=!1,r=!1){let{props:i,ref:l,patchFlag:s,children:o,transition:a}=e,c=t?ix(i||{},t):i,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&iu(c),ref:t&&t.ref?n&&l?E(l)?l.concat(ih(t)):[l,ih(t)]:ih(t):l,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==r4?-1===s?16:16|s:s,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:a,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&im(e.ssContent),ssFallback:e.ssFallback&&im(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return a&&r&&nC(u,a.clone(u)),u}function iv(e=" ",t=0){return ip(r8,null,e,t)}function iy(e="",t=!1){return t?(it(),io(r5,null,e)):ip(r5,null,e)}function ib(e){return null==e||"boolean"==typeof e?ip(r5):E(e)?ip(r4,null,e.slice()):ia(e)?i_(e):ip(r8,null,String(e))}function i_(e){return null===e.el&&-1!==e.patchFlag||e.memo?e:im(e)}function iS(e,t){let n=0,{shapeFlag:r}=e;if(null==t)t=null;else if(E(t))n=16;else if("object"==typeof t)if(65&r){let n=t.default;n&&(n._c&&(n._d=!1),iS(e,n()),n._c&&(n._d=!0));return}else{n=32;let r=t._;r||rM(t)?3===r&&t0&&(1===t0.slots._?t._=1:(t._=2,e.patchFlag|=1024)):t._ctx=t0}else I(t)?(t={default:t,_ctx:t0},n=32):(t=String(t),64&r?(n=16,t=[iv(t)]):n=8);e.children=t,e.shapeFlag|=n}function ix(...e){let t={};for(let n=0;niw||t0;c=e=>{iw=e},u=e=>{iR=e};let iA=e=>{let t=iw;return c(e),e.scope.on(),()=>{e.scope.off(),c(t)}},iE=()=>{iw&&iw.scope.off(),c(null)};function iI(e){return 4&e.vnode.shapeFlag}let iR=!1;function iO(e,t,n){I(t)?e.render=t:M(t)&&(e.setupState=tN(t)),iP(e,n)}function iM(e){h=e,d=e=>{e.render._rc&&(e.withProxy=new Proxy(e.ctx,rs))}}function iP(e,t,n){let r=e.type;if(!e.render){if(!t&&h&&!r.render){let t=r.template||rh(e).template;if(t){let{isCustomElement:n,compilerOptions:i}=e.appContext.config,{delimiters:l,compilerOptions:s}=r,o=T(T({isCustomElement:n,delimiters:l},i),s);r.render=h(t,o)}}e.render=r.render||S,d&&d(e)}{let t=iA(e);eE();try{!function(e){let t=rh(e),n=e.proxy,r=e.ctx;rc=!1,t.beforeCreate&&ru(t.beforeCreate,e,"bc");let{data:i,computed:l,methods:s,watch:o,provide:a,inject:c,created:u,beforeMount:h,mounted:d,beforeUpdate:p,updated:f,activated:g,deactivated:m,beforeUnmount:y,unmounted:b,render:_,renderTracked:x,renderTriggered:C,errorCaptured:k,serverPrefetch:T,expose:w,inheritAttrs:N,components:A,directives:O}=t;if(c&&function(e,t){for(let n in E(e)&&(e=rg(e)),e){let r,i=e[n];t_(r=M(i)?"default"in i?t8(i.from||n,i.default,!0):t8(i.from||n):t8(i))?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>r.value,set:e=>r.value=e}):t[n]=r}}(c,r),s)for(let e in s){let t=s[e];I(t)&&(r[e]=t.bind(n))}if(i){let t=i.call(n,n);M(t)&&(e.data=ta(t))}if(rc=!0,l)for(let e in l){let t=l[e],i=I(t)?t.bind(n,n):I(t.get)?t.get.bind(n,n):S,s=iV({get:i,set:!I(t)&&I(t.set)?t.set.bind(n):S});Object.defineProperty(r,e,{enumerable:!0,configurable:!0,get:()=>s.value,set:e=>s.value=e})}if(o)for(let e in o)!function e(t,n,r,i){let l=i.includes(".")?nt(r,i):()=>r[i];if(R(t)){let e=n[t];I(e)&&t7(l,e,void 0)}else if(I(t))t7(l,t.bind(r),void 0);else if(M(t))if(E(t))t.forEach(t=>e(t,n,r,i));else{let e=I(t.handler)?t.handler.bind(r):n[t.handler];I(e)&&t7(l,e,t)}}(o[e],r,n,e);if(a){let e=I(a)?a.call(n):a;Reflect.ownKeys(e).forEach(t=>{t4(t,e[t])})}function P(e,t){E(t)?t.forEach(t=>e(t.bind(n))):t&&e(t.bind(n))}if(u&&ru(u,e,"c"),P(nZ,h),P(nY,d),P(n0,p),P(n1,f),P(nW,g),P(nK,m),P(n5,k),P(n8,x),P(n4,C),P(n2,y),P(n3,b),P(n6,T),E(w))if(w.length){let t=e.exposed||(e.exposed={});w.forEach(e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})})}else e.exposed||(e.exposed={});_&&e.render===S&&(e.render=_),null!=N&&(e.inheritAttrs=N),A&&(e.components=A),O&&(e.directives=O)}(e)}finally{eI(),t()}}}let iF={get:(e,t)=>(eV(e,"get",""),e[t])};function iL(e){return{attrs:new Proxy(e.attrs,iF),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function i$(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(tN(tv(e.exposed)),{get:(t,n)=>n in t?t[n]:n in rr?rr[n](e):void 0,has:(e,t)=>t in e||t in rr})):e.proxy}function iD(e,t=!0){return I(e)?e.displayName||e.name:e.name||t&&e.__name}let iV=(e,t)=>(function(e,t=!1){let n,r;return I(e)?n=e:(n=e.get,r=e.set),new tO(n,r,t)})(e,iR);function iB(e,t,n){try{il(-1);let r=arguments.length;if(2!==r)return r>3?n=Array.prototype.slice.call(arguments,2):3===r&&ia(n)&&(n=[n]),ip(e,t,n);if(!M(t)||E(t))return ip(e,null,t);if(ia(t))return ip(e,null,[t]);return ip(e,t)}finally{il(1)}}function ij(e,t){let n=e.memo;if(n.length!=t.length)return!1;for(let e=0;e0&&ie&&ie.push(e),!0}let iU="3.5.33",iH="u">typeof window&&window.trustedTypes;if(iH)try{m=iH.createPolicy("vue",{createHTML:e=>e})}catch(e){}let iq=m?e=>m.createHTML(e):e=>e,iW="u">typeof document?document:null,iK=iW&&iW.createElement("template"),iz={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{let t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{let i="svg"===t?iW.createElementNS("http://www.w3.org/2000/svg",e):"mathml"===t?iW.createElementNS("http://www.w3.org/1998/Math/MathML",e):n?iW.createElement(e,{is:n}):iW.createElement(e);return"select"===e&&r&&null!=r.multiple&&i.setAttribute("multiple",r.multiple),i},createText:e=>iW.createTextNode(e),createComment:e=>iW.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>iW.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,i,l){let s=n?n.previousSibling:t.lastChild;if(i&&(i===l||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),i!==l&&(i=i.nextSibling););else{iK.innerHTML=iq("svg"===r?`${e}`:"mathml"===r?`${e}`:e);let i=iK.content;if("svg"===r||"mathml"===r){let e=i.firstChild;for(;e.firstChild;)i.appendChild(e.firstChild);i.removeChild(e)}t.insertBefore(i,n)}return[s?s.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},iJ="transition",iG="animation",iX=Symbol("_vtc"),iQ={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},iZ=T({},ng,iQ),iY=((t=(e,{slots:t})=>iB(ny,i2(e),t)).displayName="Transition",t.props=iZ,t),i0=(e,t=[])=>{E(e)?e.forEach(e=>e(...t)):e&&e(...t)},i1=e=>!!e&&(E(e)?e.some(e=>e.length>1):e.length>1);function i2(e){let t={};for(let n in e)n in iQ||(t[n]=e[n]);if(!1===e.css)return t;let{name:n="v",type:r,duration:i,enterFromClass:l=`${n}-enter-from`,enterActiveClass:s=`${n}-enter-active`,enterToClass:o=`${n}-enter-to`,appearFromClass:a=l,appearActiveClass:c=s,appearToClass:u=o,leaveFromClass:h=`${n}-leave-from`,leaveActiveClass:d=`${n}-leave-active`,leaveToClass:p=`${n}-leave-to`}=e,f=function(e){if(null==e)return null;{if(M(e))return[function(e){return X(e)}(e.enter),function(e){return X(e)}(e.leave)];let t=function(e){return X(e)}(e);return[t,t]}}(i),g=f&&f[0],m=f&&f[1],{onBeforeEnter:y,onEnter:b,onEnterCancelled:_,onLeave:S,onLeaveCancelled:x,onBeforeAppear:C=y,onAppear:k=b,onAppearCancelled:w=_}=t,N=(e,t,n,r)=>{e._enterCancelled=r,i6(e,t?u:o),i6(e,t?c:s),n&&n()},A=(e,t)=>{e._isLeaving=!1,i6(e,h),i6(e,p),i6(e,d),t&&t()},E=e=>(t,n)=>{let i=e?k:b,s=()=>N(t,e,n);i0(i,[t,s]),i4(()=>{i6(t,e?a:l),i3(t,e?u:o),i1(i)||i5(t,r,g,s)})};return T(t,{onBeforeEnter(e){i0(y,[e]),i3(e,l),i3(e,s)},onBeforeAppear(e){i0(C,[e]),i3(e,a),i3(e,c)},onEnter:E(!1),onAppear:E(!0),onLeave(e,t){e._isLeaving=!0;let n=()=>A(e,t);i3(e,h),e._enterCancelled?(i3(e,d),lt(e)):(lt(e),i3(e,d)),i4(()=>{e._isLeaving&&(i6(e,h),i3(e,p),i1(S)||i5(e,r,m,n))}),i0(S,[e,n])},onEnterCancelled(e){N(e,!1,void 0,!0),i0(_,[e])},onAppearCancelled(e){N(e,!0,void 0,!0),i0(w,[e])},onLeaveCancelled(e){A(e),i0(x,[e])}})}function i3(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.add(t)),(e[iX]||(e[iX]=new Set)).add(t)}function i6(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.remove(t));let n=e[iX];n&&(n.delete(t),n.size||(e[iX]=void 0))}function i4(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let i8=0;function i5(e,t,n,r){let i=e._endId=++i8,l=()=>{i===e._endId&&r()};if(null!=n)return setTimeout(l,n);let{type:s,timeout:o,propCount:a}=i9(e,t);if(!s)return r();let c=s+"end",u=0,h=()=>{e.removeEventListener(c,d),l()},d=t=>{t.target===e&&++u>=a&&h()};setTimeout(()=>{u(n[e]||"").split(", "),i=r(`${iJ}Delay`),l=r(`${iJ}Duration`),s=i7(i,l),o=r(`${iG}Delay`),a=r(`${iG}Duration`),c=i7(o,a),u=null,h=0,d=0;t===iJ?s>0&&(u=iJ,h=s,d=l.length):t===iG?c>0&&(u=iG,h=c,d=a.length):d=(u=(h=Math.max(s,c))>0?s>c?iJ:iG:null)?u===iJ?l.length:a.length:0;let p=u===iJ&&/\b(?:transform|all)(?:,|$)/.test(r(`${iJ}Property`).toString());return{type:u,timeout:h,propCount:d,hasTransform:p}}function i7(e,t){for(;e.lengthle(t)+le(e[n])))}function le(e){return"auto"===e?0:1e3*Number(e.slice(0,-1).replace(",","."))}function lt(e){return(e?e.ownerDocument:document).body.offsetHeight}let ln=Symbol("_vod"),lr=Symbol("_vsh");function li(e,t){e.style.display=t?e[ln]:"none",e[lr]=!t}let ll=Symbol("");function ls(e,t){if(1===e.nodeType){let r=e.style,i="";for(let e in t){var n;let l=null==(n=t[e])?"initial":"string"==typeof n?""===n?" ":n:String(n);r.setProperty(`--${e}`,l),i+=`--${e}: ${l};`}r[ll]=i}}let lo=/(?:^|;)\s*display\s*:/,la=/\s*!important$/;function lc(e,t,n){if(E(n))n.forEach(n=>lc(e,t,n));else if(null==n&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{let r=function(e,t){let n=lh[t];if(n)return n;let r=j(t);if("filter"!==r&&r in e)return lh[t]=r;r=q(r);for(let n=0;n111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&e.charCodeAt(2)>96&&123>e.charCodeAt(2),lS=(e,t,n,r,i,l)=>{let s="svg"===i;if("class"===t){var o;let t;o=r,(t=e[iX])&&(o=(o?[o,...t]:[...t]).join(" ")),null==o?e.removeAttribute("class"):s?e.setAttribute("class",o):e.className=o}else"style"===t?function(e,t,n){let r=e.style,i=R(n),l=!1;if(n&&!i){if(t)if(R(t))for(let e of t.split(";")){let t=e.slice(0,e.indexOf(":")).trim();null==n[t]&&lc(r,t,"")}else for(let e in t)null==n[e]&&lc(r,e,"");for(let i in n){var s,o,a,c;"display"===i&&(l=!0);let u=n[i];null!=u?(s=e,o=i,a=!R(t)&&t?t[i]:void 0,c=u,"TEXTAREA"===s.tagName&&("width"===o||"height"===o)&&R(c)&&a===c||lc(r,i,u)):lc(r,i,"")}}else if(i){if(t!==n){let e=r[ll];e&&(n+=";"+e),r.cssText=n,l=lo.test(n)}}else t&&e.removeAttribute("style");ln in e&&(e[ln]=l?r.display:"",e[lr]&&(r.display="none"))}(e,n,r):C(t)?k(t)||function(e,t,n,r=null){let i=e[lm]||(e[lm]={}),l=i[t];if(n&&l)l.value=n;else{let[a,c]=function(e){let t;if(lv.test(e)){let n;for(t={};n=e.match(lv);)e=e.slice(0,e.length-n[0].length),t[n[0].toLowerCase()]=!0}return[":"===e[2]?e.slice(3):H(e.slice(2)),t]}(t);if(n){var s,o;let l;lg(e,a,i[t]=(s=n,o=r,(l=e=>{if(e._vts){if(e._vts<=l.attached)return}else e._vts=Date.now();tD(function(e,t){if(!E(t))return t;{let n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(e=>t=>!t._stopped&&e&&e(t))}}(e,l.value),o,5,[e])}).value=s,l.attached=ly||(lb.then(()=>ly=0),ly=Date.now()),l),c)}else l&&(e.removeEventListener(a,l,c),i[t]=void 0)}}(e,t,r,l):("."===t[0]?(t=t.slice(1),0):"^"===t[0]?(t=t.slice(1),1):!function(e,t,n,r){if(r)return!!("innerHTML"===t||"textContent"===t||t in e&&l_(t)&&I(n));if("spellcheck"===t||"draggable"===t||"translate"===t||"autocorrect"===t||"sandbox"===t&&"IFRAME"===e.tagName||"form"===t||"list"===t&&"INPUT"===e.tagName||"type"===t&&"TEXTAREA"===e.tagName)return!1;if("width"===t||"height"===t){let t=e.tagName;if("IMG"===t||"VIDEO"===t||"CANVAS"===t||"SOURCE"===t)return!1}return!(l_(t)&&R(n))&&t in e}(e,t,r,s))?e._isVueCE&&(function(e,t){let n=e._def.props;if(!n)return!1;let r=j(t);return Array.isArray(n)?n.some(e=>j(e)===r):Object.keys(n).some(e=>j(e)===r)}(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!R(r)))?lf(e,j(t),r,l,t):("true-value"===t?e._trueValue=r:"false-value"===t&&(e._falseValue=r),lp(e,t,r,s)):(lf(e,t,r),e.tagName.includes("-")||"value"!==t&&"checked"!==t&&"selected"!==t||lp(e,t,r,s,l,"value"!==t))},lx={};function lC(e,t,n){let r,i=nT(e,t);"[object Object]"===(r=i,F.call(r))&&(i=T({},i,t));class l extends lT{constructor(e){super(i,e,n)}}return l.def=i,l}let lk="u">typeof HTMLElement?HTMLElement:class{};class lT extends lk{constructor(e,t={},n=l3){super(),this._def=e,this._props=t,this._createApp=n,this._isVueCE=!0,this._instance=null,this._app=null,this._nonce=this._def.nonce,this._connected=!1,this._resolved=!1,this._patching=!1,this._dirty=!1,this._numberProps=null,this._styleChildren=new WeakSet,this._styleAnchors=new WeakMap,this._ob=null,this.shadowRoot&&n!==l3?this._root=this.shadowRoot:!1!==e.shadowRoot?(this.attachShadow(T({},e.shadowRootOptions,{mode:"open"})),this._root=this.shadowRoot):this._root=this}connectedCallback(){if(!this.isConnected)return;this.shadowRoot||this._resolved||this._parseSlots(),this._connected=!0;let e=this;for(;e=e&&(e.assignedSlot||e.parentNode||e.host);)if(e instanceof lT){this._parent=e;break}this._instance||(this._resolved?this._mount(this._def):e&&e._pendingResolve?this._pendingResolve=e._pendingResolve.then(()=>{this._pendingResolve=void 0,this._resolveDef()}):this._resolveDef())}_setParent(e=this._parent){e&&(this._instance.parent=e._instance,this._inheritParentContext(e))}_inheritParentContext(e=this._parent){e&&this._app&&Object.setPrototypeOf(this._app._context.provides,e._instance.provides)}disconnectedCallback(){this._connected=!1,tz(()=>{!this._connected&&(this._ob&&(this._ob.disconnect(),this._ob=null),this._app&&this._app.unmount(),this._instance&&(this._instance.ce=void 0),this._app=this._instance=null,this._teleportTargets&&(this._teleportTargets.clear(),this._teleportTargets=void 0))})}_processMutations(e){for(let t of e)this._setAttr(t.attributeName)}_resolveDef(){if(this._pendingResolve)return;for(let e=0;e{let n;this._resolved=!0,this._pendingResolve=void 0;let{props:r,styles:i}=e;if(r&&!E(r))for(let e in r){let t=r[e];(t===Number||t&&t.type===Number)&&(e in this._props&&(this._props[e]=X(this._props[e])),(n||(n=Object.create(null)))[j(e)]=!0)}this._numberProps=n,this._resolveProps(e),this.shadowRoot&&this._applyStyles(i),this._mount(e)},t=this._def.__asyncLoader;t?this._pendingResolve=t().then(t=>{t.configureApp=this._def.configureApp,e(this._def=t,!0)}):e(this._def)}_mount(e){this._app=this._createApp(e),this._inheritParentContext(),e.configureApp&&e.configureApp(this._app),this._app._ceVNode=this._createVNode(),this._app.mount(this._root);let t=this._instance&&this._instance.exposed;if(t)for(let e in t)A(this,e)||Object.defineProperty(this,e,{get:()=>tT(t[e])})}_resolveProps(e){let{props:t}=e,n=E(t)?t:Object.keys(t||{});for(let e of Object.keys(this))"_"!==e[0]&&n.includes(e)&&this._setProp(e,this[e]);for(let e of n.map(j))Object.defineProperty(this,e,{get(){return this._getProp(e)},set(t){this._setProp(e,t,!0,!this._patching)}})}_setAttr(e){if(e.startsWith("data-v-"))return;let t=this.hasAttribute(e),n=t?this.getAttribute(e):lx,r=j(e);t&&this._numberProps&&this._numberProps[r]&&(n=X(n)),this._setProp(r,n,!1,!0)}_getProp(e){return this._props[e]}_setProp(e,t,n=!0,r=!1){if(t!==this._props[e]&&(this._dirty=!0,t===lx?delete this._props[e]:(this._props[e]=t,"key"===e&&this._app&&(this._app._ceVNode.key=t)),r&&this._instance&&this._update(),n)){let n=this._ob;n&&(this._processMutations(n.takeRecords()),n.disconnect()),!0===t?this.setAttribute(H(e),""):"string"==typeof t||"number"==typeof t?this.setAttribute(H(e),t+""):t||this.removeAttribute(H(e)),n&&n.observe(this,{attributes:!0})}}_update(){let e=this._createVNode();this._app&&(e.appContext=this._app._context),l2(e,this._root)}_createVNode(){let e={};this.shadowRoot||(e.onVnodeMounted=e.onVnodeUpdated=this._renderSlots.bind(this));let t=ip(this._def,T(e,this._props));return this._instance||(t.ce=e=>{this._instance=e,e.ce=this,e.isCE=!0;let t=(e,t)=>{let n;this.dispatchEvent(new CustomEvent(e,"[object Object]"===(n=t[0],F.call(n))?T({detail:t},t[0]):{detail:t}))};e.emit=(e,...n)=>{t(e,n),H(e)!==e&&t(H(e),n)},this._setParent()}),t}_applyStyles(e,t,n){if(!e)return;if(t){if(t===this._def||this._styleChildren.has(t))return;this._styleChildren.add(t)}let r=this._nonce,i=this.shadowRoot,l=n?this._getStyleAnchor(n)||this._getStyleAnchor(this._def):this._getRootStyleInsertionAnchor(i),s=null;for(let o=e.length-1;o>=0;o--){let a=document.createElement("style");r&&a.setAttribute("nonce",r),a.textContent=e[o],i.insertBefore(a,s||l),s=a,0===o&&(n||this._styleAnchors.set(this._def,a),t&&this._styleAnchors.set(t,a))}}_getStyleAnchor(e){if(!e)return null;let t=this._styleAnchors.get(e);return t&&t.parentNode===this.shadowRoot?t:(t&&this._styleAnchors.delete(e),null)}_getRootStyleInsertionAnchor(e){for(let t=0;t{if(!n.length)return;let t=e.moveClass||`${e.name||"v"}-move`;if(!function(e,t,n){let r=e.cloneNode(),i=e[iX];i&&i.forEach(e=>{e.split(/\s+/).forEach(e=>e&&r.classList.remove(e))}),n.split(/\s+/).forEach(e=>e&&r.classList.add(e)),r.style.display="none";let l=1===t.nodeType?t:t.parentNode;l.appendChild(r);let{hasTransform:s}=i9(r);return l.removeChild(r),s}(n[0].el,i.vnode.el,t)){n=[];return}n.forEach(lO),n.forEach(lM);let r=n.filter(lP);lt(i.vnode.el),r.forEach(e=>{let n=e.el,r=n.style;i3(n,t),r.transform=r.webkitTransform=r.transitionDuration="";let i=n[lE]=e=>{(!e||e.target===n)&&(!e||e.propertyName.endsWith("transform"))&&(n.removeEventListener("transitionend",i),n[lE]=null,i6(n,t))};n.addEventListener("transitionend",i)}),n=[]}),()=>{let s=tm(e),o=i2(s),a=s.tag||r4;if(n=[],r)for(let e=0;eMath.abs(s-1)&&(s=1),.01>Math.abs(o-1)&&(o=1),n.transform=n.webkitTransform=`translate(${r/s}px,${i/o}px)`,n.transitionDuration="0s",e}}function lF(e){let t=e.getBoundingClientRect();return{left:t.left,top:t.top}}let lL=e=>{let t=e.props["onUpdate:modelValue"]||!1;return E(t)?e=>z(t,e):t};function l$(e){e.target.composing=!0}function lD(e){let t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}let lV=Symbol("_assign");function lB(e,t,n){return t&&(e=e.trim()),n&&(e=G(e)),e}let lj={created(e,{modifiers:{lazy:t,trim:n,number:r}},i){e[lV]=lL(i);let l=r||i.props&&"number"===i.props.type;lg(e,t?"change":"input",t=>{t.target.composing||e[lV](lB(e.value,n,l))}),(n||l)&&lg(e,"change",()=>{e.value=lB(e.value,n,l)}),t||(lg(e,"compositionstart",l$),lg(e,"compositionend",lD),lg(e,"change",lD))},mounted(e,{value:t}){e.value=null==t?"":t},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:r,trim:i,number:l}},s){if(e[lV]=lL(s),e.composing)return;let o=(l||"number"===e.type)&&!/^0\d/.test(e.value)?G(e.value):e.value,a=null==t?"":t;if(o===a)return;let c=e.getRootNode();(c instanceof Document||c instanceof ShadowRoot)&&c.activeElement===e&&"range"!==e.type&&(r&&t===n||i&&e.value.trim()===a)||(e.value=a)}},lU={deep:!0,created(e,t,n){e[lV]=lL(n),lg(e,"change",()=>{let t=e._modelValue,n=lz(e),r=e.checked,i=e[lV];if(E(t)){let e=eh(t,n),l=-1!==e;if(r&&!l)i(t.concat(n));else if(!r&&l){let n=[...t];n.splice(e,1),i(n)}}else{let l;if("[object Set]"===(l=t,F.call(l))){let e=new Set(t);r?e.add(n):e.delete(n),i(e)}else i(lJ(e,r))}})},mounted:lH,beforeUpdate(e,t,n){e[lV]=lL(n),lH(e,t,n)}};function lH(e,{value:t,oldValue:n},r){let i;if(e._modelValue=t,E(t))i=eh(t,r.props.value)>-1;else{let l;if("[object Set]"===(l=t,F.call(l)))i=t.has(r.props.value);else{if(t===n)return;i=eu(t,lJ(e,!0))}}e.checked!==i&&(e.checked=i)}let lq={created(e,{value:t},n){e.checked=eu(t,n.props.value),e[lV]=lL(n),lg(e,"change",()=>{e[lV](lz(e))})},beforeUpdate(e,{value:t,oldValue:n},r){e[lV]=lL(r),t!==n&&(e.checked=eu(t,r.props.value))}},lW={deep:!0,created(e,{value:t,modifiers:{number:n}},r){let i,l="[object Set]"===(i=t,F.call(i));lg(e,"change",()=>{let t=Array.prototype.filter.call(e.options,e=>e.selected).map(e=>n?G(lz(e)):lz(e));e[lV](e.multiple?l?new Set(t):t:t[0]),e._assigning=!0,tz(()=>{e._assigning=!1})}),e[lV]=lL(r)},mounted(e,{value:t}){lK(e,t)},beforeUpdate(e,t,n){e[lV]=lL(n)},updated(e,{value:t}){e._assigning||lK(e,t)}};function lK(e,t){let n,r=e.multiple,i=E(t);if(!r||i||"[object Set]"===(n=t,F.call(n))){for(let n=0,l=e.options.length;nString(e)===String(s)):l.selected=eh(t,s)>-1}else l.selected=t.has(s);else if(eu(lz(l),t)){e.selectedIndex!==n&&(e.selectedIndex=n);return}}r||-1===e.selectedIndex||(e.selectedIndex=-1)}}function lz(e){return"_value"in e?e._value:e.value}function lJ(e,t){let n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}function lG(e,t,n,r,i){let l=function(e,t){switch(e){case"SELECT":return lW;case"TEXTAREA":return lj;default:switch(t){case"checkbox":return lU;case"radio":return lq;default:return lj}}}(e.tagName,n.props&&n.props.type)[i];l&&l(e,t,n,r)}let lX=["ctrl","shift","alt","meta"],lQ={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&0!==e.button,middle:e=>"button"in e&&1!==e.button,right:e=>"button"in e&&2!==e.button,exact:(e,t)=>lX.some(n=>e[`${n}Key`]&&!t.includes(n))},lZ={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},lY=T({patchProp:lS},iz),l0=!1;function l1(){return p=l0?p:rW(lY),l0=!0,p}let l2=(...e)=>{(p||(p=rK(lY))).render(...e)},l3=(...e)=>{let t=(p||(p=rK(lY))).createApp(...e),{mount:n}=t;return t.mount=e=>{let r=l8(e);if(!r)return;let i=t._component;I(i)||i.render||i.template||(i.template=r.innerHTML),1===r.nodeType&&(r.textContent="");let l=n(r,!1,l4(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),l},t},l6=(...e)=>{let t=l1().createApp(...e),{mount:n}=t;return t.mount=e=>{let t=l8(e);if(t)return n(t,!0,l4(t))},t};function l4(e){return e instanceof SVGElement?"svg":"function"==typeof MathMLElement&&e instanceof MathMLElement?"mathml":void 0}function l8(e){return R(e)?document.querySelector(e):e}let l5=Symbol(""),l9=Symbol(""),l7=Symbol(""),se=Symbol(""),st=Symbol(""),sn=Symbol(""),sr=Symbol(""),si=Symbol(""),sl=Symbol(""),ss=Symbol(""),so=Symbol(""),sa=Symbol(""),sc=Symbol(""),su=Symbol(""),sh=Symbol(""),sd=Symbol(""),sp=Symbol(""),sf=Symbol(""),sg=Symbol(""),sm=Symbol(""),sv=Symbol(""),sy=Symbol(""),sb=Symbol(""),s_=Symbol(""),sS=Symbol(""),sx=Symbol(""),sC=Symbol(""),sk=Symbol(""),sT=Symbol(""),sw=Symbol(""),sN=Symbol(""),sA=Symbol(""),sE=Symbol(""),sI=Symbol(""),sR=Symbol(""),sO=Symbol(""),sM=Symbol(""),sP=Symbol(""),sF=Symbol(""),sL={[l5]:"Fragment",[l9]:"Teleport",[l7]:"Suspense",[se]:"KeepAlive",[st]:"BaseTransition",[sn]:"openBlock",[sr]:"createBlock",[si]:"createElementBlock",[sl]:"createVNode",[ss]:"createElementVNode",[so]:"createCommentVNode",[sa]:"createTextVNode",[sc]:"createStaticVNode",[su]:"resolveComponent",[sh]:"resolveDynamicComponent",[sd]:"resolveDirective",[sp]:"resolveFilter",[sf]:"withDirectives",[sg]:"renderList",[sm]:"renderSlot",[sv]:"createSlots",[sy]:"toDisplayString",[sb]:"mergeProps",[s_]:"normalizeClass",[sS]:"normalizeStyle",[sx]:"normalizeProps",[sC]:"guardReactiveProps",[sk]:"toHandlers",[sT]:"camelize",[sw]:"capitalize",[sN]:"toHandlerKey",[sA]:"setBlockTracking",[sE]:"pushScopeId",[sI]:"popScopeId",[sR]:"withCtx",[sO]:"unref",[sM]:"isRef",[sP]:"withMemo",[sF]:"isMemoSame"},s$={start:{line:1,column:1,offset:0},end:{line:1,column:1,offset:0},source:""};function sD(e,t,n,r,i,l,s,o=!1,a=!1,c=!1,u=s$){var h,d,p,f;return e&&(o?(e.helper(sn),e.helper((h=e.inSSR,d=c,h||d?sr:si))):e.helper((p=e.inSSR,f=c,p||f?sl:ss)),s&&e.helper(sf)),{type:13,tag:t,props:n,children:r,patchFlag:i,dynamicProps:l,directives:s,isBlock:o,disableTracking:a,isComponent:c,loc:u}}function sV(e,t=s$){return{type:17,loc:t,elements:e}}function sB(e,t=s$){return{type:15,loc:t,properties:e}}function sj(e,t){return{type:16,loc:s$,key:R(e)?sU(e,!0):e,value:t}}function sU(e,t=!1,n=s$,r=0){return{type:4,loc:n,content:e,isStatic:t,constType:t?3:r}}function sH(e,t=s$){return{type:8,loc:t,children:e}}function sq(e,t=[],n=s$){return{type:14,loc:n,callee:e,arguments:t}}function sW(e,t,n=!1,r=!1,i=s$){return{type:18,params:e,returns:t,newline:n,isSlot:r,loc:i}}function sK(e,t,n,r=!0){return{type:19,test:e,consequent:t,alternate:n,newline:r,loc:s$}}function sz(e,{helper:t,removeHelper:n,inSSR:r}){if(!e.isBlock){var i,l;e.isBlock=!0,n((i=e.isComponent,r||i?sl:ss)),t(sn),t((l=e.isComponent,r||l?sr:si))}}let sJ=new Uint8Array([123,123]),sG=new Uint8Array([125,125]);function sX(e){return e>=97&&e<=122||e>=65&&e<=90}function sQ(e){return 32===e||10===e||9===e||12===e||13===e}function sZ(e){return 47===e||62===e||sQ(e)}function sY(e){let t=new Uint8Array(e.length);for(let n=0;n4===e.type&&e.isStatic;function s4(e){switch(e){case"Teleport":case"teleport":return l9;case"Suspense":case"suspense":return l7;case"KeepAlive":case"keep-alive":return se;case"BaseTransition":case"base-transition":return st}}let s8=/^$|^\d|[^\$\w\xA0-\uFFFF]/,s5=/[A-Za-z_$\xA0-\uFFFF]/,s9=/[\.\?\w$\xA0-\uFFFF]/,s7=/\s+[.[]\s*|\s*[.[]\s+/g,oe=e=>4===e.type?e.content:e.loc.source,ot=e=>{let t=oe(e).trim().replace(s7,e=>e.trim()),n=0,r=[],i=0,l=0,s=null;for(let e=0;e|^\s*(?:async\s+)?function(?:\s+[\w$]+)?\s*\(/;function or(e,t,n=!1){for(let r=0;r4===e.key.type&&e.key.content===r)}return n}function of(e,t){return`_${t}_${e.replace(/[^\w]/g,(t,n)=>"-"===t?"_":e.charCodeAt(n).toString())}`}let og=/([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/;function om(e){for(let t=0;t0,isVoidTag:x,isPreTag:x,isIgnoreNewlineTag:x,isCustomElement:x,onError:s1,onWarn:s2,comments:!1,prefixIdentifiers:!1},o_=ob,oS=null,ox="",oC=null,ok=null,oT="",ow=-1,oN=-1,oA=0,oE=!1,oI=null,oR=[],oO=new class{constructor(e,t){this.stack=e,this.cbs=t,this.state=1,this.buffer="",this.sectionStart=0,this.index=0,this.entityStart=0,this.baseState=1,this.inRCDATA=!1,this.inXML=!1,this.inVPre=!1,this.newlines=[],this.mode=0,this.delimiterOpen=sJ,this.delimiterClose=sG,this.delimiterIndex=-1,this.currentSequence=void 0,this.sequenceIndex=0}get inSFCRoot(){return 2===this.mode&&0===this.stack.length}reset(){this.state=1,this.mode=0,this.buffer="",this.sectionStart=0,this.index=0,this.baseState=1,this.inRCDATA=!1,this.currentSequence=void 0,this.newlines.length=0,this.delimiterOpen=sJ,this.delimiterClose=sG}getPos(e){let t=1,n=e+1,r=this.newlines.length,i=-1;if(r>100){let t=-1,n=r;for(;t+1>>1;this.newlines[r]=0;t--)if(e>this.newlines[t]){i=t;break}return i>=0&&(t=i+2,n=e-this.newlines[i]),{column:n,line:t,offset:e}}peek(){return this.buffer.charCodeAt(this.index+1)}stateText(e){60===e?(this.index>this.sectionStart&&this.cbs.ontext(this.sectionStart,this.index),this.state=5,this.sectionStart=this.index):this.inVPre||e!==this.delimiterOpen[0]||(this.state=2,this.delimiterIndex=0,this.stateInterpolationOpen(e))}stateInterpolationOpen(e){if(e===this.delimiterOpen[this.delimiterIndex])if(this.delimiterIndex===this.delimiterOpen.length-1){let e=this.index+1-this.delimiterOpen.length;e>this.sectionStart&&this.cbs.ontext(this.sectionStart,e),this.state=3,this.sectionStart=e}else this.delimiterIndex++;else this.inRCDATA?(this.state=32,this.stateInRCDATA(e)):(this.state=1,this.stateText(e))}stateInterpolation(e){e===this.delimiterClose[0]&&(this.state=4,this.delimiterIndex=0,this.stateInterpolationClose(e))}stateInterpolationClose(e){e===this.delimiterClose[this.delimiterIndex]?this.delimiterIndex===this.delimiterClose.length-1?(this.cbs.oninterpolation(this.sectionStart,this.index+1),this.inRCDATA?this.state=32:this.state=1,this.sectionStart=this.index+1):this.delimiterIndex++:(this.state=3,this.stateInterpolation(e))}stateSpecialStartSequence(e){let t=this.sequenceIndex===this.currentSequence.length;if(t?sZ(e):(32|e)===this.currentSequence[this.sequenceIndex]){if(!t)return void this.sequenceIndex++}else this.inRCDATA=!1;this.sequenceIndex=0,this.state=6,this.stateInTagName(e)}stateInRCDATA(e){if(this.sequenceIndex===this.currentSequence.length){if(62===e||sQ(e)){let t=this.index-this.currentSequence.length;if(this.sectionStart=e||(28===this.state?this.currentSequence===s0.CdataEnd?this.cbs.oncdata(this.sectionStart,e):this.cbs.oncomment(this.sectionStart,e):6===this.state||11===this.state||18===this.state||17===this.state||12===this.state||13===this.state||14===this.state||15===this.state||16===this.state||20===this.state||19===this.state||21===this.state||9===this.state||this.cbs.ontext(this.sectionStart,e))}emitCodePoint(e,t){}}(oR,{onerr:oJ,ontext(e,t){o$(oF(e,t),e,t)},ontextentity(e,t,n){o$(e,t,n)},oninterpolation(e,t){if(oE)return o$(oF(e,t),e,t);let n=e+oO.delimiterOpen.length,r=t-oO.delimiterClose.length;for(;sQ(ox.charCodeAt(n));)n++;for(;sQ(ox.charCodeAt(r-1));)r--;let i=oF(n,r);i.includes("&")&&(i=o_.decodeEntities(i,!1)),oq({type:5,content:oz(i,!1,oW(n,r)),loc:oW(e,t)})},onopentagname(e,t){let n=oF(e,t);oC={type:1,tag:n,ns:o_.getNamespace(n,oR[0],o_.ns),tagType:0,props:[],children:[],loc:oW(e-1,t),codegenNode:void 0}},onopentagend(e){oL(e)},onclosetag(e,t){let n=oF(e,t);if(!o_.isVoidTag(n)){let r=!1;for(let e=0;e0&&oR[0].loc.start.offset;for(let n=0;n<=e;n++)oD(oR.shift(),t,n(7===e.type?e.rawName:e.name)===t)},onattribend(e,t){oC&&ok&&(oK(ok.loc,t),0!==e&&(oT.includes("&")&&(oT=o_.decodeEntities(oT,!0)),6===ok.type?("class"===ok.name&&(oT=oH(oT).trim()),ok.value={type:2,content:oT,loc:1===e?oW(ow,oN):oW(ow-1,oN+1)},oO.inSFCRoot&&"template"===oC.tag&&"lang"===ok.name&&oT&&"html"!==oT&&oO.enterRCDATA(sY("{let i=t.start.offset+n,l=i+e.length;return oz(e,!1,oW(i,l),0,+!!r)},o={source:s(l.trim(),n.indexOf(l,i.length)),value:void 0,key:void 0,index:void 0,finalized:!1},a=i.trim().replace(oP,"").trim(),c=i.indexOf(a),u=a.match(oM);if(u){let e;a=a.replace(oM,"").trim();let t=u[1].trim();if(t&&(e=n.indexOf(t,c+a.length),o.key=s(t,e,!0)),u[2]){let r=u[2].trim();r&&(o.index=s(r,n.indexOf(r,o.key?e+t.length:c+a.length),!0))}}return a&&(o.value=s(a,c,!0)),o}(ok.exp)))),(7!==ok.type||"pre"!==ok.name)&&oC.props.push(ok)),oT="",ow=oN=-1},oncomment(e,t){o_.comments&&oq({type:3,content:oF(e,t),loc:oW(e-4,t+3)})},onend(){let e=ox.length;for(let t=0;t64&&n<91||s4(e)||o_.isBuiltInComponent&&o_.isBuiltInComponent(e)||o_.isNativeTag&&!o_.isNativeTag(e))return!0;for(let e=0;e=0;)n--;return n}let oB=new Set(["if","else","else-if","for","slot"]),oj=/\r\n/g;function oU(e){let t="preserve"!==o_.whitespace,n=!1;for(let r=0;r3!==e.type);return 1!==t.length||1!==t[0].type||ou(t[0])?null:t[0]}function oX(e,t){let{constantCache:n}=t;switch(e.type){case 1:if(0!==e.tagType)return 0;let r=n.get(e);if(void 0!==r)return r;let i=e.codegenNode;if(13!==i.type||i.isBlock&&"svg"!==e.tag&&"foreignObject"!==e.tag&&"math"!==e.tag)return 0;if(void 0!==i.patchFlag)return n.set(e,0),0;{let r=3,c=oZ(e,t);if(0===c)return n.set(e,0),0;c1)for(let i=0;i{l--};for(;lt===e:t=>e.test(t);return(e,r)=>{if(1===e.type){let{props:i}=e;if(3===e.tagType&&i.some(oa))return;let l=[];for(let s=0;s`${sL[e]}: _${sL[e]}`;function o6(e,t,{helper:n,push:r,newline:i,isTS:l}){let s=n("component"===t?su:sd);for(let n=0;n3;t.push("["),n&&t.indent(),o8(e,t,n),n&&t.deindent(),t.push("]")}function o8(e,t,n=!1,r=!0){let{push:i,newline:l}=t;for(let s=0;se||"null")}([a,c,u,i,d]),t),l(")"),f&&l(")"),p&&(l(", "),o5(p,t),l(")"))}(e,t);break;case 14:!function(e,t){let{push:n,helper:r,pure:i}=t,l=R(e.callee)?e.callee:r(e.callee);i&&n(o2),n(l+"(",-2,e),o8(e.arguments,t),n(")")}(e,t);break;case 15:!function(e,t){let{push:n,indent:r,deindent:i,newline:l}=t,{properties:s}=e;if(!s.length)return n("{}",-2,e);let o=s.length>1;n(o?"{":"{ "),o&&r();for(let e=0;e "),(a||o)&&(n("{"),r()),s?(a&&n("return "),E(s)?o4(s,t):o5(s,t)):o&&o5(o,t),(a||o)&&(i(),n("}")),c&&n(")")}(e,t);break;case 19:!function(e,t){let{test:n,consequent:r,alternate:i,newline:l}=e,{push:s,indent:o,deindent:a,newline:c}=t;if(4===n.type){let e,r=(e=n.content,!!s8.test(e));r&&s("("),o9(n,t),r&&s(")")}else s("("),o5(n,t),s(")");l&&o(),t.indentLevel++,l||s(" "),s("? "),o5(r,t),t.indentLevel--,l&&c(),l||s(" "),s(": ");let u=19===i.type;!u&&t.indentLevel++,o5(i,t),!u&&t.indentLevel--,l&&a(!0)}(e,t);break;case 20:!function(e,t){let{push:n,helper:r,indent:i,deindent:l,newline:s}=t,{needPauseTracking:o,needArraySpread:a}=e;a&&n("[...("),n(`_cache[${e.index}] || (`),o&&(i(),n(`${r(sA)}(-1`),e.inVOnce&&n(", true"),n("),"),s(),n("(")),n(`_cache[${e.index}] = `),o5(e.value,t),o&&(n(`).cacheIndex = ${e.index},`),s(),n(`${r(sA)}(1),`),s(),n(`_cache[${e.index}]`),l()),n(")"),a&&n(")]")}(e,t);break;case 21:o8(e.body,t,!0,!1)}}function o9(e,t){let{content:n,isStatic:r}=e;t.push(r?JSON.stringify(n):n,-3,e)}function o7(e,t){for(let n=0;n(function(e,t,n,r){if("else"!==t.name&&(!t.exp||!t.exp.content.trim())){let r=t.exp?t.exp.loc:e.loc;n.onError(s3(28,t.loc)),t.exp=sU("true",!1,r)}if("if"===t.name){var i;let l=at(e,t),s={type:9,loc:oW((i=e.loc).start.offset,i.end.offset),branches:[l]};if(n.replaceNode(s),r)return r(s,l,!0)}else{let i=n.parent.children,l=i.indexOf(e);for(;l-- >=-1;){let s=i[l];if(s&&oy(s)){n.removeNode(s);continue}if(s&&9===s.type){("else-if"===t.name||"else"===t.name)&&void 0===s.branches[s.branches.length-1].condition&&n.onError(s3(30,e.loc)),n.removeNode();let i=at(e,t);s.branches.push(i);let l=r&&r(s,i,!1);o0(i,n),l&&l(),n.currentNode=null}else n.onError(s3(30,e.loc));break}}})(e,t,n,(e,t,r)=>{let i=n.parent.children,l=i.indexOf(e),s=0;for(;l-- >=0;){let e=i[l];e&&9===e.type&&(s+=e.branches.length)}return()=>{r?e.codegenNode=an(t,s,n):function(e){for(;;)if(19===e.type)if(19!==e.alternate.type)return e;else e=e.alternate;else 20===e.type&&(e=e.value)}(e.codegenNode).alternate=an(t,s+e.branches.length-1,n)}}));function at(e,t){let n=3===e.tagType;return{type:10,loc:e.loc,condition:"else"===t.name?void 0:t.exp,children:n&&!or(e,"for")?e.children:[e],userKey:oi(e,"key"),isTemplateIf:n}}function an(e,t,n){return e.condition?sK(e.condition,ar(e,t,n),sq(n.helper(so),['""',"true"])):ar(e,t,n)}function ar(e,t,n){let{helper:r}=n,i=sj("key",sU(`${t}`,!1,s$,2)),{children:l}=e,s=l[0];if(1!==l.length||1!==s.type)if(1!==l.length||11!==s.type)return sD(n,r(l5),sB([i]),l,64,void 0,void 0,!0,!1,!1,e.loc);else{let e=s.codegenNode;return od(e,i,n),e}{let e=s.codegenNode,t=14===e.type&&e.callee===sP?e.arguments[1].returns:e;return 13===t.type&&sz(t,n),od(t,i,n),e}}let ai=o1("for",(e,t,n)=>{let{helper:r,removeHelper:i}=n;return function(e,t,n,r){if(!t.exp)return void n.onError(s3(31,t.loc));let i=t.forParseResult;if(!i)return void n.onError(s3(32,t.loc));al(i);let{scopes:l}=n,{source:s,value:o,key:a,index:c}=i,u={type:11,loc:t.loc,source:s,valueAlias:o,keyAlias:a,objectIndexAlias:c,parseResult:i,children:oc(e)?e.children:[e]};n.replaceNode(u),l.vFor++;let h=r&&r(u);return()=>{l.vFor--,h&&h()}}(e,t,n,t=>{let l=sq(r(sg),[t.source]),s=oc(e),o=or(e,"memo"),a=oi(e,"key",!1,!0);a&&a.type;let c=a&&(6===a.type?a.value?sU(a.value.content,!0):void 0:a.exp),u=a&&c?sj("key",c):null,h=4===t.source.type&&t.source.constType>0,d=h?64:a?128:256;return t.codegenNode=sD(n,r(l5),void 0,l,d,void 0,void 0,!0,!h,!1,e.loc),()=>{let a,{children:d}=t,p=1!==d.length||1!==d[0].type,f=ou(e)?e:s&&1===e.children.length&&ou(e.children[0])?e.children[0]:null;if(f)a=f.codegenNode,s&&u&&od(a,u,n);else if(p)a=sD(n,r(l5),u?sB([u]):void 0,e.children,64,void 0,void 0,!0,void 0,!1);else{var g,m,y,b,_,S,x,C;a=d[0].codegenNode,s&&u&&od(a,u,n),!h!==a.isBlock&&(a.isBlock?(i(sn),i((g=n.inSSR,m=a.isComponent,g||m?sr:si))):i((y=n.inSSR,b=a.isComponent,y||b?sl:ss))),(a.isBlock=!h,a.isBlock)?(r(sn),r((_=n.inSSR,S=a.isComponent,_||S?sr:si))):r((x=n.inSSR,C=a.isComponent,x||C?sl:ss))}if(o){let e=sW(as(t.parseResult,[sU("_cached")]));e.body={type:21,body:[sH(["const _memo = (",o.exp,")"]),sH(["if (_cached && _cached.el",...c?[" && _cached.key === ",c]:[],` && ${n.helperString(sF)}(_cached, _memo)) return _cached`]),sH(["const _item = ",a]),sU("_item.memo = _memo"),sU("return _item")],loc:s$},l.arguments.push(e,sU("_cache"),sU(String(n.cached.length))),n.cached.push(null)}else l.arguments.push(sW(as(t.parseResult),a,!0))}})});function al(e,t){e.finalized||(e.finalized=!0)}function as({value:e,key:t,index:n},r=[]){var i=[e,t,n,...r];let l=i.length;for(;l--&&!i[l];);return i.slice(0,l+1).map((e,t)=>e||sU("_".repeat(t+1),!1))}let ao=sU("undefined",!1),aa=(e,t)=>{if(1===e.type&&(1===e.tagType||3===e.tagType)){let n=or(e,"slot");if(n)return n.exp,t.scopes.vSlot++,()=>{t.scopes.vSlot--}}};function ac(e,t,n){let r=[sj("name",e),sj("fn",t)];return null!=n&&r.push(sj("key",sU(String(n),!0))),sB(r)}let au=new WeakMap,ah=(e,t)=>function(){let n,r,i,l,s;if(1!==(e=t.currentNode).type||0!==e.tagType&&1!==e.tagType)return;let{tag:o,props:a}=e,c=1===e.tagType,u=c?function(e,t,n=!1){let{tag:r}=e,i=af(r),l=oi(e,"is",!1,!0);if(l)if(i){let e;if(6===l.type?e=l.value&&sU(l.value.content,!0):(e=l.exp)||(e=sU("is",!1,l.arg.loc)),e)return sq(t.helper(sh),[e])}else 6===l.type&&l.value.content.startsWith("vue:")&&(r=l.value.content.slice(4));let s=s4(r)||t.isBuiltInComponent(r);return s?(n||t.helper(s),s):(t.helper(su),t.components.add(r),of(r,"component"))}(e,t):`"${o}"`,h=M(u)&&u.callee===sh,d=0,p=h||u===l9||u===l7||!c&&("svg"===o||"foreignObject"===o||"math"===o);if(a.length>0){let r=ad(e,t,void 0,c,h);n=r.props,d=r.patchFlag,l=r.dynamicPropNames;let i=r.directives;s=i&&i.length?sV(i.map(e=>(function(e,t){let n=[],r=au.get(e);r?n.push(t.helperString(r)):(t.helper(sd),t.directives.add(e.name),n.push(of(e.name,"directive")));let{loc:i}=e;if(e.exp&&n.push(e.exp),e.arg&&(e.exp||n.push("void 0"),n.push(e.arg)),Object.keys(e.modifiers).length){e.arg||(e.exp||n.push("void 0"),n.push("void 0"));let t=sU("true",!1,i);n.push(sB(e.modifiers.map(e=>sj(e,t)),i))}return sV(n,e.loc)})(e,t))):void 0,r.shouldUseBlock&&(p=!0)}if(e.children.length>0)if(u===se&&(p=!0,d|=1024),c&&u!==l9&&u!==se){let{slots:n,hasDynamicSlots:i}=function(e,t,n=(e,t,n,r)=>sW(e,n,!1,!0,n.length?n[0].loc:r)){t.helper(sR);let{children:r,loc:i}=e,l=[],s=[],o=t.scopes.vSlot>0||t.scopes.vFor>0,a=or(e,"slot",!0);if(a){let{arg:e,exp:t}=a;e&&!s6(e)&&(o=!0),l.push(sj(e||sU("default",!0),n(t,void 0,r,i)))}let c=!1,u=!1,h=[],d=new Set,p=0;for(let e=0;esj("default",n(e,void 0,t,i));c?h.length&&!h.every(ov)&&(u?t.onError(s3(39,h[0].loc)):l.push(e(void 0,h))):l.push(e(void 0,r))}let f=o?2:!function e(t){for(let n=0;n0,f=!1,g=0,m=!1,y=!1,b=!1,_=!1,S=!1,x=!1,k=[],T=e=>{u.length&&(h.push(sB(ap(u),a)),u=[]),e&&h.push(e)},w=()=>{t.scopes.vFor>0&&u.push(sj(sU("ref_for",!0),sU("true")))},N=({key:e,value:n})=>{if(s6(e)){let l=e.content,s=C(l);s&&(!r||i)&&"onclick"!==l.toLowerCase()&&"onUpdate:modelValue"!==l&&!$(l)&&(_=!0),s&&$(l)&&(x=!0),s&&14===n.type&&(n=n.arguments[0]),20===n.type||(4===n.type||8===n.type)&&oX(n,t)>0||("ref"===l?m=!0:"class"===l?y=!0:"style"===l?b=!0:"key"===l||k.includes(l)||k.push(l),r&&("class"===l||"style"===l)&&!k.includes(l)&&k.push(l))}else S=!0};for(let i=0;i"prop"===e.content)&&(g|=32);let x=t.directiveTransforms[n];if(x){let{props:n,needRuntime:r}=x(s,e,t);l||n.forEach(N),_&&i&&!s6(i)?T(sB(n,a)):u.push(...n),r&&(d.push(s),O(r)&&au.set(s,r))}else!D(n)&&(d.push(s),p&&(f=!0))}}if(h.length?(T(),s=h.length>1?sq(t.helper(sb),h,a):h[0]):u.length&&(s=sB(ap(u),a)),S?g|=16:(y&&!r&&(g|=2),b&&!r&&(g|=4),k.length&&(g|=8),_&&(g|=32)),!f&&(0===g||32===g)&&(m||x||d.length>0)&&(g|=512),!t.inSSR&&s)switch(s.type){case 15:let A=-1,E=-1,I=!1;for(let e=0;e{if(ou(e)){let{children:n,loc:r}=e,{slotName:i,slotProps:l}=function(e,t){let n,r='"default"',i=[];for(let t=0;t0){let{props:r,directives:l}=ad(e,t,i,!1,!1);n=r,l.length&&t.onError(s3(36,l[0].loc))}return{slotName:r,slotProps:n}}(e,t),s=[t.prefixIdentifiers?"_ctx.$slots":"$slots",i,"{}","undefined","true"],o=2;l&&(s[2]=l,o=3),n.length&&(s[3]=sW([],n,!1,!1,r),o=4),t.scopeId&&!t.slotted&&(o=5),s.splice(o),e.codegenNode=sq(t.helper(sm),s,r)}},am=(e,t,n,r)=>{let i,{loc:l,modifiers:s,arg:o}=e;if(!e.exp&&!s.length,4===o.type)if(o.isStatic){let e=o.content;e.startsWith("vue:")&&(e=`vnode-${e.slice(4)}`),i=sU(0!==t.tagType||e.startsWith("vnode")||!/[A-Z]/.test(e)?W(j(e)):`on:${e}`,!0,o.loc)}else i=sH([`${n.helperString(sN)}(`,o,")"]);else(i=o).children.unshift(`${n.helperString(sN)}(`),i.children.push(")");let a=e.exp;a&&!a.content.trim()&&(a=void 0);let c=n.cacheHandlers&&!a&&!n.inVOnce;if(a){let e,t=ot(a),n=!(t||(e=a,on.test(oe(e)))),r=a.content.includes(";");(n||c&&t)&&(a=sH([`${n?"$event":"(...args)"} => ${r?"{":"("}`,a,r?"}":")"]))}let u={props:[sj(i,a||sU("() => {}",!1,l))]};return r&&(u=r(u)),c&&(u.props[0].value=n.cache(u.props[0].value)),u.props.forEach(e=>e.key.isHandlerKey=!0),u},av=(e,t,n)=>{let{modifiers:r}=e,i=e.arg,{exp:l}=e;return l&&4===l.type&&!l.content.trim()&&(l=void 0),4!==i.type?(i.children.unshift("("),i.children.push(') || ""')):i.isStatic||(i.content=i.content?`${i.content} || ""`:'""'),r.some(e=>"camel"===e.content)&&(4===i.type?i.isStatic?i.content=j(i.content):i.content=`${n.helperString(sT)}(${i.content})`:(i.children.unshift(`${n.helperString(sT)}(`),i.children.push(")"))),!n.inSSR&&(r.some(e=>"prop"===e.content)&&ay(i,"."),r.some(e=>"attr"===e.content)&&ay(i,"^")),{props:[sj(i,l)]}},ay=(e,t)=>{4===e.type?e.isStatic?e.content=t+e.content:e.content=`\`${t}\${${e.content}}\``:(e.children.unshift(`'${t}' + (`),e.children.push(")"))},ab=(e,t)=>{if(0===e.type||1===e.type||11===e.type||10===e.type)return()=>{let n,r=e.children,i=!1;for(let e=0;e7===e.type&&!t.directiveTransforms[e.name]))))for(let e=0;e{if(1===e.type&&or(e,"once",!0)&&!a_.has(e)&&!t.inVOnce&&!t.inSSR)return a_.add(e),t.inVOnce=!0,t.helper(sA),()=>{t.inVOnce=!1;let e=t.currentNode;e.codegenNode&&(e.codegenNode=t.cache(e.codegenNode,!0,!0))}},ax=(e,t,n)=>{let r,{exp:i,arg:l}=e;if(!i)return n.onError(s3(41,e.loc)),aC();let s=i.loc.source.trim(),o=4===i.type?i.content:s,a=n.bindingMetadata[s];if("props"===a||"props-aliased"===a||"literal-const"===a||"setup-const"===a)return i.loc,aC();if(!o.trim()||!ot(i))return n.onError(s3(42,i.loc)),aC();let c=l||sU("modelValue",!0),u=l?s6(l)?`onUpdate:${j(l.content)}`:sH(['"onUpdate:" + ',l]):"onUpdate:modelValue",h=n.isTS?"($event: any)":"$event";r=sH([`${h} => ((`,i,") = $event)"]);let d=[sj(c,e.exp),sj(u,r)];if(e.modifiers.length&&1===t.tagType){let t=e.modifiers.map(e=>e.content).map(e=>(s8.test(e)?JSON.stringify(e):e)+": true").join(", "),n=l?s6(l)?`${l.content}Modifiers`:sH([l,' + "Modifiers"']):"modelModifiers";d.push(sj(n,sU(`{ ${t} }`,!1,e.loc,2)))}return aC(d)};function aC(e=[]){return{props:e}}let ak=new WeakSet,aT=(e,t)=>{if(1===e.type){let n=or(e,"memo");if(!(!n||ak.has(e))&&!t.inSSR)return ak.add(e),()=>{let r=e.codegenNode||t.currentNode.codegenNode;r&&13===r.type&&(1!==e.tagType&&sz(r,t),e.codegenNode=sq(t.helper(sP),[n.exp,sW(void 0,r),"_cache",String(t.cached.length)]),t.cached.push(null))}}},aw=(e,t)=>{if(1===e.type){for(let n of e.props)if(7===n.type&&"bind"===n.name&&(!n.exp||4===n.exp.type&&!n.exp.content.trim())&&n.arg){let e=n.arg;if(4===e.type&&e.isStatic){let t=j(e.content);(s5.test(t[0])||"-"===t[0])&&(n.exp=sU(t,!1,e.loc))}else t.onError(s3(53,e.loc)),n.exp=sU("",!0,e.loc)}}},aN=Symbol(""),aA=Symbol(""),aE=Symbol(""),aI=Symbol(""),aR=Symbol(""),aO=Symbol(""),aM=Symbol(""),aP=Symbol(""),aF=Symbol(""),aL=Symbol("");Object.getOwnPropertySymbols(r={[aN]:"vModelRadio",[aA]:"vModelCheckbox",[aE]:"vModelText",[aI]:"vModelSelect",[aR]:"vModelDynamic",[aO]:"withModifiers",[aM]:"withKeys",[aP]:"vShow",[aF]:"Transition",[aL]:"TransitionGroup"}).forEach(e=>{sL[e]=r[e]});let a$={parseMode:"html",isVoidTag:ea,isNativeTag:e=>el(e)||es(e)||eo(e),isPreTag:e=>"pre"===e,isIgnoreNewlineTag:e=>"pre"===e||"textarea"===e,decodeEntities:function(e,t=!1){return(f||(f=document.createElement("div")),t)?(f.innerHTML=`
`,f.children[0].getAttribute("foo")):(f.innerHTML=e,f.textContent)},isBuiltInComponent:e=>"Transition"===e||"transition"===e?aF:"TransitionGroup"===e||"transition-group"===e?aL:void 0,getNamespace(e,t,n){let r=t?t.ns:n;if(t&&2===r)if("annotation-xml"===t.tag){if("svg"===e)return 1;t.props.some(e=>6===e.type&&"encoding"===e.name&&null!=e.value&&("text/html"===e.value.content||"application/xhtml+xml"===e.value.content))&&(r=0)}else/^m(?:[ions]|text)$/.test(t.tag)&&"mglyph"!==e&&"malignmark"!==e&&(r=0);else t&&1===r&&("foreignObject"===t.tag||"desc"===t.tag||"title"===t.tag)&&(r=0);if(0===r){if("svg"===e)return 1;if("math"===e)return 2}return r}},aD=y("passive,once,capture"),aV=y("stop,prevent,self,ctrl,shift,alt,meta,exact,middle"),aB=y("left,right"),aj=y("onkeyup,onkeydown,onkeypress"),aU=(e,t)=>s6(e)&&"onclick"===e.content.toLowerCase()?sU(t,!0):4!==e.type?sH(["(",e,`) === "onClick" ? "${t}" : (`,e,")"]):e,aH=(e,t)=>{1===e.type&&0===e.tagType&&("script"===e.tag||"style"===e.tag)&&t.removeNode()},aq=[e=>{1===e.type&&e.props.forEach((t,n)=>{let r,i;6===t.type&&"style"===t.name&&t.value&&(e.props[n]={type:7,name:"bind",arg:sU("style",!0,t.loc),exp:(r=t.value.content,i=t.loc,sU(JSON.stringify(er(r)),!1,i,3)),modifiers:[],loc:t.loc})})}],aW={cloak:()=>({props:[]}),html:(e,t,n)=>{let{exp:r,loc:i}=e;return r||n.onError(s3(54,i)),t.children.length&&(n.onError(s3(55,i)),t.children.length=0),{props:[sj(sU("innerHTML",!0,i),r||sU("",!0))]}},text:(e,t,n)=>{let{exp:r,loc:i}=e;return r||n.onError(s3(56,i)),t.children.length&&(n.onError(s3(57,i)),t.children.length=0),{props:[sj(sU("textContent",!0),r?oX(r,n)>0?r:sq(n.helperString(sy),[r],i):sU("",!0))]}},model:(e,t,n)=>{let r=ax(e,t,n);if(!r.props.length||1===t.tagType)return r;e.arg&&n.onError(s3(59,e.arg.loc));let{tag:i}=t,l=n.isCustomElement(i);if("input"===i||"textarea"===i||"select"===i||l){let s=aE,o=!1;if("input"===i||l){let r=oi(t,"type");if(r){if(7===r.type)s=aR;else if(r.value)switch(r.value.content){case"radio":s=aN;break;case"checkbox":s=aA;break;case"file":o=!0,n.onError(s3(60,e.loc))}}else t.props.some(e=>7===e.type&&"bind"===e.name&&(!e.arg||4!==e.arg.type||!e.arg.isStatic))&&(s=aR)}else"select"===i&&(s=aI);o||(r.needRuntime=n.helper(s))}else n.onError(s3(58,e.loc));return r.props=r.props.filter(e=>4!==e.key.type||"modelValue"!==e.key.content),r},on:(e,t,n)=>am(e,t,n,t=>{let{modifiers:r}=e;if(!r.length)return t;let{key:i,value:l}=t.props[0],{keyModifiers:s,nonKeyModifiers:o,eventOptionModifiers:a}=((e,t,n,r)=>{let i=[],l=[],s=[];for(let n=0;n{let{exp:r,loc:i}=e;return r||n.onError(s3(62,i)),{props:[],needRuntime:n.helper(aP)}}},aK=Object.create(null);function az(e,t){if(!R(e))if(!e.nodeType)return S;else e=e.innerHTML;let n=e+JSON.stringify(t,(e,t)=>"function"==typeof t?t.toString():t),r=aK[n];if(r)return r;if("#"===e[0]){let t=document.querySelector(e);e=t?t.innerHTML:""}let i=T({hoistStatic:!0,onError:void 0,onWarn:S},t);!i.isCustomElement&&"u">typeof customElements&&(i.isCustomElement=e=>!!customElements.get(e));let{code:l}=function(e,t={}){return function(e,t={}){var n;let r,i=t.onError||s1,l="module"===t.mode;!0===t.prefixIdentifiers?i(s3(48)):l&&i(s3(49)),t.cacheHandlers&&i(s3(50)),t.scopeId&&!l&&i(s3(51));let s=T({},t,{prefixIdentifiers:!1}),o=R(e)?function(e,t){if(oO.reset(),oC=null,ok=null,oT="",ow=-1,oN=-1,oR.length=0,ox=e,o_=T({},ob),t){let e;for(e in t)null!=t[e]&&(o_[e]=t[e])}oO.mode="html"===o_.parseMode?1:2*("sfc"===o_.parseMode),oO.inXML=1===o_.ns||2===o_.ns;let n=t&&t.delimiters;n&&(oO.delimiterOpen=sY(n[0]),oO.delimiterClose=sY(n[1]));let r=oS=function(e,t=""){return{type:0,source:t,children:e,helpers:new Set,components:[],directives:[],hoists:[],imports:[],cached:[],temps:0,codegenNode:void 0,loc:s$}}([],e);return oO.parse(ox),r.loc=oW(0,e.length),r.children=oU(r.children),oS=null,r}(e,s):e,[a,c]=[[aw,aS,ae,aT,ai,ag,ah,aa,ab],{on:am,bind:av,model:ax}];return r=function(e,{filename:t="",prefixIdentifiers:n=!1,hoistStatic:r=!1,hmr:i=!1,cacheHandlers:l=!1,nodeTransforms:s=[],directiveTransforms:o={},transformHoist:a=null,isBuiltInComponent:c=S,isCustomElement:u=S,expressionPlugins:h=[],scopeId:d=null,slotted:p=!0,ssr:f=!1,inSSR:g=!1,ssrCssVars:m="",bindingMetadata:y=b,inline:_=!1,isTS:x=!1,onError:C=s1,onWarn:k=s2,compatConfig:T}){let w=t.replace(/\?.*$/,"").match(/([^/\\]+)\.\w+$/),N={filename:t,selfName:w&&q(j(w[1])),prefixIdentifiers:n,hoistStatic:r,hmr:i,cacheHandlers:l,nodeTransforms:s,directiveTransforms:o,transformHoist:a,isBuiltInComponent:c,isCustomElement:u,expressionPlugins:h,scopeId:d,slotted:p,ssr:f,inSSR:g,ssrCssVars:m,bindingMetadata:y,inline:_,isTS:x,onError:C,onWarn:k,compatConfig:T,root:e,helpers:new Map,components:new Set,directives:new Set,hoists:[],imports:[],cached:[],constantCache:new WeakMap,temps:0,identifiers:Object.create(null),scopes:{vFor:0,vSlot:0,vPre:0,vOnce:0},parent:null,grandParent:null,currentNode:e,childIndex:0,inVOnce:!1,helper(e){let t=N.helpers.get(e)||0;return N.helpers.set(e,t+1),e},removeHelper(e){let t=N.helpers.get(e);if(t){let n=t-1;n?N.helpers.set(e,n):N.helpers.delete(e)}},helperString:e=>`_${sL[N.helper(e)]}`,replaceNode(e){N.parent.children[N.childIndex]=N.currentNode=e},removeNode(e){let t=N.parent.children,n=e?t.indexOf(e):N.currentNode?N.childIndex:-1;e&&e!==N.currentNode?N.childIndex>n&&(N.childIndex--,N.onNodeRemoved()):(N.currentNode=null,N.onNodeRemoved()),N.parent.children.splice(n,1)},onNodeRemoved:S,addIdentifiers(e){},removeIdentifiers(e){},hoist(e){R(e)&&(e=sU(e)),N.hoists.push(e);let t=sU(`_hoisted_${N.hoists.length}`,!1,e.loc,2);return t.hoisted=e,t},cache(e,t=!1,n=!1){let r=function(e,t,n=!1,r=!1){return{type:20,index:e,value:t,needPauseTracking:n,inVOnce:r,needArraySpread:!1,loc:s$}}(N.cached.length,e,t,n);return N.cached.push(r),r}};return N}(o,n=T({},s,{nodeTransforms:[...a,...t.nodeTransforms||[]],directiveTransforms:T({},c,t.directiveTransforms||{})})),o0(o,r),n.hoistStatic&&function e(t,n,r,i=!1,l=!1){let{children:s}=t,o=[];for(let n=0;n0){if(e>=2){a.codegenNode.patchFlag=-1,o.push(a);continue}}else{let e=a.codegenNode;if(13===e.type){let t=e.patchFlag;if((void 0===t||512===t||1===t)&&oZ(a,r)>=2){let t=oY(a);t&&(e.props=r.hoist(t))}e.dynamicProps&&(e.dynamicProps=r.hoist(e.dynamicProps))}}}else if(12===a.type&&(i?0:oX(a,r))>=2){14===a.codegenNode.type&&a.codegenNode.arguments.length>0&&a.codegenNode.arguments.push("-1"),o.push(a);continue}if(1===a.type){let n=1===a.tagType;n&&r.scopes.vSlot++,e(a,t,r,!1,l),n&&r.scopes.vSlot--}else if(11===a.type)e(a,t,r,1===a.children.length,!0);else if(9===a.type)for(let n=0;ne.key===t||e.key.content===t);return n&&n.value}}o.length&&r.transformHoist&&r.transformHoist(s,r,t)}(o,void 0,r,!!oG(o)),n.ssr||function(e,t){let{helper:n}=t,{children:r}=e;if(1===r.length){let n=oG(e);if(n&&n.codegenNode){let r=n.codegenNode;13===r.type&&sz(r,t),e.codegenNode=r}else e.codegenNode=r[0]}else r.length>1&&(e.codegenNode=sD(t,n(l5),void 0,e.children,64,void 0,void 0,!0,void 0,!1))}(o,r),o.helpers=new Set([...r.helpers.keys()]),o.components=[...r.components],o.directives=[...r.directives],o.imports=r.imports,o.hoists=r.hoists,o.temps=r.temps,o.cached=r.cached,o.transformed=!0,function(e,t={}){let n=function(e,{mode:t="function",prefixIdentifiers:n="module"===t,sourceMap:r=!1,filename:i="template.vue.html",scopeId:l=null,optimizeImports:s=!1,runtimeGlobalName:o="Vue",runtimeModuleName:a="vue",ssrRuntimeModuleName:c="vue/server-renderer",ssr:u=!1,isTS:h=!1,inSSR:d=!1}){let p={mode:t,prefixIdentifiers:n,sourceMap:r,filename:i,scopeId:l,optimizeImports:s,runtimeGlobalName:o,runtimeModuleName:a,ssrRuntimeModuleName:c,ssr:u,isTS:h,inSSR:d,source:e.source,code:"",column:1,line:1,offset:0,indentLevel:0,pure:!1,map:void 0,helper:e=>`_${sL[e]}`,push(e,t=-2,n){p.code+=e},indent(){f(++p.indentLevel)},deindent(e=!1){e?--p.indentLevel:f(--p.indentLevel)},newline(){f(p.indentLevel)}};function f(e){p.push(` +`+" ".repeat(e),0)}return p}(e,t);t.onContextCreated&&t.onContextCreated(n);let{mode:r,push:i,prefixIdentifiers:l,indent:s,deindent:o,newline:a,ssr:c}=n,u=Array.from(e.helpers),h=u.length>0,d=!l&&"module"!==r;!function(e,t){let{push:n,newline:r,runtimeGlobalName:i}=t,l=Array.from(e.helpers);if(l.length>0&&(n(`const _Vue = ${i} +`,-1),e.hoists.length)){let e=[sl,ss,so,sa,sc].filter(e=>l.includes(e)).map(o3).join(", ");n(`const { ${e} } = _Vue +`,-1)}(function(e,t){if(!e.length)return;t.pure=!0;let{push:n,newline:r}=t;r();for(let i=0;i0)&&a()),e.directives.length&&(o6(e.directives,"directive",n),e.temps>0&&a()),e.temps>0){i("let ");for(let t=0;t0?", ":""}_temp${t}`)}return(e.components.length||e.directives.length||e.temps)&&(i(` +`,0),a()),c||i("return "),e.codegenNode?o5(e.codegenNode,n):i("null"),d&&(o(),i("}")),o(),i("}"),{ast:e,code:n.code,preamble:"",map:n.map?n.map.toJSON():void 0}}(o,s)}(e,T({},a$,t,{nodeTransforms:[aH,...aq,...t.nodeTransforms||[]],directiveTransforms:T({},aW,t.directiveTransforms||{}),transformHoist:null}))}(e,i),s=Function(l)();return s._rc=!0,aK[n]=s}return iM(az),e.BaseTransition=ny,e.BaseTransitionPropsValidators=ng,e.Comment=r5,e.DeprecationTypes=null,e.EffectScope=em,e.ErrorCodes={SETUP_FUNCTION:0,0:"SETUP_FUNCTION",RENDER_FUNCTION:1,1:"RENDER_FUNCTION",NATIVE_EVENT_HANDLER:5,5:"NATIVE_EVENT_HANDLER",COMPONENT_EVENT_HANDLER:6,6:"COMPONENT_EVENT_HANDLER",VNODE_HOOK:7,7:"VNODE_HOOK",DIRECTIVE_HOOK:8,8:"DIRECTIVE_HOOK",TRANSITION_HOOK:9,9:"TRANSITION_HOOK",APP_ERROR_HANDLER:10,10:"APP_ERROR_HANDLER",APP_WARN_HANDLER:11,11:"APP_WARN_HANDLER",FUNCTION_REF:12,12:"FUNCTION_REF",ASYNC_COMPONENT_LOADER:13,13:"ASYNC_COMPONENT_LOADER",SCHEDULER:14,14:"SCHEDULER",COMPONENT_UPDATE:15,15:"COMPONENT_UPDATE",APP_UNMOUNT_CLEANUP:16,16:"APP_UNMOUNT_CLEANUP"},e.ErrorTypeStrings=null,e.Fragment=r4,e.KeepAlive={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){let n=iN(),r=n.ctx,i=new Map,l=new Set,s=null,o=n.suspense,{renderer:{p:a,m:c,um:u,o:{createElement:h}}}=r,d=h("div");function p(e){nJ(e),u(e,n,o,!0)}function f(e){i.forEach((t,n)=>{let r=iD(nj(t)?t.type.__asyncResolved||{}:t.type);r&&!e(r)&&g(n)})}function g(e){let t=i.get(e);!t||s&&ic(t,s)?s&&nJ(s):p(t),i.delete(e),l.delete(e)}r.activate=(e,t,n,r,i)=>{let l=e.component;c(e,t,n,0,o),a(l.vnode,e,t,n,l,o,r,e.slotScopeIds,i),rq(()=>{l.isDeactivated=!1,l.a&&z(l.a);let t=e.props&&e.props.onVnodeMounted;t&&iC(t,l.parent,e)},o)},r.deactivate=e=>{let t=e.component;rQ(t.m),rQ(t.a),c(e,d,null,1,o),rq(()=>{t.da&&z(t.da);let n=e.props&&e.props.onVnodeUnmounted;n&&iC(n,t.parent,e),t.isDeactivated=!0},o)},t7(()=>[e.include,e.exclude],([e,t])=>{e&&f(t=>nq(e,t)),t&&f(e=>!nq(t,e))},{flush:"post",deep:!0});let m=null,y=()=>{null!=m&&(rZ(n.subTree.type)?rq(()=>{i.set(m,nG(n.subTree))},n.subTree.suspense):i.set(m,nG(n.subTree)))};return nY(y),n1(y),n2(()=>{i.forEach(e=>{let{subTree:t,suspense:r}=n,i=nG(t);if(e.type===i.type&&e.key===i.key){nJ(i);let e=i.component.da;e&&rq(e,r);return}p(e)})}),()=>{if(m=null,!t.default)return s=null;let n=t.default(),r=n[0];if(n.length>1)return s=null,n;if(!ia(r)||!(4&r.shapeFlag)&&!(128&r.shapeFlag))return s=null,r;let o=nG(r);if(o.type===r5)return s=null,o;let a=o.type,c=iD(nj(o)?o.type.__asyncResolved||{}:a),{include:u,exclude:h,max:d}=e;if(u&&(!c||!nq(u,c))||h&&c&&nq(h,c))return o.shapeFlag&=-257,s=o,r;let p=null==o.key?a:o.key,f=i.get(p);return o.el&&(o=im(o),128&r.shapeFlag&&(r.ssContent=o)),m=p,f?(o.el=f.el,o.component=f.component,o.transition&&nC(o,o.transition),o.shapeFlag|=512,l.delete(p),l.add(p)):(l.add(p),d&&l.size>parseInt(d,10)&&g(l.values().next().value)),o.shapeFlag|=256,s=o,rZ(r.type)?r:o}}},e.ReactiveEffect=ey,e.Static=r9,e.Suspense={name:"Suspense",__isSuspense:!0,process(e,t,n,r,i,l,s,o,a,c){if(null==e)!function(e,t,n,r,i,l,s,o,a){let{p:c,o:{createElement:u}}=a,h=u("div"),d=e.suspense=r1(e,i,r,t,h,n,l,s,o,a);c(null,d.pendingBranch=e.ssContent,h,null,r,d,l,s),d.deps>0?(r0(e,"onPending"),r0(e,"onFallback"),c(null,e.ssFallback,t,n,r,null,l,s),r6(d,e.ssFallback)):d.resolve(!1,!0)}(t,n,r,i,l,s,o,a,c);else{if(l&&l.deps>0&&!e.suspense.isInFallback){t.suspense=e.suspense,t.suspense.vnode=t,t.el=e.el;return}!function(e,t,n,r,i,l,s,o,{p:a,um:c,o:{createElement:u}}){let h=t.suspense=e.suspense;h.vnode=t,t.el=e.el;let d=t.ssContent,p=t.ssFallback,{activeBranch:f,pendingBranch:g,isInFallback:m,isHydrating:y}=h;if(g)h.pendingBranch=d,ic(g,d)?(a(g,d,h.hiddenContainer,null,i,h,l,s,o),h.deps<=0?h.resolve():m&&!y&&(a(f,p,n,r,i,null,l,s,o),r6(h,p))):(h.pendingId=rY++,y?(h.isHydrating=!1,h.activeBranch=g):c(g,i,h),h.deps=0,h.effects.length=0,h.hiddenContainer=u("div"),m?(a(null,d,h.hiddenContainer,null,i,h,l,s,o),h.deps<=0?h.resolve():(a(f,p,n,r,i,null,l,s,o),r6(h,p))):f&&ic(f,d)?(a(f,d,n,r,i,h,l,s,o),h.resolve(!0)):(a(null,d,h.hiddenContainer,null,i,h,l,s,o),h.deps<=0&&h.resolve()));else if(f&&ic(f,d))a(f,d,n,r,i,h,l,s,o),r6(h,d);else if(r0(t,"onPending"),h.pendingBranch=d,512&d.shapeFlag?h.pendingId=d.component.suspenseId:h.pendingId=rY++,a(null,d,h.hiddenContainer,null,i,h,l,s,o),h.deps<=0)h.resolve();else{let{timeout:e,pendingId:t}=h;e>0?setTimeout(()=>{h.pendingId===t&&h.fallback(p)},e):0===e&&h.fallback(p)}}(e,t,n,r,i,s,o,a,c)}},hydrate:function(e,t,n,r,i,l,s,o,a){let c=t.suspense=r1(t,r,n,e.parentNode,document.createElement("div"),null,i,l,s,o,!0),u=a(e,c.pendingBranch=t.ssContent,n,c,l,s);return 0===c.deps&&c.resolve(!1,!0),u},normalize:function(e){let{shapeFlag:t,children:n}=e,r=32&t;e.ssContent=r2(r?n.default:n),e.ssFallback=r?r2(n.fallback):ip(r5)}},e.Teleport={name:"Teleport",__isTeleport:!0,process(e,t,n,r,i,l,s,o,a,c){let{mc:u,pc:h,pbc:d,o:{insert:p,querySelector:f,createText:g,parentNode:m}}=c,y=ni(t.props),{dynamicChildren:b}=t,_=(e,t,n)=>{16&e.shapeFlag&&u(e.children,t,n,i,l,s,o,a)},S=(e=t)=>{let n=ni(e.props),r=e.target=no(e.props,f),l=nu(r,e,g,p);r&&("svg"!==s&&nl(r)?s="svg":"mathml"!==s&&ns(r)&&(s="mathml"),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(r),n||(_(e,r,l),nc(e,!1)))},x=e=>{let t=()=>{if(nn.get(e)===t){if(nn.delete(e),ni(e.props)){let t=m(e.el)||n;_(e,t,e.anchor),nc(e,!0)}S(e)}};nn.set(e,t),rq(t,l)};if(null==e){let e,i=t.el=g(""),s=t.anchor=g("");if(p(i,n,r),p(s,n,r),(e=t.props)&&(e.defer||""===e.defer)||l&&l.pendingBranch)return void x(t);y&&(_(t,n,s),nc(t,!0)),S()}else{t.el=e.el;let r=t.anchor=e.anchor,u=nn.get(e);if(u){u.flags|=8,nn.delete(e),x(t);return}t.targetStart=e.targetStart;let p=t.target=e.target,g=t.targetAnchor=e.targetAnchor,m=ni(e.props),_=m?n:p,S=m?r:g;if("svg"===s||nl(p)?s="svg":("mathml"===s||ns(p))&&(s="mathml"),b?(d(e.dynamicChildren,b,_,i,l,s,o),rX(e,t,!0)):a||h(e,t,_,S,i,l,s,o,!1),y)m?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):na(t,n,r,c,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){let e=t.target=no(t.props,f);e&&na(t,e,null,c,0)}else m&&na(t,p,g,c,1);nc(t,y)}},remove(e,t,n,{um:r,o:{remove:i}},l){let{shapeFlag:s,children:o,anchor:a,targetStart:c,targetAnchor:u,target:h,props:d}=e,p=l||!ni(d),f=nn.get(e);if(f&&(f.flags|=8,nn.delete(e),p=!1),h&&(i(c),i(u)),l&&i(a),16&s)for(let e=0;ee[r]});return n},e.createRenderer=function(e){return rK(e)},e.createSSRApp=l6,e.createSlots=function(e,t){for(let n=0;n{let t=r.fn(...e);return t&&(t.key=r.key),t}:r.fn)}return e},e.createStaticVNode=function(e,t){let n=ip(r9,null,e);return n.staticCount=t,n},e.createTextVNode=iv,e.createVNode=ip,e.customRef=tE,e.defineAsyncComponent=function(e){let t;I(e)&&(e={loader:e});let{loader:n,loadingComponent:r,errorComponent:i,delay:l=200,hydrate:s,timeout:o,suspensible:a=!0,onError:c}=e,u=null,h=0,d=()=>{let e;return u||(e=u=n().catch(e=>{if(e=e instanceof Error?e:Error(String(e)),c)return new Promise((t,n)=>{c(e,()=>t((h++,u=null,d())),()=>n(e),h+1)});throw e}).then(n=>e!==u&&u?u:(n&&(n.__esModule||"Module"===n[Symbol.toStringTag])&&(n=n.default),t=n,n)))};return nT({name:"AsyncComponentWrapper",__asyncLoader:d,__asyncHydrate(e,n,r){let i=!1;(n.bu||(n.bu=[])).push(()=>i=!0);let l=()=>{i||r()},o=s?()=>{let t=s(l,t=>(function(e,t){if(nP(e)&&"["===e.data){let n=1,r=e.nextSibling;for(;r;){if(1===r.nodeType){if(!1===t(r))break}else if(nP(r))if("]"===r.data){if(0==--n)break}else"["===r.data&&n++;r=r.nextSibling}}else t(e)})(e,t));t&&(n.bum||(n.bum=[])).push(t)}:l;t?o():d().then(()=>!n.isUnmounted&&o())},get __asyncResolved(){return t},setup(){let e=iw;if(nw(e),t)return()=>nU(t,e);let n=t=>{u=null,tV(t,e,13,!i)};if(a&&e.suspense)return d().then(t=>()=>nU(t,e)).catch(e=>(n(e),()=>i?ip(i,{error:e}):null));let s=tS(!1),c=tS(),h=tS(!!l);return l&&setTimeout(()=>{h.value=!1},l),null!=o&&setTimeout(()=>{if(!s.value&&!c.value){let e=Error(`Async component timed out after ${o}ms.`);n(e),c.value=e}},o),d().then(()=>{s.value=!0,e.parent&&nH(e.parent.vnode)&&e.parent.update()}).catch(e=>{n(e),c.value=e}),()=>s.value&&t?nU(t,e):c.value&&i?ip(i,{error:c.value}):r&&!h.value?nU(r,e):void 0}})},e.defineComponent=nT,e.defineCustomElement=lC,e.defineEmits=function(){return null},e.defineExpose=function(e){},e.defineModel=function(){},e.defineOptions=function(e){},e.defineProps=function(){return null},e.defineSSRCustomElement=(e,t)=>lC(e,t,l6),e.defineSlots=function(){return null},e.devtools=void 0,e.effect=function(e,t){e.effect instanceof ey&&(e=e.effect.fn);let n=new ey(e);t&&T(n,t);try{n.run()}catch(e){throw n.stop(),e}let r=n.run.bind(n);return r.effect=n,r},e.effectScope=function(e){return new em(e)},e.getCurrentInstance=iN,e.getCurrentScope=function(){return l},e.getCurrentWatcher=function(){return g},e.getTransitionRawChildren=nk,e.guardReactiveProps=ig,e.h=iB,e.handleError=tV,e.hasInjectionContext=function(){return!!(iN()||rS)},e.hydrate=(...e)=>{l1().hydrate(...e)},e.hydrateOnIdle=(e=1e4)=>t=>{let n=nV(t,{timeout:e});return()=>nB(n)},e.hydrateOnInteraction=(e=[])=>(t,n)=>{R(e)&&(e=[e]);let r=!1,i=e=>{r||(r=!0,l(),t(),e.target.dispatchEvent(new e.constructor(e.type,e)))},l=()=>{n(t=>{for(let n of e)t.removeEventListener(n,i)})};return n(t=>{for(let n of e)t.addEventListener(n,i,{once:!0})}),l},e.hydrateOnMediaQuery=e=>t=>{if(e){let n=matchMedia(e);if(!n.matches)return n.addEventListener("change",t,{once:!0}),()=>n.removeEventListener("change",t);t()}},e.hydrateOnVisible=e=>(t,n)=>{let r=new IntersectionObserver(e=>{for(let n of e)if(n.isIntersecting){r.disconnect(),t();break}},e);return n(e=>{if(e instanceof Element){if(function(e){let{top:t,left:n,bottom:r,right:i}=e.getBoundingClientRect(),{innerHeight:l,innerWidth:s}=window;return(t>0&&t0&&r0&&n0&&ir.disconnect()},e.initCustomFormatter=function(){},e.initDirectivesForSSR=S,e.inject=t8,e.isMemoSame=ij,e.isProxy=tg,e.isReactive=td,e.isReadonly=tp,e.isRef=t_,e.isRuntimeOnly=()=>!h,e.isShallow=tf,e.isVNode=ia,e.markRaw=tv,e.mergeDefaults=function(e,t){let n=ra(e);for(let e in t){if(e.startsWith("__skip"))continue;let r=n[e];r?E(r)||I(r)?r=n[e]={type:r,default:t[e]}:r.default=t[e]:null===r&&(r=n[e]={default:t[e]}),r&&t[`__skip_${e}`]&&(r.skipFactory=!0)}return n},e.mergeModels=function(e,t){return e&&t?E(e)&&E(t)?e.concat(t):T({},ra(e),ra(t)):e||t},e.mergeProps=ix,e.nextTick=tz,e.nodeOps=iz,e.normalizeClass=ei,e.normalizeProps=function(e){if(!e)return null;let{class:t,style:n}=e;return t&&!R(t)&&(e.class=ei(t)),n&&(e.style=Y(n)),e},e.normalizeStyle=Y,e.onActivated=nW,e.onBeforeMount=nZ,e.onBeforeUnmount=n2,e.onBeforeUpdate=n0,e.onDeactivated=nK,e.onErrorCaptured=n5,e.onMounted=nY,e.onRenderTracked=n8,e.onRenderTriggered=n4,e.onScopeDispose=function(e,t=!1){l&&l.cleanups.push(e)},e.onServerPrefetch=n6,e.onUnmounted=n3,e.onUpdated=n1,e.onWatcherCleanup=tF,e.openBlock=it,e.patchProp=lS,e.popScopeId=function(){t1=null},e.provide=t4,e.proxyRefs=tN,e.pushScopeId=function(e){t1=e},e.queuePostFlushCb=tX,e.reactive=ta,e.readonly=tu,e.ref=tS,e.registerRuntimeCompiler=iM,e.render=l2,e.renderList=function(e,t,n,r){let i,l=n&&n[r],s=E(e);if(s||R(e)){let n=s&&td(e),r=!1,o=!1;n&&(r=!tf(e),o=tp(e),e=eU(e)),i=Array(e.length);for(let n=0,s=e.length;nt(e,n,void 0,l&&l[n]));else{let n=Object.keys(e);i=Array(n.length);for(let r=0,s=n.length;r0;return"default"!==t&&(n.name=t),it(),io(r4,null,[ip("slot",n,r&&r())],e?-2:64)}let l=e[t];l&&l._c&&(l._d=!1),it();let s=l&&function e(t){return t.some(t=>!ia(t)||t.type!==r5&&(t.type!==r4||!!e(t.children)))?t:null}(l(n)),o=n.key||s&&s.key,a=io(r4,{key:(o&&!O(o)?o:`_${t}`)+(!s&&r?"_fb":"")},s||(r?r():[]),s&&1===e._?64:-2);return!i&&a.scopeId&&(a.slotScopeIds=[a.scopeId+"-s"]),l&&l._c&&(l._d=!0),a},e.resolveComponent=function(e,t){return re(n9,e,!0,t)||e},e.resolveDirective=function(e){return re("directives",e)},e.resolveDynamicComponent=function(e){return R(e)?re(n9,e,!1)||e:e||n7},e.resolveFilter=null,e.resolveTransitionHooks=n_,e.setBlockTracking=il,e.setDevtoolsHook=S,e.setTransitionHooks=nC,e.shallowReactive=tc,e.shallowReadonly=function(e){return th(e,!0,e8,tr,to)},e.shallowRef=tx,e.ssrContextKey=t5,e.ssrUtils=null,e.stop=function(e){e.effect.stop()},e.toDisplayString=ep,e.toHandlerKey=W,e.toHandlers=function(e,t){let n={};for(let r in e)n[t&&/[A-Z]/.test(r)?`on:${r}`:W(r)]=e[r];return n},e.toRaw=tm,e.toRef=function(e,t,n){if(t_(e))return e;if(I(e))return new tR(e);if(!M(e)||!(arguments.length>1))return tS(e);return new tI(e,t,n)},e.toRefs=function(e){let t=E(e)?Array(e.length):{};for(let n in e)t[n]=new tI(e,n,void 0);return t},e.toValue=function(e){return I(e)?e():tT(e)},e.transformVNodeArgs=function(e){},e.triggerRef=function(e){e.dep&&e.dep.trigger()},e.unref=tT,e.useAttrs=function(){return ro().attrs},e.useCssModule=function(e="$style"){return b},e.useCssVars=function(e){let t=iN();if(!t)return;let n=t.ut=(n=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(e=>ls(e,n))},r=()=>{let r=e(t.proxy);t.ce?ls(t.ce,r):function e(t,n){if(128&t.shapeFlag){let r=t.suspense;t=r.activeBranch,r.pendingBranch&&!r.isHydrating&&r.effects.push(()=>{e(r.activeBranch,n)})}for(;t.component;)t=t.component.subTree;if(1&t.shapeFlag&&t.el)ls(t.el,n);else if(t.type===r4)t.children.forEach(t=>e(t,n));else if(t.type===r9){let{el:e,anchor:r}=t;for(;e&&(ls(e,n),e!==r);)e=e.nextSibling}}(t.subTree,r),n(r)};n0(()=>{tX(r)}),nY(()=>{t7(r,S,{flush:"post"});let e=new MutationObserver(r);e.observe(t.subTree.el.parentNode,{childList:!0}),n3(()=>e.disconnect())})},e.useHost=lw,e.useId=function(){let e=iN();return e?(e.appContext.config.idPrefix||"v")+"-"+e.ids[0]+e.ids[1]++:""},e.useModel=function(e,t,n=b){let r=iN(),i=j(t),l=H(t),s=rx(e,i),o=tE((s,o)=>{let a,c,u=b;return t9(()=>{let t=e[i];K(a,t)&&(a=t,o())}),{get:()=>(s(),n.get?n.get(a):a),set(e){let s=n.set?n.set(e):e;if(!K(s,a)&&!(u!==b&&K(e,u)))return;let h=r.vnode.props;h&&(t in h||i in h||l in h)&&(`onUpdate:${t}`in h||`onUpdate:${i}`in h||`onUpdate:${l}`in h)||(a=e,o()),r.emit(`update:${t}`,s),K(e,s)&&K(e,u)&&!K(s,c)&&o(),u=e,c=s}}});return o[Symbol.iterator]=()=>{let e=0;return{next:()=>e<2?{value:e++?s||b:o,done:!1}:{done:!0}}},o},e.useSSRContext=()=>{},e.useShadowRoot=function(){let e=lw();return e&&e.shadowRoot},e.useSlots=function(){return ro().slots},e.useTemplateRef=function(e){let t=iN(),n=tx(null);return t&&Object.defineProperty(t.refs===b?t.refs={}:t.refs,e,{enumerable:!0,get:()=>n.value,set:e=>n.value=e}),n},e.useTransitionState=np,e.vModelCheckbox=lU,e.vModelDynamic={created(e,t,n){lG(e,t,n,null,"created")},mounted(e,t,n){lG(e,t,n,null,"mounted")},beforeUpdate(e,t,n,r){lG(e,t,n,r,"beforeUpdate")},updated(e,t,n,r){lG(e,t,n,r,"updated")}},e.vModelRadio=lq,e.vModelSelect=lW,e.vModelText=lj,e.vShow={name:"show",beforeMount(e,{value:t},{transition:n}){e[ln]="none"===e.style.display?"":e.style.display,n&&t?n.beforeEnter(e):li(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),li(e,!0),r.enter(e)):r.leave(e,()=>{li(e,!1)}):li(e,t))},beforeUnmount(e,{value:t}){li(e,t)}},e.version=iU,e.warn=S,e.watch=function(e,t,n){return t7(e,t,n)},e.watchEffect=function(e,t){return t7(e,null,t)},e.watchPostEffect=function(e,t){return t7(e,null,{flush:"post"})},e.watchSyncEffect=t9,e.withAsyncContext=function(e){let t=iN(),n=iR,r=e();iE(),n&&u(!1);let i=()=>{iA(t),n&&u(!0)},l=()=>{iN()!==t&&t.scope.off(),iE(),n&&u(!1)};return P(r)&&(r=r.catch(e=>{throw i(),Promise.resolve().then(()=>Promise.resolve().then(l)),e})),[r,()=>{i(),Promise.resolve().then(l)}]},e.withCtx=t3,e.withDefaults=function(e,t){return null},e.withDirectives=function(e,t){if(null===t0)return e;let n=i$(t0),r=e.dirs||(e.dirs=[]);for(let e=0;e{let n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=n=>{if(!("key"in n))return;let r=H(n.key);if(t.some(e=>e===r||lZ[e]===r))return e(n)})},e.withMemo=function(e,t,n,r){let i=n[r];if(i&&ij(i,e))return i;let l=t();return l.memo=e.slice(),l.cacheIndex=r,n[r]=l},e.withModifiers=(e,t)=>{if(!e)return e;let n=e._withMods||(e._withMods={}),r=t.join(".");return n[r]||(n[r]=(n,...r)=>{for(let e=0;et3,e}({}); diff --git a/agv_app/style.css b/agv_app/style.css new file mode 100644 index 0000000..3ee0cd7 --- /dev/null +++ b/agv_app/style.css @@ -0,0 +1,722 @@ +/* ========== 全局样式 ========== */ +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; + background: #0f1923; + color: #e8eaed; + font-size: 14px; + min-height: 100vh; +} + +a { color: #4fc3f7; text-decoration: none; } +a:hover { text-decoration: underline; } + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px 16px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +/* ========== 顶部栏 ========== */ +.topbar { + background: #1a2332; + border-bottom: 1px solid #2a3441; + padding: 0 20px; + display: flex; + align-items: center; + height: 56px; + gap: 32px; + position: sticky; + top: 0; + z-index: 100; +} + +.logo { font-size: 18px; font-weight: bold; color: #4fc3f7; } + +.nav { display: flex; gap: 4px; } +.nav-link { + padding: 8px 16px; + border-radius: 6px; + color: #9aa0a6; + transition: all 0.2s; +} +.nav-link:hover { background: #2a3441; color: #e8eaed; text-decoration: none; } +.nav-link.active { background: #263238; color: #4fc3f7; } + +.status-bar { margin-left: auto; display: flex; gap: 12px; } +.status-item { + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: bold; +} +.status-item.setting { background: #1b3a2f; color: #4caf50; } +.status-item.running { background: #2a2a1b; color: #ffeb3b; } +.status-item.paused { background: #3a2a1a; color: #ff9800; } +.status-item.idle { background: #2a2a2a; color: #9aa0a6; } + +/* ========== 卡片 ========== */ +.card { + background: #1a2332; + border-radius: 12px; + padding: 20px; + border: 1px solid #2a3441; +} +.card h2 { font-size: 16px; margin-bottom: 16px; color: #4fc3f7; } + +/* ========== 状态卡片 ========== */ +.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; } +.status-card { + background: #0f1923; + border-radius: 8px; + padding: 16px; + text-align: center; + border: 1px solid #2a3441; +} +.status-card.ok { border-color: #2e7d32; background: #0d1f14; } +.status-card.error { border-color: #c62828; background: #1f0d0d; } +.status-icon { font-size: 24px; margin-bottom: 8px; } +.status-label { font-size: 12px; color: #9aa0a6; margin-bottom: 4px; } +.status-value { font-size: 14px; font-weight: bold; } + +/* ========== 按钮 ========== */ +.btn { + padding: 8px 16px; + border-radius: 6px; + border: none; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; + background: #263238; + color: #e8eaed; + font-family: inherit; +} +.btn:hover:not(:disabled) { background: #37474f; } +.btn:disabled { opacity: 0.5; cursor: not-allowed; } + +.btn-primary { background: #0277bd; color: #fff; } +.btn-primary:hover:not(:disabled) { background: #0288d1; } +.btn-secondary { background: #37474f; } +.btn-danger { background: #d32f2f; color: #fff; } +.btn-danger:hover:not(:disabled) { background: #f44336; } +.btn-success { background: #2e7d32; color: #fff; } +.btn-success:hover:not(:disabled) { background: #388e3c; } +.btn-warning { background: #e65100; color: #fff; } +.btn-error { background: #c62828; color: #fff; } +.btn-large { padding: 12px 24px; font-size: 16px; } +.btn-small { padding: 4px 10px; font-size: 12px; } +.btn-icon { background: none; border: none; cursor: pointer; font-size: 14px; padding: 4px; } +.btn-row { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; } + +/* ========== 表单 ========== */ +.form-group { margin-bottom: 12px; } +.form-group label { display: block; font-size: 12px; color: #9aa0a6; margin-bottom: 4px; } +.form-group input, +.form-group select { + width: 100%; + padding: 8px 12px; + background: #0f1923; + border: 1px solid #2a3441; + border-radius: 6px; + color: #e8eaed; + font-size: 14px; + font-family: inherit; +} +.form-group input:focus, +.form-group select:focus { outline: none; border-color: #4fc3f7; } +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + +/* ========== Tabs ========== */ +.tabs { + background: #1a2332; + border-bottom: 1px solid #2a3441; + padding: 0 20px; + display: flex; + gap: 4px; +} +.tab { + padding: 12px 20px; + background: none; + border: none; + color: #9aa0a6; + cursor: pointer; + font-size: 14px; + border-bottom: 2px solid transparent; + font-family: inherit; +} +.tab.active { color: #4fc3f7; border-bottom-color: #4fc3f7; } +.tab:hover { color: #e8eaed; } + +/* ========== 摄像头预览 ========== */ +.camera-preview { + width: 100%; + max-width: 480px; + border-radius: 8px; + overflow: hidden; + margin: 0 auto 16px; + background: #000; +} +.camera-preview img, +.camera-full img { + width: 100%; + display: block; + aspect-ratio: 16/9; + object-fit: cover; +} +.camera-full { + width: 100%; + border-radius: 8px; + overflow: hidden; + background: #000; +} + +/* ========== 关节控制 ========== */ +.joints-panel { margin-top: 16px; } +.joints-panel h3 { margin-bottom: 12px; font-size: 14px; } +.joint-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.joint-control { + background: #0f1923; + border-radius: 8px; + padding: 12px; + text-align: center; + border: 1px solid #2a3441; +} +.joint-control label { font-size: 12px; color: #4fc3f7; font-weight: bold; } +.joint-value { font-size: 18px; font-weight: bold; color: #fff; margin: 4px 0; } +.joint-buttons { display: flex; align-items: center; gap: 4px; justify-content: center; } +.joint-buttons button { + width: 32px; + height: 32px; + border-radius: 4px; + border: 1px solid #2a3441; + background: #263238; + color: #e8eaed; + cursor: pointer; + font-size: 14px; +} +.joint-buttons input { + width: 60px; + padding: 4px; + text-align: center; + background: #0f1923; + border: 1px solid #2a3441; + border-radius: 4px; + color: #e8eaed; + font-size: 12px; +} + +/* ========== 点位列表 ========== */ +.point-item { + background: #0f1923; + border: 1px solid #2a3441; + border-radius: 8px; + padding: 12px; + margin-bottom: 12px; +} +.point-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } +.point-name { font-weight: bold; font-size: 15px; } +.point-coords { font-size: 12px; color: #9aa0a6; margin-bottom: 8px; } +.badge { + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + background: #263238; + color: #4fc3f7; +} +.pose-list { margin-top: 8px; } +.pose-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid #2a3441; + font-size: 13px; +} +.angles { color: #9aa0a6; font-size: 11px; font-family: monospace; } +.pose-add { + display: flex; + gap: 8px; + align-items: center; + margin-top: 8px; +} +.pose-add input { flex: 1; padding: 6px 10px; background: #1a2332; border: 1px solid #2a3441; border-radius: 4px; color: #e8eaed; font-size: 13px; } +.pose-add select { padding: 6px; background: #1a2332; border: 1px solid #2a3441; border-radius: 4px; color: #e8eaed; } + +.empty-hint { color: #9aa0a6; text-align: center; padding: 20px; } +.hint { font-size: 12px; color: #9aa0a6; margin-top: 8px; } +.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 12px; } +.alert-error { background: #1f0d0d; border: 1px solid #c62828; color: #ef5350; } +.checkbox-group { display: flex; gap: 16px; } +.checkbox-group label { display: flex; align-items: center; gap: 6px; cursor: pointer; color: #e8eaed; } + +/* ========== 运行页面 ========== */ +.running-header { display: flex; align-items: center; gap: 20px; margin-bottom: 16px; } +.running-status { + font-size: 18px; + font-weight: bold; + display: flex; + align-items: center; + gap: 8px; +} +.running-status.idle { color: #9aa0a6; } +.running-status.running { color: #4caf50; } +.running-status.paused { color: #ff9800; } +.pulse { + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + animation: pulse 1.5s infinite; +} +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } +} +.running-progress { flex: 1; display: flex; align-items: center; gap: 12px; } +.progress-bar { flex: 1; height: 8px; background: #2a3441; border-radius: 4px; overflow: hidden; } +.progress-fill { height: 100%; background: #4fc3f7; border-radius: 4px; transition: width 0.3s; } + +/* ========== 报告 ========== */ +.report-summary { display: flex; gap: 16px; margin-bottom: 16px; } +.stat { padding: 8px 16px; border-radius: 8px; background: #0f1923; border: 1px solid #2a3441; } +.stat.ok { border-color: #2e7d32; color: #4caf50; } +.stat.error { border-color: #c62828; color: #ef5350; } +.report-item { padding: 8px 12px; background: #0f1923; border-radius: 6px; margin-bottom: 8px; border: 1px solid #2a3441; } +.report-point { display: flex; align-items: center; gap: 8px; font-weight: bold; } +.report-status { font-size: 16px; } +.report-pose { font-size: 12px; color: #9aa0a6; padding-left: 24px; margin-top: 4px; } + +/* ========== 响应式 ========== */ +@media (max-width: 768px) { + .container { grid-template-columns: 1fr; } + .grid-3 { grid-template-columns: 1fr; } + .form-row { grid-template-columns: 1fr; } + .joint-grid { grid-template-columns: repeat(2, 1fr); } + .form-row { grid-template-columns: 1fr; } +} + +/* AGV 移动控制面板 */ +.agv-status-bar { + display: flex; + gap: 16px; + align-items: center; + padding: 10px 14px; + background: #0f1923; + border-radius: 8px; + margin-bottom: 16px; + font-size: 13px; + color: #9aa0a6; + flex-wrap: wrap; +} +.agv-status-bar strong { color: #e8eaed; } + +.agv-control-panel { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + max-width: 280px; + margin: 0 auto; +} +.agv-dir-row { + display: grid; + grid-template-columns: 80px 80px 80px; + gap: 6px; + width: 100%; +} +.agv-dir-placeholder { width: 80px; height: 44px; } +.agv-btn { + height: 44px; + border-radius: 8px; + border: 1px solid #2a3441; + background: #263238; + color: #e8eaed; + cursor: pointer; + font-size: 13px; + font-family: inherit; + transition: background 0.15s; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + user-select: none; +} +.agv-btn:active, .agv-btn:focus { outline: none; } +.agv-btn-up { background: #1b3a2f; border-color: #2e7d32; color: #4caf50; } +.agv-btn-down { background: #3a1b1b; border-color: #7d2e2e; color: #f44336; } +.agv-btn-left { background: #1b2d3a; border-color: #1565c0; color: #42a5f5; } +.agv-btn-right { background: #2d2a1b; border-color: #7d6e2e; color: #ffc107; } +.agv-btn-stop { background: #37474f; border-color: #546e7a; } +.agv-btn-up:active { background: #1e4d38; } +.agv-btn-down:active { background: #4d2020; } +.agv-btn-left:active { background: #1e3a4d; } +.agv-btn-right:active { background: #3d3820; } +.agv-btn-stop:active { background: #455a64; } +.agv-btn-lateral { + background: #2d1b4a; + border-color: #7c4dff; + color: #b388ff; + font-size: 13px; + min-width: 120px; +} +.agv-btn-lateral:active { background: #3d2560; } +.agv-lateral-row { + display: flex; + gap: 12px; + justify-content: center; + margin-top: 8px; + max-width: 280px; + width: 100%; +} + +.speed-control { + display: flex; + align-items: center; + gap: 10px; +} +.speed-value { + min-width: 44px; + text-align: right; + font-weight: bold; + color: #4fc3f7; +} + +/* 双摄像头预览布局 */ +.camera-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-top: 12px; +} +.camera-box { + background: #111; + border-radius: 8px; + overflow: hidden; +} +.camera-label { + padding: 8px 12px; + font-size: 13px; + color: #aaa; + background: #1a1a1a; + border-bottom: 1px solid #333; +} +.camera-img { + width: 100%; + display: block; + aspect-ratio: 4/3; + object-fit: cover; +} +.camera-placeholder { + width: 100%; + aspect-ratio: 4/3; + display: flex; + align-items: center; + justify-content: center; + color: #666; + font-size: 14px; +} +/* ========== 地图标记 ========== */ +.map-marker { + position: absolute; + transform: translate(-50%, -100%); + font-size: 20px; + cursor: pointer; + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); + z-index: 10; +} +.map-marker:hover { + transform: translate(-50%, -100%) scale(1.2); +} + +/* ========== 任务配置 M×N 网格 ========== */ +.mission-grid-wrap { + margin-top: 12px; + overflow-x: auto; +} +.mission-grid { + display: grid; + gap: 4px; + grid-template-columns: 80px repeat(var(--cols,4), 90px); +} +.grid-cell { + min-width: 80px; + min-height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + font-size: 12px; + cursor: pointer; + border: 1px solid #2a3441; + background: #0f1923; + transition: background 0.15s, border-color 0.15s; +} +.grid-cell.active { + background: #1b3a2f; + border-color: #2e7d32; + color: #4caf50; +} +.grid-cell.active:hover { + background: #234; +} +.grid-cell.selected { + border-color: #4fc3f7 !important; + box-shadow: 0 0 0 2px #4fc3f7; +} +.grid-header { + background: transparent; + border-color: transparent; + cursor: default; + font-weight: bold; + color: #9aa0a6; + font-size: 12px; +} + +/* 机器配置表单 */ +.machine-form { + background: #0f1923; + border: 1px solid #2a3441; + border-radius: 8px; + padding: 16px; + margin-top: 12px; +} +.machine-form h3 { + font-size: 14px; + color: #4fc3f7; + margin-bottom: 10px; +} +.machine-form h4 { + font-size: 13px; + color: #9aa0a6; + margin: 8px 0 6px; +} + +/* 姿态列表 */ +.pose-list { + margin-top: 8px; +} +.pose-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid #2a3441; + font-size: 13px; +} +.pose-name { + font-weight: bold; + min-width: 80px; +} +.pose-angles { + color: #9aa0a6; + font-size: 11px; + font-family: monospace; + flex: 1; +} +.pose-add { + display: flex; + gap: 8px; + align-items: center; + margin-top: 8px; +} +.pose-add input { + flex: 1; + padding: 6px 10px; + background: #1a2332; + border: 1px solid #2a3441; + border-radius: 4px; + color: #e8eaed; + font-size: 13px; +} + +/* 蛇形序列预览 */ +.sequence-preview { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 320px; + overflow-y: auto; +} +.sequence-step { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 12px; + background: #0f1923; + border-radius: 6px; + border: 1px solid #2a3441; + font-size: 13px; +} +.step-index { + background: #263238; + color: #4fc3f7; + border-radius: 10px; + min-width: 28px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: bold; +} +.step-info { + flex: 1; +} +.step-side { + padding: 2px 8px; + border-radius: 8px; + font-size: 11px; + font-weight: bold; +} +.step-side:contains('正面') { + background: #1b3a2f; + color: #4caf50; +} +.step-side:contains('背面') { + background: #3a1b2f; + color: #ce93d8; +} + +/* 网格单元格点位配置 */ +.cell-machine { + font-size: 11px; + font-weight: bold; + color: #2c3e50; +} +.cell-points { + margin-top: 2px; + font-size: 9px; +} +.point-row { + display: flex; + align-items: center; + gap: 2px; + padding: 1px 2px; + background: #f8f9fa; + border-radius: 3px; + cursor: pointer; + margin: 1px 0; +} +.point-row:hover { + background: #e9ecef; +} +.point-label { + color: #666; + min-width: 24px; +} +.point-coords { + color: #0366d6; + font-family: monospace; + font-size: 8px; + flex: 1; +} +.btn-icon-small { + background: none; + border: none; + cursor: pointer; + font-size: 10px; + padding: 1px 3px; + border-radius: 3px; +} +.btn-icon-small:hover { + background: #ddd; +} + +/* ========== 任务配置 弹窗 + 网格增强样式 ========== */ + +/* 弹窗遮罩 */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} +.modal-box { + background: #1a1f2e; + border: 1px solid #2a3a50; + border-radius: 12px; + padding: 20px 24px; + min-width: 380px; + max-width: 500px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); +} +.modal-box h3 { margin: 0 0 8px; color: #e0e6f0; font-size: 16px; } + +/* 点位行单元格 */ +.point-cell { cursor: pointer; flex-direction: column; gap: 2px; } +.point-cell:hover { border-color: #4fc3f7; background: #162030; } +.point-cell.point-filled { background: #0d2535; border-color: #1565c0; } +.point-coords { font-size: 10px; color: #64b5f6; font-family: monospace; } +.point-empty { font-size: 10px; color: #455a64; } + +/* 机器行单元格 */ +.machine-cell { cursor: pointer; } +.machine-cell:hover { border-color: #4caf50; background: #1b3a2f; } +.machine-cell.active { background: #1b3a2f; border-color: #2e7d32; color: #4caf50; } +.machine-icon { font-size: 18px; } +.machine-empty { font-size: 16px; color: #455a64; } +/* ========== 任务配置 弹窗 + 网格增强样式 ========== */ + +/* 弹窗遮罩 */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} +.modal-box { + background: #1a1f2e; + border: 1px solid #2a3a50; + border-radius: 12px; + padding: 20px 24px; + min-width: 380px; + max-width: 500px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); +} +.modal-box h3 { margin: 0 0 8px; color: #e0e6f0; font-size: 16px; } + +/* 点位行单元格 */ +.point-cell { cursor: pointer; flex-direction: column; gap: 2px; } +.point-cell:hover { border-color: #4fc3f7; background: #162030; } +.point-cell.point-filled { background: #0d2535; border-color: #1565c0; } +.point-coords { font-size: 10px; color: #64b5f6; font-family: monospace; } +.point-empty { font-size: 10px; color: #455a64; } + +/* 机器行单元格 */ +.machine-cell { cursor: pointer; } +.machine-cell:hover { border-color: #4caf50; background: #1b3a2f; } +.machine-cell.active { background: #1b3a2f; border-color: #2e7d32; color: #4caf50; } +.machine-icon { font-size: 18px; } +.machine-empty { font-size: 16px; color: #455a64; } +/* 点位编辑弹窗 */ +.modal-overlay .modal-box { min-width: 420px; } +.modal-overlay .form-row { gap: 8px; } +.modal-overlay .btn-row { gap: 8px; flex-wrap: wrap; } + +/* 地图坐标点覆盖层 */ +.map-container { position: relative; } +.map-overlay { + position: absolute; top: 0; left: 0; right: 0; bottom: 0; + pointer-events: none; +} +.map-dot { + position: absolute; + transform: translate(-50%, -50%); +} +.point-dot { + width: 10px; height: 10px; + background: #f39c12; + border-radius: 50%; + border: 2px solid #fff; + box-shadow: 0 0 6px rgba(243,156,18,0.9); +} diff --git a/agv_app/templates/index.html b/agv_app/templates/index.html new file mode 100644 index 0000000..c32b73b --- /dev/null +++ b/agv_app/templates/index.html @@ -0,0 +1,148 @@ + + + + + + AGV 拍摄系统 + + + +
+ +
+ + +
+ + [[ statusText ]] + +
+
+ + +
+ +
+

📡 系统连接状态

+
+
+
+ + [[ agvConnected ? '✅' : '❌' ]] +
+
AGV
+
+ 重连中... + [[ agvConnected ? '已连接' : '未连接' ]](点击重连) +
+
+
+
+ + [[ armConnected ? '✅' : '❌' ]] +
+
机械臂
+
+ 重连中... + [[ armConnected ? '已连接' : '未连接' ]](点击重连) +
+
+
+
+ + [[ cameraOpened ? '✅' : '❌' ]] +
+
AGV摄像头
+
+ 重连中... + [[ cameraOpened ? '已打开' : '未打开' ]](点击重连) +
+
+
+
+ + [[ armCameraOpened ? '✅' : '❌' ]] +
+
机械臂摄像头
+
+ 重连中... + [[ armCameraOpened ? '已打开' : '未打开' ]](点击重连) +
+
+
+
+ + +
+
+ + +
+

📷 摄像头预览

+
+
+
AGV 摄像头
+ +
AGV 摄像头异常
+
未打开(先点击连接设备)
+
+
+
机械臂摄像头
+ +
机械臂摄像头异常
+
未连接
+
+
+
+ + +
+

🗺️ 地图信息

+
+

地图目录: [[ mapConfig.map_dir ]]

+

地图文件: [[ mapConfig.map_file ]]

+
+
+

尚未加载地图,请前往 设置页面 配置地图

+
+
+ + +
+

📍 点位概览

+

已配置 [[ pointsCount ]] 个拍摄点位

+ +
+ + +
+

🚀 快捷入口

+ +

请先连接所有设备并加载地图

+
+
+
+ + + + + diff --git a/agv_app/templates/running.html b/agv_app/templates/running.html new file mode 100644 index 0000000..9294256 --- /dev/null +++ b/agv_app/templates/running.html @@ -0,0 +1,82 @@ + + + + + + 运行监控 - AGV 拍摄系统 + + + +
+
+ + +
+ +
+ +
+
+
+ + [[ missionStateText ]] +
+
+ 点位 [[ currentPoint + 1 ]] / [[ totalPoints ]] +
+
+
+
+
+
+ + + +
+
+ + +
+

📷 摄像头预览

+
+ +
+
+ + +
+

📋 任务报告

+
+
✅ 完成: [[ report.completed ]]
+
❌ 失败: [[ report.failed ]]
+
总计: [[ report.total_points ]]
+
+
+
+
+ [[ detail.status === 'completed' ? '✅' : '❌' ]] + [[ detail.point_name ]] +
+
+ [[ pose.photo_type ]] - [[ pose.status ]] +
+
+
+
+
+
+ + + + + diff --git a/agv_app/templates/setting.html b/agv_app/templates/setting.html new file mode 100644 index 0000000..a14e794 --- /dev/null +++ b/agv_app/templates/setting.html @@ -0,0 +1,507 @@ + + + + + + 设置 - AGV 拍摄系统 + + + +
+
+ + +
+ + +
+ + + + + +
+ +
+ +
+
+

地图配置

+
+
+ + +
+
+ + +
+
+ + +
+
+

{% raw %}{{ mapMsg }}{% endraw %}

+
+
+

地图可视化

+
+ + +
+ +
+
+
+
+
+
+ + +
+
+

📦 机型配置

+ + +
+

添加新机型

+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+

暂无机型配置,请添加新机型

+
+ +
+
+ +
+
+ {{ m.name }} + ID: {{ m.id }} + {{ m.description }} + 【{{ m.notes }}】 +
+
+ +
+
+ + +
+ +
+

🔵 正面姿态

+
+
+ {{ pose.name or '正面姿态' }} + +
+
+
+ J{{j}} + + ° +
+
+
+ +
+
+ + +
+
+ 当前机械臂角度: + + J1={{ currentAngles[0]?.toFixed(1) }}° J2={{ currentAngles[1]?.toFixed(1) }}° J3={{ currentAngles[2]?.toFixed(1) }}° J4={{ currentAngles[3]?.toFixed(1) }}° J5={{ currentAngles[4]?.toFixed(1) }}° J6={{ currentAngles[5]?.toFixed(1) }}° + + (未连接机械臂) +
+
+
+ + +
+

🔴 背面姿态

+
+
+ {{ pose.name or '背面姿态' }} + +
+
+
+ J{{j}} + + ° +
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+ + +
+ + +
+

① 网格配置 (M×N)

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
第{% raw %}{{ c }}{% endraw %}列
+ + + + + + +
点位行 1
+
+ {% raw %}{{ getPointAt(0, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(0, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %} + +
+ + + + + +
机器行 {% raw %}{{ missionConfig.rows }}{% endraw %}
+
+ + +
+ + +
点位行 {% raw %}{{ missionConfig.rows+1 }}{% endraw %}
+
+ {% raw %}{{ getPointAt(missionConfig.rows, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(missionConfig.rows, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %} + +
+
+

点击「点位行」配置拍摄坐标;点击「机器行」切换有无机器
中间点位同时服务于上下两台机器(上机器背面 / 下机器正面),删除机器不影响点位配置

+
+
+ + +
+

② 点位配置 — 第{% raw %}{{ selectedMachine.row+1 }}{% endraw %}行 第{% raw %}{{ selectedMachine.col+1 }}{% endraw %}列

+ + +
+

📷 正面点位

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+

正面姿态 ({% raw %}{{ selectedMachine.front.poses.length }}{% endraw %} 个)

+
+ {% raw %}{{ pose.name }}{% endraw %} + 角度: {% raw %}{{ formatAngles(pose.arm_angles) }}{% endraw %} + +
+
+
+ + +
+
+ + +
+

📷 背面点位

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+

背面姿态 ({% raw %}{{ selectedMachine.back.poses.length }}{% endraw %} 个)

+
+ {% raw %}{{ pose.name }}{% endraw %} + 角度: {% raw %}{{ formatAngles(pose.arm_angles) }}{% endraw %} + +
+
+
+ + +
+
+ +
+ + +
+
+ + +
+

③ 🐍 蛇形拍摄序列预览

+
+
+ {% raw %}{{ idx+1 }}{% endraw %} + + 第{% raw %}{{ step.row+1 }}{% endraw %}行 第{% raw %}{{ step.col+1 }}{% endraw %}列 + {% raw %}{{ step.side === 'front' ? '正面' : '背面' }}{% endraw %} + +
+
+
+ +
+
+
+ + + + + + +
+
+

🤖 机械臂控制

+
+ ⚠️ 机械臂未连接,请先在首页连接设备 +
+
+
+ +
+
+

关节角度控制

+
+
+ +
{% raw %}{{ currentAngles[j-1] ? currentAngles[j-1].toFixed(1) : '—' }}{% endraw %}°
+
+ + + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+

🚗 AGV 移动控制

+
+ ⚠️ AGV 未连接,请先在首页连接设备 +
+
+
+ +
+
+ 🔋 电压: {% raw %}{{ agvBattery !== null ? agvBattery + 'V' : '—' }}{% endraw %} + 📍 位置: X={% raw %}{{ agvPosition[0] ? agvPosition[0].toFixed(2) : '?' }}{% endraw %} Y={% raw %}{{ agvPosition[1] ? agvPosition[1].toFixed(2) : '?' }}{% endraw %} + +
+
+
+
+ +
+
+
+ + + +
+
+
+ +
+
+
+
+ + +
+
+
+ +
+ + {% raw %}{{ (agvSpeed * 100).toFixed(0) }}{% endraw %}% +
+
+
+
+ + +
+
+
+
+
+
+ + + + + diff --git a/agv_app/templates/setting.js b/agv_app/templates/setting.js new file mode 100644 index 0000000..217f9e8 --- /dev/null +++ b/agv_app/templates/setting.js @@ -0,0 +1,438 @@ +const { createApp } = Vue +const API = '' + +createApp({ + data() { + return { + tab: 'map', + // 任务配置 + missionConfig: { rows: 3, cols: 3, grid: [], machines: [] }, + selectedMachine: null, + // 地图 + mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' }, + mapMsg: '', + mapLoaded: false, + mapImageUrl: '', + mapMeta: null, + // 点位 + points: [], + newPointName: '', + newPointMode: 'front', + newPointSequence: ['front', 'back'], + // 机型(姿态组) + models: [], + selectedModelId: null, + newModelName: '', + newModelSerial: '', + poseForm: {}, + // 机械臂 + armConnected: false, + currentAngles: [], + angleInputs: [], + previewUrl: API + '/api/camera/preview', + jogIntervals: {}, + // AGV + cameraOpened: false, + agvConnected: false, + agvBattery: null, + agvPosition: null, + agvSpeed: 0.5, + agvMoveInterval: null, + agvCameraUrl: API + '/api/camera/refresh', + agvCameraTimer: null, + } + }, + mounted() { + this.refresh() + this.refreshAngles() + }, + watch: { + tab(val) { + if (val === 'agv') { + this.agvCameraTimer = setInterval(() => { + this.agvCameraUrl = API + '/api/camera/refresh?t=' + Date.now() + }, 1000) + } else { + if (this.agvCameraTimer) { + clearInterval(this.agvCameraTimer) + this.agvCameraTimer = null + } + } + } + }, + beforeUnmount() { + Object.values(this.jogIntervals).forEach(i => clearInterval(i)) + if (this.agvCameraTimer) clearInterval(this.agvCameraTimer) + }, + methods: { + async refresh() { + try { + const res = await fetch(API + '/api/status') + const data = await res.json() + this.agvConnected = data.agv_connected + this.armConnected = data.arm_connected + this.cameraOpened = data.camera_opened + this.mapLoaded = data.map_loaded + // 如果地图已加载,自动获取地图图像和元数据 + if (data.map_loaded) { + this.mapImageUrl = API + '/api/map/image?t=' + Date.now() + try { + const metaRes = await fetch(API + '/api/map/meta') + const meta = await metaRes.json() + if (meta.ok) this.mapMeta = meta + } catch (e) {} + } + } catch (e) {} + await this.loadAllPoints() + await this.loadAllModels() + }, + // === 地图 === + async loadMap() { + const res = await fetch(API + '/api/map/load', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.mapForm) + }) + const data = await res.json() + this.mapMsg = data.ok ? '✅ 地图加载成功' : '❌ ' + (data.error || '加载失败') + this.mapLoaded = data.ok + if (data.ok) { + this.mapImageUrl = API + '/api/map/image?t=' + Date.now() + try { + const metaRes = await fetch(API + '/api/map/meta') + const meta = await metaRes.json() + if (meta.ok) this.mapMeta = meta + } catch (e) {} + } + }, + onMapError() { + this.mapMsg = '❌ 地图图像加载失败' + }, + getMapX(coords) { + if (!coords || !this.mapMeta) return 50 + const [x, y, yaw] = coords + const { resolution, origin, width } = this.mapMeta + const px = (x - origin[0]) / (resolution * width) * 100 + return Math.max(0, Math.min(100, px)) + }, + getMapY(coords) { + if (!coords || !this.mapMeta) return 50 + const [x, y, yaw] = coords + const { resolution, origin, height } = this.mapMeta + const py = (y - origin[1]) / (resolution * height) * 100 + return Math.max(0, Math.min(100, 100 - py)) + }, + async saveMap() { + await fetch(API + '/api/map/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.mapForm) + }) + this.mapMsg = '✅ 地图配置已保存' + }, + // === 点位 === + async loadAllPoints() { + const res = await fetch(API + '/api/points/list') + const data = await res.json() + this.points = data.points || [] + }, + async addPoint() { + const res = await fetch(API + '/api/points/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: this.newPointName || 'point_' + (this.points.length + 1), + photo_mode: this.newPointMode, + sequence: this.newPointSequence + }) + }) + const data = await res.json() + if (data.ok) { + await this.loadAllPoints() + this.newPointName = '' + } + }, + async deletePoint(id) { + if (!confirm('确定删除该点位?')) return + await fetch(API + '/api/points/delete/' + id, { method: 'DELETE' }) + await this.loadAllPoints() + }, + async saveAllPoints() { + await fetch(API + '/api/points/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ points: this.points }) + }) + alert('点位已保存') + }, + getPoint(id) { + return this.points.find(p => p.id === id) + }, + formatAngles(angles) { + if (!angles) return '—' + return angles.map(a => (a || 0).toFixed(1) + '°').join(' / ') + }, + // === 机型管理 === + async loadAllModels() { + const res = await fetch(API + '/api/models/list') + const data = await res.json() + this.models = data.models || [] + // 初始化 poseForm + this.models.forEach(m => { + if (!this.poseForm[m.id]) { + this.poseForm[m.id] = { name: '', photo_type: 'front', description: '' } + } + }) + }, + async addModel() { + const res = await fetch(API + '/api/models/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: this.newModelName || 'model_' + (this.models.length + 1), + serial_prefix: this.newModelSerial, + description: '' + }) + }) + const data = await res.json() + if (data.ok) { + await this.loadAllModels() + this.newModelName = '' + this.newModelSerial = '' + } + }, + async deleteModel(modelId) { + if (!confirm('确定删除该机型?其下所有姿态将被删除!')) return + await fetch(API + '/api/models/delete/' + modelId, { method: 'DELETE' }) + await this.loadAllModels() + }, + // === 姿态管理(属于机型)=== + async addPose(modelId) { + const form = this.poseForm[modelId] + if (!form) return + await fetch(API + '/api/models/poses/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model_id: modelId, + name: form.name || '姿态' + ((this.getModel(modelId)?.poses?.length || 0) + 1), + photo_type: form.photo_type, + arm_angles: this.currentAngles, + speed: 500, + description: form.description || '' + }) + }) + await this.loadAllModels() + form.name = '' + form.description = '' + }, + async deletePose(modelId, poseId) { + if (!confirm('确定删除该姿态?')) return + await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' }) + await this.loadAllModels() + }, + getModel(id) { + return this.models.find(m => m.id === id) + }, + // === 机械臂 === + + clearSelection() { this.selectedMachine = null }, + async saveMachineCoords() { + if (!this.selectedMachine) return + try { + const res = await fetch(API + '/api/mission/machines/' + this.selectedMachine.id, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + front: this.selectedMachine.front, + back: this.selectedMachine.back + }) + }) + if (res.ok) { + this.mapMsg = '✅ 机器坐标已保存' + setTimeout(() => this.mapMsg = '', 2000) + } else { + alert('保存失败: ' + res.status) + } + } catch (e) { alert('保存失败: ' + e.message) } + }, + selectMachine(machine) { + // 确保 front/back 永远有 coords 数组,避免 v-model 赋值失败 + if (!machine.front) machine.front = { coords: [0, 0, 0], poses: [] } + else if (!Array.isArray(machine.front.coords)) machine.front.coords = [0, 0, 0] + if (!machine.back) machine.back = { coords: [0, 0, 0], poses: [] } + else if (!Array.isArray(machine.back.coords)) machine.back.coords = [0, 0, 0] + this.selectedMachine = machine + console.log('selectedMachine:', machine) + }, + onCellClick(ri, ci) { + let m = this.getMachineAt(ri, ci) + if (!m) { + // 自动创建机器记录 + this.createMachine(ri, ci).then(() => { + m = this.getMachineAt(ri, ci) + if (m) this.selectMachine(m) + }) + } else { + this.selectMachine(m) + } + }, + async createMachine(ri, ci) { + try { + const res = await fetch(API + '/api/mission/machines', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ row: ri, col: ci, front: { coords: [0, 0, 0], poses: [] }, back: { coords: [0, 0, 0], poses: [] } }) + }) + await this.loadAllMachines() + return res.ok + } catch (e) { alert('创建机器失败: ' + e.message); return false } + }, + + async loadAllMachines() { + try { + const res = await fetch(API + '/api/mission/machines') + const data = await res.json() + this.missionConfig.machines = data.machines || [] + } catch (e) { console.error('加载机器列表失败', e) } + }, + getMachineAt(ri, ci) { + if (!this.missionConfig.machines) return null + return this.missionConfig.machines.find(m => m.row === ri && m.col === ci) || null + }, + getPositionAt(ri, ci) { + if (!this.missionConfig.machines) return null + const machine = this.getMachineAt(ri, ci) + if (!machine) return null + if (ri === 0) return machine.front + const prevMachine = this.getMachineAt(ri - 1, ci) + return prevMachine ? prevMachine.back : machine.front + }, + async refreshAngles() { + if (!this.armConnected) return + try { + const res = await fetch(API + '/api/arm/get_angles') + const data = await res.json() + if (data.ok && data.angles) { + this.currentAngles = data.angles + this.angleInputs = [...data.angles] + } + } catch (e) {} + }, + async setAngle(idx, val) { + await fetch(API + '/api/arm/set_angle', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ joint: 'J' + (idx + 1), angle: val }) + }) + }, + async applyAngles() { + await fetch(API + '/api/arm/set_angles', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ angles: this.angleInputs, speed: 500 }) + }) + }, + jogStart(idx, dir) { + const joint = 'J' + (idx + 1) + fetch(API + '/api/arm/jog', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ joint, direction: dir }) + }) + this.jogIntervals[idx] = setInterval(() => this.refreshAngles(), 200) + }, + jogStop(idx) { + clearInterval(this.jogIntervals[idx]) + const joint = 'J' + (idx + 1) + fetch(API + '/api/arm/jog', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ joint, direction: 0 }) + }) + setTimeout(() => this.refreshAngles(), 300) + }, + onPreviewError(e) { + e.target.style.display = 'none' + }, + // === AGV 控制 === + async refreshAgvPosition() { + if (!this.agvConnected) return + try { + const res = await fetch(API + '/api/agv/position') + const data = await res.json() + if (data.ok) { + this.agvPosition = data.position + this.agvBattery = data.battery + } + } catch (e) {} + }, + agvMoveStart(dir) { + if (!this.agvConnected) return + fetch(API + '/api/agv/move', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ direction: dir, speed: this.agvSpeed }) + }) + }, + agvMoveStop() { + fetch(API + '/api/agv/move', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ direction: 'stop' }) + }) + }, + async agvStop() { + await fetch(API + '/api/agv/stop', { method: 'POST' }) + }, + async agvResetCollision() { + if (!this.agvConnected) { + alert('AGV 未连接') + return + } + if (!confirm('确定执行撞物体后复位?')) return + try { + const res = await fetch(API + '/api/agv/reset', { method: 'POST' }) + const data = await res.json() + if (data.ok) { + alert('✅ ' + data.message) + await this.refresh() + await this.refreshAgvPosition() + } else { + alert('❌ 复位失败: ' + (data.error || '未知错误')) + } + } catch (e) { + alert('❌ 复位请求失败: ' + e.message) + } + }, + async capturePosition(ri, ci, side) { + if (!this.agvConnected) { alert('请先连接AGV'); return } + let machine = this.getMachineAt(ri, ci) + if (!machine) { + try { + const res = await fetch(API + '/api/mission/machines', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ row: ri, col: ci, front: { coords: [0, 0, 0], poses: [] }, back: { coords: [0, 0, 0], poses: [] } }) + }) + if (!res.ok) throw new Error('创建失败') + await this.loadAllMachines() + machine = this.getMachineAt(ri, ci) + } catch (e) { alert('创建机器失败: ' + e.message); return } + } + try { + const res = await fetch(API + '/api/agv/position') + const pos = await res.json() + let x = 0, y = 0, theta = 0 + if (pos.position && Array.isArray(pos.position)) { x = pos.position[0]||0; y = pos.position[1]||0; theta = pos.position[2]||0 } + if (!machine) { machine = this.getMachineAt(ri, ci) } + if (!machine) { alert('机器记录不存在'); return } + if (side === 'front') { machine.front.coords = [x, y, theta] } else { machine.back.coords = [x, y, theta] } + await fetch(API + '/api/mission/machines/' + machine.id, { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(machine) + }) + alert((side==='front'?'正面':'背面')+'点位已更新: ('+x.toFixed(2)+','+y.toFixed(2)+','+theta.toFixed(2)+')') + } catch (e) { alert('读取位置失败: '+e.message) } + }, + } +}).mount('#app') diff --git a/agv_app/utils/__init__.py b/agv_app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agv_app/utils/agv_controller.py b/agv_app/utils/agv_controller.py new file mode 100644 index 0000000..8ce8d23 --- /dev/null +++ b/agv_app/utils/agv_controller.py @@ -0,0 +1,161 @@ +""" +AGV 导航控制模块 - 通过 pymycobot 控制 AGV 运动 +""" +import time +import logging +from typing import Tuple, Optional, List + +logger = logging.getLogger(__name__) + +# 尝试导入 pymycobot +try: + from pymycobot import MyAGVPro + MYCOBOT_AVAILABLE = True +except ImportError: + MYCOBOT_AVAILABLE = False + logger.warning("pymycobot 未安装,AGV 控制功能不可用") + + +class AGVController: + """AGV 运动控制""" + + def __init__(self, device: str = "/dev/agvpro_controller", baudrate: int = 1000000): + self.device = device + self.baudrate = baudrate + self._agv: Optional[MyAGVPro] = None + self._connected = False + + def connect(self) -> bool: + """连接 AGV""" + if not MYCOBOT_AVAILABLE: + logger.error("pymycobot 不可用") + return False + try: + self._agv = MyAGVPro(self.device, self.baudrate, debug=False) + # 检查是否上电 + if self._agv.is_power_on(): + self._connected = True + logger.info("AGV 连接成功") + return True + else: + logger.warning("AGV 未上电,尝试上电...") + self._agv.power_on() + time.sleep(2) + if self._agv.is_power_on(): + self._connected = True + return True + return False + except Exception as e: + logger.error(f"AGV 连接失败: {e}") + return False + + def is_connected(self) -> bool: + return self._connected and self._agv is not None + + def move_forward(self, speed: float = 0.5, duration: float = None): + """前进""" + if not self.is_connected(): + return + self._agv.move_forward(speed) + if duration: + time.sleep(duration) + self.stop() + + def move_backward(self, speed: float = 0.5, duration: float = None): + """后退""" + if not self.is_connected(): + return + self._agv.move_backward(speed) + if duration: + time.sleep(duration) + self.stop() + + def turn_left(self, speed: float = 0.5, duration: float = None): + """左转""" + if not self.is_connected(): + return + self._agv.turn_left(speed) + if duration: + time.sleep(duration) + self.stop() + + def turn_right(self, speed: float = 0.5, duration: float = None): + """右转""" + if not self.is_connected(): + return + self._agv.turn_right(speed) + if duration: + time.sleep(duration) + self.stop() + + def move_left_lateral(self, speed: float = 0.5, duration: float = None): + """向左横向移动""" + if not self.is_connected(): + return + self._agv.move_left_lateral(speed) + if duration: + time.sleep(duration) + self.stop() + + def move_right_lateral(self, speed: float = 0.5, duration: float = None): + """向右横向移动""" + if not self.is_connected(): + return + self._agv.move_right_lateral(speed) + if duration: + time.sleep(duration) + self.stop() + + def stop(self): + """停止""" + if self.is_connected(): + self._agv.stop() + + def get_position(self) -> Optional[List[float]]: + """获取 AGV 当前位置 [x, y, rz]""" + if not self.is_connected(): + return None + try: + # 启用自动报告以获取位置 + self._agv.set_auto_report_state(1) + time.sleep(0.5) + msg = self._agv.get_auto_report_message() + if msg and len(msg) >= 3: + return [msg[0], msg[1], msg[2]] + except Exception as e: + logger.error(f"获取 AGV 位置失败: {e}") + return None + + def go_to_point(self, x: float, y: float, rz: float = None, speed: float = 0.5) -> bool: + """移动到目标点(简单的方向控制实现)""" + # 注意:AGV Pro 的 pymycobot 没有直接 goto API + # 需要 ROS2 SLAM 导航支持,此处提供基础运动接口 + # 实际导航需要结合地图和路径规划 + logger.warning("go_to_point 需要 ROS2 导航支持,当前仅记录目标") + return True + + def get_battery(self) -> Optional[float]: + """获取电池电压""" + if not self.is_connected(): + return None + try: + self._agv.set_auto_report_state(1) + msg = self._agv.get_auto_report_message() + if msg and len(msg) > 5: + return msg[5] # 电池电压 + except: + pass + return None + + def disconnect(self): + if self._agv: + self.stop() + self._agv = None + self._connected = False + + def __enter__(self): + self.connect() + return self + + def __exit__(self, *args): + self.disconnect() \ No newline at end of file diff --git a/agv_app/utils/agv_controller_ros2.py b/agv_app/utils/agv_controller_ros2.py new file mode 100644 index 0000000..e2ea2c4 --- /dev/null +++ b/agv_app/utils/agv_controller_ros2.py @@ -0,0 +1,236 @@ +""" +AGV 导航控制模块 - 通过 ROS2 控制 AGV 运动 +使用 ros2 CLI 命令进行通信,避免 rclpy 导入问题 +""" +import time +import subprocess +import json +import logging +import math +from typing import Tuple, Optional, List + +logger = logging.getLogger(__name__) + +# ROS2 环境设置 +ROS2_SETUP_CMD = "export ROS_DOMAIN_ID=1 && source ~/agv_pro_ros2/install/setup.bash" + + +class AGVController: + """AGV 运动控制 - ROS2 版本""" + + def __init__(self, device: str = "/dev/agvpro_controller", baudrate: int = 1000000): + self.device = device + self.baudrate = baudrate + self._connected = False + self._position = [0.0, 0.0, 0.0] # [x, y, yaw] + self._voltage = 0.0 + self._ros2_available = False + + def _run_ros2_cmd(self, cmd: str, timeout: float = 5.0) -> tuple: + """执行 ros2 命令""" + full_cmd = f"bash -c '{ROS2_SETUP_CMD} && {cmd}'" + try: + result = subprocess.run( + full_cmd, + shell=True, + capture_output=True, + text=True, + timeout=timeout + ) + return result.returncode, result.stdout.strip(), result.stderr.strip() + except subprocess.TimeoutExpired: + return -1, "", "Timeout" + except Exception as e: + return -1, "", str(e) + + def connect(self) -> bool: + """连接 AGV - 检查 ROS2 节点和 topic""" + try: + # 检查 agv_pro_node 是否运行 + rc, out, err = self._run_ros2_cmd("ros2 node list") + if rc != 0: + logger.error(f"ROS2 节点列表获取失败: {err}") + return False + + if "/agv_pro_node" not in out: + logger.error("agv_pro_node 未运行") + return False + + # 检查 /odom topic + rc, out, err = self._run_ros2_cmd("ros2 topic list") + if "/odom" not in out: + logger.error("/odom topic 不存在") + return False + + # 尝试获取一次位置数据 + rc, out, err = self._run_ros2_cmd( + "timeout 5 ros2 topic echo /odom 2>timeout 10 ros2 topic echo /odom --once 2>/dev/null1 | head -1", + timeout=6 + ) + + if rc == 0 and out: + self._connected = True + self._ros2_available = True + logger.info("AGV ROS2 连接成功") + return True + else: + # /odom 可能暂时没数据,但节点存在也算连接成功 + self._connected = True + self._ros2_available = True + logger.info("AGV ROS2 连接成功 (节点存在,等待 odom 数据)") + return True + + except Exception as e: + logger.error(f"AGV 连接失败: {e}") + return False + + def is_connected(self) -> bool: + return self._connected + + def _publish_cmd_vel(self, linear_x: float = 0.0, linear_y: float = 0.0, angular_z: float = 0.0): + """发布速度命令到 /cmd_vel""" + # 直接执行,避免引号嵌套问题 + msg = f'{{"linear": {{"x": {linear_x}, "y": {linear_y}, "z": 0.0}}, "angular": {{"x": 0.0, "y": 0.0, "z": {angular_z}}}}}' + full_cmd = f"bash -c '{ROS2_SETUP_CMD} && ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \"{msg}\" --once'" + try: + result = subprocess.run( + full_cmd, + shell=True, + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode != 0: + logger.warning(f"发布 cmd_vel 失败: {result.stderr.strip()}") + except subprocess.TimeoutExpired: + logger.warning("发布 cmd_vel 超时") + except Exception as e: + logger.warning(f"发布 cmd_vel 失败: {e}") + + def move_forward(self, speed: float = 0.5, duration: float = None): + """前进""" + if not self.is_connected(): + return + self._publish_cmd_vel(linear_x=speed) + if duration: + time.sleep(duration) + self.stop() + + def move_backward(self, speed: float = 0.5, duration: float = None): + """后退""" + if not self.is_connected(): + return + self._publish_cmd_vel(linear_x=-speed) + if duration: + time.sleep(duration) + self.stop() + + def turn_left(self, speed: float = 0.5, duration: float = None): + """左转""" + if not self.is_connected(): + return + self._publish_cmd_vel(angular_z=speed) + if duration: + time.sleep(duration) + self.stop() + + def turn_right(self, speed: float = 0.5, duration: float = None): + """右转""" + if not self.is_connected(): + return + self._publish_cmd_vel(angular_z=-speed) + if duration: + time.sleep(duration) + self.stop() + + def move_left_lateral(self, speed: float = 0.5, duration: float = None): + """向左横向移动""" + if not self.is_connected(): + return + self._publish_cmd_vel(linear_y=speed) + if duration: + time.sleep(duration) + self.stop() + + def move_right_lateral(self, speed: float = 0.5, duration: float = None): + """向右横向移动""" + if not self.is_connected(): + return + self._publish_cmd_vel(linear_y=-speed) + if duration: + time.sleep(duration) + self.stop() + + def stop(self): + """停止""" + if self.is_connected(): + self._publish_cmd_vel(0, 0, 0) + + def get_position(self) -> Optional[List[float]]: + """获取 AGV 当前位置 [x, y, yaw]""" + if not self.is_connected(): + return None + try: + # 从 /odom topic 获取位置 + rc, out, err = self._run_ros2_cmd( + "timeout 5 ros2 topic echo /odom 2>timeout 10 ros2 topic echo /odom --once 2>/dev/null1 | head -1", + timeout=6 + ) + if rc == 0 and out: + # 解析 odom 消息 (YAML 格式) + # ros2 topic echo 输出可能含多个 --- 分隔的文档,只取第一个 + import yaml + yaml_str = out.split('---')[0] + data = yaml.safe_load(yaml_str) + if data: + pos = data.get("pose", {}).get("pose", {}).get("position", {}) + x = pos.get("x", 0.0) + y = pos.get("y", 0.0) + # 从四元数计算 yaw + orient = data.get("pose", {}).get("pose", {}).get("orientation", {}) + qz = orient.get("z", 0.0) + qw = orient.get("w", 1.0) + yaw = math.atan2(2.0 * qw * qz, 1.0 - 2.0 * qz * qz) + self._position = [x, y, yaw] + return self._position + except Exception as e: + logger.debug(f"获取位置失败: {e}") + return None + + def go_to_point(self, x: float, y: float, rz: float = None, speed: float = 0.5) -> bool: + """移动到目标点(需要 ROS2 导航栈)""" + logger.warning("go_to_point 需要 ROS2 Nav2 支持,当前仅记录目标") + return True + + def get_battery(self) -> Optional[float]: + """获取电池电压""" + if not self.is_connected(): + return None + try: + # 从 /voltage topic 获取电压 + rc, out, err = self._run_ros2_cmd( + "timeout 5 ros2 topic echo /voltage 2>timeout 10 ros2 topic echo /voltage --once 2>/dev/null1 | head -1", + timeout=6 + ) + if rc == 0 and out: + # 解析电压消息(ros2 topic echo 可能输出多文档 YAML) + import yaml + yaml_str = out.split('---')[0] + data = yaml.safe_load(yaml_str) + if data: + self._voltage = data.get("data", 0.0) + return self._voltage + except Exception as e: + logger.debug(f"获取电压失败: {e}") + return None + + def disconnect(self): + self.stop() + self._connected = False + + def __enter__(self): + self.connect() + return self + + def __exit__(self, *args): + self.disconnect() diff --git a/agv_app/utils/arm_client.py b/agv_app/utils/arm_client.py new file mode 100644 index 0000000..e58ed5f --- /dev/null +++ b/agv_app/utils/arm_client.py @@ -0,0 +1,161 @@ +""" +机械臂通信客户端 - 通过 TCP 连接机械臂端 TCP 服务器 +服务器再转发给 RoboFlow (630 Socket API) +""" +import socket +import time +import logging +from typing import List, Optional, Tuple + +logger = logging.getLogger(__name__) + + +class ArmClient: + """TCP 客户端,连接机械臂端的 arm_server""" + + def __init__(self, host: str, port: int, timeout: float = 10): + self.host = host + self.port = port + self.timeout = timeout + self._sock: Optional[socket.socket] = None + + def connect(self) -> bool: + """建立 TCP 连接""" + try: + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.settimeout(self.timeout) + self._sock.connect((self.host, self.port)) + logger.info(f"已连接到机械臂 {self.host}:{self.port}") + return True + except Exception as e: + logger.error(f"连接机械臂失败: {e}") + return False + + def send_command(self, cmd: str) -> Tuple[bool, str]: + """发送命令并接收响应""" + if not self._sock: + return False, "未连接" + try: + # 发送命令(自动加换行) + self._sock.sendall((cmd + "\n").encode("utf-8")) + # 接收响应 + resp = self._sock.recv(1024).decode("utf-8").strip() + return True, resp + except socket.timeout: + return False, "命令超时" + except Exception as e: + return False, str(e) + + def close(self): + if self._sock: + self._sock.close() + self._sock = None + + def reconnect(self) -> bool: + self.close() + time.sleep(1) + return self.connect() + + # ========== 封装机械臂命令 ========== + + def get_angles(self) -> Tuple[bool, List[float]]: + """获取所有关节角度""" + ok, resp = self.send_command("get_angles()") + if ok and resp.startswith("get_angles:["): + try: + # get_angles:[0.174, 0.520, ...] → list + nums = resp.split("[")[1].split("]")[0] + angles = [float(x) for x in nums.split(",")] + return True, angles + except: + return False, [] + return False, [] + + def set_angles(self, angles: List[float], speed: int = 500) -> bool: + """设置所有关节角度""" + if len(angles) != 6: + return False + cmd = f"set_angles({angles[0]:.2f},{angles[1]:.2f},{angles[2]:.2f},{angles[3]:.2f},{angles[4]:.2f},{angles[5]:.2f},{speed})" + ok, resp = self.send_command(cmd) + return ok and "ok" in resp + + def set_angle(self, joint: str, angle: float, speed: int = 500) -> bool: + """设置单个关节角度""" + cmd = f"set_angle({joint},{angle:.2f},{speed})" + ok, resp = self.send_command(cmd) + return ok and "ok" in resp + + def jog_angle(self, joint: str, direction: int, speed: int = 500) -> bool: + """连续调节关节角度(direction: -1负方向/0停止/1正方向)""" + cmd = f"jog_angle({joint},{direction},{speed})" + ok, resp = self.send_command(cmd) + return ok + + def get_coords(self) -> Tuple[bool, List[float]]: + """获取当前坐标和姿态 [x, y, z, rx, ry, rz]""" + ok, resp = self.send_command("get_coords()") + if ok and "get_coords:" in resp: + try: + nums = resp.split("[")[1].split("]")[0] + coords = [float(x) for x in nums.split(",")] + return True, coords + except: + return False, [] + return False, [] + + def set_coords(self, coords: List[float], speed: int = 500) -> bool: + """设置坐标和姿态""" + if len(coords) != 6: + return False + cmd = f"set_coords({coords[0]:.2f},{coords[1]:.2f},{coords[2]:.2f},{coords[3]:.2f},{coords[4]:.2f},{coords[5]:.2f},{speed})" + ok, resp = self.send_command(cmd) + return ok and "ok" in resp + + def jog_coord(self, axis: str, direction: int, speed: int = 500) -> bool: + """连续调节坐标轴""" + cmd = f"jog_coord({axis},{direction},{speed})" + ok, resp = self.send_command(cmd) + return ok + + def power_on(self) -> bool: + ok, _ = self.send_command("power_on()") + return ok + + def state_on(self) -> bool: + ok, _ = self.send_command("state_on()") + return ok + + def state_off(self) -> bool: + ok, _ = self.send_command("state_off()") + return ok + + def state_check(self) -> bool: + """检查机械臂状态是否正常""" + ok, resp = self.send_command("state_check()") + return ok and resp == "state_check:1" + + def check_running(self) -> bool: + """检查机械臂是否在运行""" + ok, resp = self.send_command("check_running()") + return ok and resp == "check_running:1" + + def wait_done(self, timeout: float = 30) -> bool: + """等待上一条命令执行完成""" + start = time.time() + while time.time() - start < timeout: + ok, resp = self.send_command("check_running()") + if ok and resp == "check_running:0": + return True + time.sleep(0.5) + return False + + def task_stop(self) -> bool: + ok, _ = self.send_command("task_stop()") + return ok + + def __enter__(self): + self.connect() + return self + + def __exit__(self, *args): + self.close() \ No newline at end of file diff --git a/agv_app/utils/config.py b/agv_app/utils/config.py new file mode 100644 index 0000000..85b01b5 --- /dev/null +++ b/agv_app/utils/config.py @@ -0,0 +1,87 @@ +""" +配置文件 - 所有可配置参数集中管理 +""" +import os + +# 基础路径(部署后对应 ~/work/agv_app) +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +# ========== AGV 参数 ========== +AGV_CONFIG = { + "device": "/dev/agvpro_controller", + "baudrate": 10000000, + "move_speed": 0.5, + "turn_speed": 0.5, +} + +# ========== 机械臂 TCP 客户端 ========== +ARM_CONFIG = { + "host": "192.168.110.164", + "port": 5002, + "timeout": 8, + "retry_times": 3, + "retry_interval": 1, +} + +# ========== 地图 ========== +MAP_CONFIG = { + "map_dir": "/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/", + "map_file": "map.yaml", +} + +# ========== 摄像头 ========== +CAMERA_CONFIG = { + "device_index": 4, # AGV 摄像头 video4(标准彩色摄像头,V4L2后端) + "backend": "v4l2", # 使用 V4L2 后端获取标准彩色格式(640x480) + "qr_detect_interval": 0.5, + "capture_delay": 0.5, +} + +# ========== 机械臂摄像头流 ========== +ARM_CAMERA_CONFIG = { + "url": "http://192.168.110.164:5003/api/camera/preview", +} + +# ========== HTTP 上传 ========== +UPLOAD_CONFIG = { + "url": "https://ts.zhijian168.com/prod-api/file/uploadImage", + "timeout": 30, + "max_retries": 3, +} + +# ========== Flask 服务器 ========== +SERVER_CONFIG = { + "host": "0.0.0.0", + "port": 5000, + "secret_key": "agv630_secret_key_2024", + "debug": False, +} + +# ========== 任务配置存储路径 ========== +DATA_DIR = os.path.join(BASE_DIR, "data") +os.makedirs(DATA_DIR, exist_ok=True) + +# ========== 关节角度范围限制 ========== +JOINT_LIMITS = { + "J1": (-180.0, 180.0), + "J2": (-270.0, 90.0), + "J3": (-150.0, 150.0), + "J4": (-260.0, 80.0), + "J5": (-168.0, 168.0), + "J6": (-174.0, 174.0), +} + +# ========== 机械臂默认速度 ========== +DEFAULT_ARM_SPEED = 500 + +# ========== 状态定义 ========== +class State: + SETTING = "setting" + RUNNING = "running" + PAUSED = "paused" + IDLE = "idle" + +class PhotoType: + FRONT = "front" + BACK = "back" + NAMEPLATE = "nameplate" \ No newline at end of file diff --git a/agv_app/utils/image_uploader.py b/agv_app/utils/image_uploader.py new file mode 100644 index 0000000..39e8bf2 --- /dev/null +++ b/agv_app/utils/image_uploader.py @@ -0,0 +1,76 @@ +""" +HTTP 上传模块 - 将图片上传到指定服务器 +""" +import os +import time +import logging +import requests +from typing import Optional +import uuid + +logger = logging.getLogger(__name__) + + +class ImageUploader: + """图片上传器""" + + def __init__(self, upload_url: str, timeout: int = 30, max_retries: int = 3): + self.upload_url = upload_url + self.timeout = timeout + self.max_retries = max_retries + + def upload(self, image_path: str, serial_number: str, photo_index: int, + photo_type: str = "front") -> Optional[str]: + """ + 上传单张图片 + 返回: 服务器返回的消息(成功时),失败返回 None + """ + if not os.path.exists(image_path): + logger.error(f"图片文件不存在: {image_path}") + return None + + for attempt in range(self.max_retries): + try: + with open(image_path, "rb") as f: + files = {"file": (os.path.basename(image_path), f, "image/jpeg")} + data = { + "serialNumber": serial_number, + "index": photo_index + } + resp = requests.post( + self.upload_url, + files=files, + data=data, + timeout=self.timeout + ) + + if resp.status_code == 200: + logger.info(f"图片上传成功: {serialNumber} #{photo_index} ({photo_type})") + try: + return resp.json().get("msg", "success") + except: + return resp.text + else: + logger.warning(f"上传失败 [{resp.status_code}]: {resp.text[:100]}") + + except requests.exceptions.RequestException as e: + logger.warning(f"上传异常 (尝试 {attempt+1}/{self.max_retries}): {e}") + if attempt < self.max_retries - 1: + time.sleep(2) + + logger.error(f"图片上传最终失败: {image_path}") + return None + + def upload_batch(self, image_paths: list, serial_number: str, + start_index: int = 0) -> dict: + """批量上传图片""" + results = [] + for i, path in enumerate(image_paths): + result = self.upload(path, serial_number, start_index + i) + results.append({ + "index": start_index + i, + "path": path, + "success": result is not None, + "msg": result + }) + return results \ No newline at end of file diff --git a/agv_app/utils/map_navigator.py b/agv_app/utils/map_navigator.py new file mode 100644 index 0000000..cc266aa --- /dev/null +++ b/agv_app/utils/map_navigator.py @@ -0,0 +1,663 @@ +""" +地图导航模块 - A* 路径规划 + Pure Pursuit 路径跟踪 +在已知地图上规划路径,控制 AGV 自动导航到目标坐标 + +依赖:numpy, cv2, Pillow(均已安装在 AGV 上) +不依赖:激光雷达、SLAM、Nav2 +""" + +import os +import math +import heapq +import time +import logging +import threading +import subprocess +import numpy as np +import cv2 +import yaml +from typing import List, Tuple, Optional, Dict +from enum import Enum + +logger = logging.getLogger(__name__) + +# ROS2 环境设置(与 agv_controller_ros2.py 保持一致) +ROS2_SETUP_CMD = "export ROS_DOMAIN_ID=0 && source ~/agv_pro_ros2/install/setup.bash" + + +# ========== 坐标转换 ========== + +class CoordTransformer: + """地图世界坐标 ↔ 栅格坐标 双向转换""" + + def __init__(self, resolution: float, origin: List[float], width: int, height: int): + """ + Args: + resolution: 地图分辨率(米/像素) + origin: [x, y, yaw] 地图原点在世界坐标系中的位置 + width: 地图宽度(像素) + height: 地图高度(像素) + """ + self.resolution = resolution + self.origin = origin # [ox, oy, oyaw] + self.width = width + self.height = height + + def world_to_grid(self, wx: float, wy: float) -> Tuple[int, int]: + """世界坐标 → 栅格坐标 [col, row]""" + col = int((wx - self.origin[0]) / self.resolution) + row = int((wy - self.origin[1]) / self.resolution) + # ROS 地图 row=0 对应图像最上方(y 最大值),需要翻转 + row = self.height - 1 - row + return (col, row) + + def grid_to_world(self, col: int, row: int) -> Tuple[float, float]: + """栅格坐标 [col, row] → 世界坐标 [x, y]""" + # 翻转 row + actual_row = self.height - 1 - row + wx = col * self.resolution + self.origin[0] + wy = actual_row * self.resolution + self.origin[1] + return (wx, wy) + + def world_to_grid_center(self, wx: float, wy: float) -> Tuple[float, float]: + """世界坐标 → 栅格中心的世界坐标(对齐到栅格)""" + col, row = self.world_to_grid(wx, wy) + return self.grid_to_world(col, row) + + +# ========== A* 路径规划 ========== + +class AStarPlanner: + """A* 路径规划器,在栅格地图上规划最短路径""" + + # 8方向移动:右、左、下、上、右下、右上、左下、左上 + DIRECTIONS = [ + (1, 0), (-1, 0), (0, 1), (0, -1), + (1, 1), (1, -1), (-1, 1), (-1, -1) + ] + # 对角线移动的代价乘数(sqrt(2)) + DIR_COSTS = [1.0, 1.0, 1.0, 1.0, 1.414, 1.414, 1.414, 1.414] + + def __init__(self, occupancy_grid: np.ndarray, inflation_radius: int = 3): + """ + Args: + occupancy_grid: 栅格地图,0=空闲,255=障碍物 + inflation_radius: 障碍物膨胀半径(像素),AGV 有一定体积不能贴墙走 + """ + self.grid = occupancy_grid + self.height, self.width = occupancy_grid.shape + self.inflated = self._inflate(inflation_radius) + + def _inflate(self, radius: int) -> np.ndarray: + """膨胀障碍物区域""" + if radius <= 0: + return self.grid.copy() + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * radius + 1, 2 * radius + 1)) + inflated = cv2.dilate(self.grid, kernel, iterations=1) + # 确保二值化 + inflated = np.where(inflated > 50, 255, 0).astype(np.uint8) + return inflated + + def plan(self, start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: + """ + A* 路径规划 + + Args: + start: 起点栅格坐标 (col, row) + goal: 终点栅格坐标 (col, row) + + Returns: + 路径点列表 [(col, row), ...],包含起点和终点;无法规划时返回 None + """ + # 边界检查 + if not self._is_valid(start) or not self._is_valid(goal): + logger.warning(f"起点或终点无效: start={start}, goal={goal}") + # 尝试找最近的可行点 + start = self._find_nearest_free(start) + goal = self._find_nearest_free(goal) + if start is None or goal is None: + logger.error("无法找到有效的起点或终点") + return None + + # 检查终点是否被障碍物包围 + if self.inflated[goal[1], goal[0]] > 50: + goal = self._find_nearest_free(goal) + + if goal is None: + logger.error("终点周围无可行区域") + return None + + # A* 算法 + open_set = [] + heapq.heappush(open_set, (0.0, start)) + came_from = {} + g_score = {start: 0.0} + closed_set = set() + + while open_set: + _, current = heapq.heappop(open_set) + + if current in closed_set: + continue + closed_set.add(current) + + if current == goal: + # 回溯路径 + path = [] + while current in came_from: + path.append(current) + current = came_from[current] + path.append(start) + path.reverse() + return path + + for i, (dx, dy) in enumerate(self.DIRECTIONS): + neighbor = (current[0] + dx, current[1] + dy) + + if neighbor in closed_set: + continue + + if not self._is_valid(neighbor): + continue + + if self.inflated[neighbor[1], neighbor[0]] > 50: + continue + + move_cost = self.DIR_COSTS[i] + tentative_g = g_score[current] + move_cost + + if tentative_g < g_score.get(neighbor, float('inf')): + came_from[neighbor] = current + g_score[neighbor] = tentative_g + f_score = tentative_g + self._heuristic(neighbor, goal) + heapq.heappush(open_set, (f_score, neighbor)) + + logger.warning("A* 无法找到路径") + return None + + def _heuristic(self, a: Tuple[int, int], b: Tuple[int, int]) -> float: + """对角线距离启发式""" + dx = abs(a[0] - b[0]) + dy = abs(a[1] - b[1]) + return max(dx, dy) + (1.414 - 1) * min(dx, dy) + + def _is_valid(self, pos: Tuple[int, int]) -> bool: + return 0 <= pos[0] < self.width and 0 <= pos[1] < self.height + + def _find_nearest_free(self, pos: Tuple[int, int], max_dist: int = 10) -> Optional[Tuple[int, int]]: + """在 pos 附近找最近的可行点""" + for r in range(1, max_dist + 1): + for dx in range(-r, r + 1): + for dy in range(-r, r + 1): + n = (pos[0] + dx, pos[1] + dy) + if self._is_valid(n) and self.inflated[n[1], n[0]] == 0: + return n + return None + + +# ========== 路径平滑 ========== + +def smooth_path(grid: np.ndarray, path: List[Tuple[int, int]], + weight_data: float = 0.3, weight_smooth: float = 0.5, + tolerance: float = 1e-5, max_iter: int = 500) -> List[Tuple[int, int]]: + """ + 路径平滑(梯度下降法) + 在障碍物约束下让路径更平滑,减少不必要的转向 + """ + if len(path) <= 2: + return path + + height, width = grid.shape + new_path = [list(p) for p in path] + + for iteration in range(max_iter): + change = 0.0 + for i in range(1, len(new_path) - 1): + for j in range(2): + old_val = new_path[i][j] + # 数据项:趋向原始路径点 + data_gradient = weight_data * (path[i][j] - new_path[i][j]) + # 平滑项:趋向邻居中点 + smooth_gradient = weight_smooth * ( + new_path[i - 1][j] + new_path[i + 1][j] - 2 * new_path[i][j] + ) + new_path[i][j] += data_gradient + smooth_gradient + + # 边界约束 + new_path[i][0] = max(0, min(width - 1, new_path[i][0])) + new_path[i][1] = max(0, min(height - 1, new_path[i][1])) + + # 障碍物约束 + col, row = int(round(new_path[i][0])), int(round(new_path[i][1])) + if 0 <= col < width and 0 <= row < height: + if grid[row, col] > 50: + new_path[i][j] = old_val # 回退 + + change += abs(new_path[i][j] - old_val) + + if change < tolerance: + break + + return [(int(round(p[0])), int(round(p[1]))) for p in new_path] + + +# ========== 路径降采样 ========== + +def downsample_path(path: List[Tuple[int, int]], min_dist: int = 3) -> List[Tuple[int, int]]: + """降采样路径,移除过近的点,减少 cmd_vel 发布频率""" + if len(path) <= 2: + return path + + result = [path[0]] + for p in path[1:]: + last = result[-1] + dist = math.hypot(p[0] - last[0], p[1] - last[1]) + if dist >= min_dist: + result.append(p) + # 确保终点包含在内 + if result[-1] != path[-1]: + result.append(path[-1]) + return result + + +# ========== Pure Pursuit 控制器 ========== + +class PurePursuitController: + """Pure Pursuit 路径跟踪控制器""" + + def __init__(self, lookahead_distance: float = 0.3, + max_linear_speed: float = 0.4, + max_angular_speed: float = 0.8, + goal_tolerance: float = 0.15, + slow_down_distance: float = 0.5): + """ + Args: + lookahead_distance: 前视距离(米),越大转弯越平缓 + max_linear_speed: 最大线速度 (m/s) + max_angular_speed: 最大角速度 (rad/s) + goal_tolerance: 到达目标容差(米) + slow_down_distance: 开始减速的距离(米) + """ + self.lookahead_distance = lookahead_distance + self.max_linear_speed = max_linear_speed + self.max_angular_speed = max_angular_speed + self.goal_tolerance = goal_tolerance + self.slow_down_distance = slow_down_distance + self.transformer: Optional[CoordTransformer] = None + + def set_transformer(self, transformer: CoordTransformer): + self.transformer = transformer + + def compute(self, current_pos: Tuple[float, float, float], + path_world: List[Tuple[float, float]]) -> Tuple[float, float, bool]: + """ + 计算控制量 + + Args: + current_pos: (x, y, yaw) 当前世界坐标 + path_world: 路径点列表 [(x, y), ...] 世界坐标 + + Returns: + (linear_x, angular_z, reached) 线速度、角速度、是否到达 + """ + if not path_world: + return (0.0, 0.0, True) + + x, y, yaw = current_pos + + # 检查是否到达终点 + goal = path_world[-1] + dist_to_goal = math.hypot(goal[0] - x, goal[1] - y) + if dist_to_goal < self.goal_tolerance: + return (0.0, 0.0, True) + + # 找前视点(lookahead point) + lookahead_point = self._find_lookahead_point(x, y, path_world) + + if lookahead_point is None: + # 已经越过最后一个点 + return (0.0, 0.0, True) + + lx, ly = lookahead_point + + # 转换到机器人坐标系 + dx = lx - x + dy = ly - y + + # 旋转到机器人坐标系(x 轴朝前) + local_x = dx * math.cos(yaw) + dy * math.sin(yaw) + local_y = -dx * math.sin(yaw) + dy * math.cos(yaw) + + # 弧长 = 角度 * 半径 → curvature = 2 * ly / L^2 + L = math.hypot(local_x, local_y) + if L < 1e-6: + return (0.0, 0.0, True) + + curvature = 2.0 * local_y / (L * L) + angular_z = curvature * self.max_linear_speed + + # 根据距离调整速度 + linear_x = self.max_linear_speed + if dist_to_goal < self.slow_down_distance: + ratio = max(0.15, dist_to_goal / self.slow_down_distance) + linear_x *= ratio + + # 限制角速度 + angular_z = max(-self.max_angular_speed, min(self.max_angular_speed, angular_z)) + + # 如果角度偏差太大,先原位转弯 + angle_to_goal = math.atan2(ly - y, lx - x) - yaw + angle_to_goal = math.atan2(math.sin(angle_to_goal), math.cos(angle_to_goal)) + + if abs(angle_to_goal) > math.pi / 3: + # 角度偏差 > 60°,先原位转弯 + linear_x = 0.0 + angular_z = max(-self.max_angular_speed, min(self.max_angular_speed, angle_to_goal * 1.5)) + + return (linear_x, angular_z, False) + + def _find_lookahead_point(self, x: float, y: float, + path: List[Tuple[float, float]]) -> Optional[Tuple[float, float]]: + """沿路径找到前视距离处的点""" + for i in range(len(path) - 1, -1, -1): + dist = math.hypot(path[i][0] - x, path[i][1] - y) + if dist >= self.lookahead_distance: + return path[i] + # 如果所有点都在前视距离内,返回终点 + return path[-1] if path else None + + +# ========== 导航器(核心模块) ========== + +class NavStatus(Enum): + IDLE = "idle" + PLANNING = "planning" + NAVIGATING = "navigating" + REACHED = "reached" + FAILED = "failed" + CANCELLED = "cancelled" + + +class MapNavigator: + """地图导航器 — 整合路径规划与路径跟踪""" + + def __init__(self, map_yaml_path: str): + """ + Args: + map_yaml_path: map.yaml 文件的绝对路径 + """ + self.map_yaml_path = map_yaml_path + self.transformer: Optional[CoordTransformer] = None + self.planner: Optional[AStarPlanner] = None + self.controller = PurePursuitController() + self.controller.set_transformer(self.transformer) + + # 导航状态 + self.status = NavStatus.IDLE + self._nav_thread: Optional[threading.Thread] = None + self._cancel_event = threading.Event() + + # 当前路径(世界坐标) + self.path_world: List[Tuple[float, float]] = [] + self.current_position = [0.0, 0.0, 0.0] # [x, y, yaw] + + # 加载地图 + self._load_map() + + def _load_map(self): + """加载地图 PGM + YAML""" + with open(self.map_yaml_path, 'r') as f: + meta = yaml.safe_load(f) + + map_dir = os.path.dirname(self.map_yaml_path) + pgm_path = os.path.join(map_dir, meta['image']) + + # 读取 PGM 灰度图 + img = cv2.imread(pgm_path, cv2.IMREAD_GRAYSCALE) + if img is None: + raise FileNotFoundError(f"无法读取地图文件: {pgm_path}") + + # ROS 地图:0=占用(障碍物),254=空闲,205=未知 + # 转为二值:空闲=0,障碍物=255 + self.occupancy = np.where(img <= 50, 255, 0).astype(np.uint8) + # 未知区域(205 附近)也视为障碍物 + self.occupancy = np.where((img > 50) & (img < 250), 255, self.occupancy) + + resolution = meta['resolution'] + origin = meta.get('origin', [0, 0, 0]) + height, width = img.shape + + self.transformer = CoordTransformer(resolution, origin, width, height) + self.planner = AStarPlanner(self.occupancy, inflation_radius=3) + self.controller.set_transformer(self.transformer) + + self._map_meta = meta + logger.info(f"地图加载完成: {width}x{height}, 分辨率 {resolution}m, 原点 {origin}") + + def get_odom(self) -> List[float]: + """从 /odom 话题获取当前位置 [x, y, yaw]""" + try: + cmd = f"timeout 5 ros2 topic echo /odom --once 2>/dev/null" + full_cmd = f"bash -c '{ROS2_SETUP_CMD} && {cmd}'" + result = subprocess.run( + full_cmd, shell=True, capture_output=True, text=True, timeout=6 + ) + if result.returncode == 0 and result.stdout: + yaml_str = result.stdout.split('---')[0] + data = yaml.safe_load(yaml_str) + if data: + pos = data.get("pose", {}).get("pose", {}).get("position", {}) + x, y = pos.get("x", 0.0), pos.get("y", 0.0) + orient = data.get("pose", {}).get("pose", {}).get("orientation", {}) + qz, qw = orient.get("z", 0.0), orient.get("w", 1.0) + yaw = math.atan2(2.0 * qw * qz, 1.0 - 2.0 * qz * qz) + self.current_position = [x, y, yaw] + return self.current_position + except Exception as e: + logger.debug(f"获取 odom 失败: {e}") + return self.current_position + + def _publish_cmd_vel(self, linear_x: float, angular_z: float): + """发布速度命令到 /cmd_vel""" + msg = ( + f'{{"linear": {{"x": {linear_x:.4f}, "y": 0.0, "z": 0.0}}, ' + f'"angular": {{"x": 0.0, "y": 0.0, "z": {angular_z:.4f}}}}}' + ) + full_cmd = f"bash -c '{ROS2_SETUP_CMD} && ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \"{msg}\" --once'" + try: + subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=3) + except subprocess.TimeoutExpired: + logger.warning("发布 cmd_vel 超时") + + def _stop_cmd_vel(self): + """发布停止命令""" + self._publish_cmd_vel(0.0, 0.0) + + def plan_path(self, goal_x: float, goal_y: float, + start_x: float = None, start_y: float = None) -> bool: + """ + 规划路径(不执行导航) + + Args: + goal_x, goal_y: 目标世界坐标(米) + start_x, start_y: 起点世界坐标(米),默认使用当前 odom + + Returns: + 是否规划成功 + """ + if self.transformer is None: + logger.error("地图未加载") + return False + + # 获取起点 + if start_x is None or start_y is None: + pos = self.get_odom() + start_x, start_y = pos[0], pos[1] + + # 坐标转换 + start_grid = self.transformer.world_to_grid(start_x, start_y) + goal_grid = self.transformer.world_to_grid(goal_x, goal_y) + + logger.info(f"规划路径: 起点(世界){start_x:.2f},{start_y:.2f} → (栅格){start_grid}") + logger.info(f" 终点(世界){goal_x:.2f},{goal_y:.2f} → (栅格){goal_grid}") + + # A* 规划 + path_grid = self.planner.plan(start_grid, goal_grid) + if path_grid is None: + logger.warning("路径规划失败") + return False + + # 路径平滑 + path_grid = smooth_path(self.planner.inflated, path_grid) + + # 降采样 + path_grid = downsample_path(path_grid, min_dist=2) + + # 转换为世界坐标 + self.path_world = [self.transformer.grid_to_world(c, r) for c, r in path_grid] + + logger.info(f"路径规划成功: {len(self.path_world)} 个路径点") + return True + + def navigate_to(self, goal_x: float, goal_y, blocking: bool = False) -> bool: + """ + 导航到目标点 + + Args: + goal_x, goal_y: 目标世界坐标(米) + blocking: 是否阻塞等待导航完成 + + Returns: + 非阻塞模式下返回 True(表示已启动),阻塞模式下返回是否到达 + """ + if self.status == NavStatus.NAVIGATING: + logger.warning("导航正在进行中,请先停止当前导航") + return False + + # 规划路径 + if not self.plan_path(goal_x, goal_y): + self.status = NavStatus.FAILED + return False + + # 启动导航线程 + self._cancel_event.clear() + self.status = NavStatus.NAVIGATING + self._nav_thread = threading.Thread( + target=self._navigate_thread, + args=(goal_x, goal_y), + daemon=True + ) + self._nav_thread.start() + + if blocking: + self._nav_thread.join() + return self.status == NavStatus.REACHED + + return True + + def _navigate_thread(self, goal_x: float, goal_y: float): + """导航线程""" + logger.info(f"开始导航 → 目标 ({goal_x:.2f}, {goal_y:.2f})") + + try: + # 转弯朝向第一个路径点 + self._initial_turn() + + # 跟踪路径 + last_cmd_time = time.time() + cmd_interval = 0.2 # cmd_vel 发布间隔(秒) + + while not self._cancel_event.is_set(): + pos = self.get_odom() + x, y, yaw = pos + + linear_x, angular_z, reached = self.controller.compute( + (x, y, yaw), self.path_world + ) + + if reached: + self._stop_cmd_vel() + self.status = NavStatus.REACHED + logger.info("✅ 已到达目标点") + return + + # 控制发布频率 + now = time.time() + if now - last_cmd_time >= cmd_interval: + self._publish_cmd_vel(linear_x, angular_z) + last_cmd_time = now + + time.sleep(0.05) # 50ms 控制循环 + + # 被取消 + self._stop_cmd_vel() + self.status = NavStatus.CANCELLED + logger.info("导航已取消") + + except Exception as e: + self._stop_cmd_vel() + self.status = NavStatus.FAILED + logger.error(f"导航异常: {e}") + + def _initial_turn(self): + """导航开始前,先原地转向朝向第一个路径点""" + if len(self.path_world) < 2: + return + + pos = self.get_odom() + x, y, yaw = pos + target = self.path_world[1] # 第一个路径点是当前位置,取第二个 + + angle_to_target = math.atan2(target[1] - y, target[0] - x) - yaw + angle_to_target = math.atan2(math.sin(angle_to_target), math.cos(angle_to_target)) + + if abs(angle_to_target) < 0.1: # < 6°,不需要转弯 + return + + logger.info(f"初始转向: {math.degrees(angle_to_target):.1f}°") + + # 分段旋转(避免一步到位导致超调) + steps = max(3, int(abs(angle_to_target) / 0.2)) + step_angle = angle_to_target / steps + step_time = abs(step_angle) / self.controller.max_angular_speed + 0.1 + + for _ in range(steps): + if self._cancel_event.is_set(): + return + angular = max(-self.controller.max_angular_speed, + min(self.controller.max_angular_speed, step_angle * 2)) + self._publish_cmd_vel(0.0, angular) + time.sleep(step_time) + + self._stop_cmd_vel() + time.sleep(0.2) # 稳定后继续 + + def stop(self): + """停止当前导航""" + if self.status == NavStatus.NAVIGATING: + self._cancel_event.set() + self._stop_cmd_vel() + if self._nav_thread and self._nav_thread.is_alive(): + self._nav_thread.join(timeout=3) + self.status = NavStatus.CANCELLED + + def get_status(self) -> dict: + """获取导航状态""" + pos = self.get_odom() + return { + "status": self.status.value, + "current_position": pos, + "path_length": len(self.path_world), + "path": self.path_world if self.status in (NavStatus.NAVIGATING, NavStatus.REACHED) else [] + } + + def get_path_preview(self, goal_x: float, goal_y: float) -> Optional[List[Tuple[float, float]]]: + """ + 预览路径(仅规划不执行),用于前端可视化 + + Returns: + 世界坐标路径列表,或 None(规划失败) + """ + if self.plan_path(goal_x, goal_y): + return self.path_world + return None diff --git a/agv_app/utils/mission_executor.py b/agv_app/utils/mission_executor.py new file mode 100644 index 0000000..182e407 --- /dev/null +++ b/agv_app/utils/mission_executor.py @@ -0,0 +1,231 @@ +""" +任务调度器 - 管理拍摄任务的执行 +""" +import os +import json +import time +import logging +from typing import List, Dict, Optional +from enum import Enum + +from .arm_client import ArmClient +from .agv_controller import AGVController +from .qr_scanner import QRScanner +from .image_uploader import ImageUploader + +logger = logging.getLogger(__name__) + + +class TaskStatus(Enum): + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + PAUSED = "paused" + + +class MissionExecutor: + """任务执行器 - 负责按顺序执行点位拍摄任务""" + + def __init__(self, config: dict): + self.config = config + self.status = TaskStatus.PENDING + self.current_point_index = 0 + self.current_pose_index = 0 + self.snapshot_serial_map = {} # {point_id: serial_number} 缓存已扫描的 serialNumber + + # 初始化各模块 + self.agv = AGVController( + device=config.get("device", "/dev/agvpro_controller"), + baudrate=config.get("baudrate", 1000000) + ) + self.arm_client: Optional[ArmClient] = None + self.uploader = ImageUploader( + upload_url=config["upload_url"], + timeout=config.get("upload_timeout", 30), + max_retries=config.get("upload_retries", 3) + ) + self.qr_scanner = QRScanner(device_index=config.get("camera_index", 0)) + + # ========== 连接管理 ========== + + def connect_all(self) -> Dict[str, bool]: + """连接 AGV、机械臂、摄像头""" + results = {} + + # 连接 AGV + results["agv"] = self.agv.connect() + + # 连接机械臂(通过 TCP) + arm_cfg = self.config["arm"] + self.arm_client = ArmClient(arm_cfg["host"], arm_cfg["port"]) + results["arm"] = self.arm_client.connect() + + # 打开摄像头 + results["camera"] = self.qr_scanner.open() + + return results + + def disconnect_all(self): + """断开所有连接""" + if self.arm_client: + self.arm_client.close() + self.agv.disconnect() + self.qr_scanner.close() + + # ========== 任务执行 ========== + + def execute_mission(self, mission_data: dict) -> dict: + """ + 执行一个完整任务(一个地图的所有点位) + mission_data: 包含点位列表的完整任务配置 + 返回执行报告 + """ + self.status = TaskStatus.RUNNING + report = { + "total_points": len(mission_data.get("points", [])), + "completed": 0, + "failed": 0, + "details": [] + } + + points = mission_data.get("points", []) + for i, point in enumerate(points): + self.current_point_index = i + try: + result = self._execute_point(point) + report["details"].append(result) + if result["status"] == "completed": + report["completed"] += 1 + else: + report["failed"] += 1 + except Exception as e: + logger.error(f"点位 {i} 执行异常: {e}") + report["failed"] += 1 + report["details"].append({ + "point_index": i, + "point_name": point.get("name", f"point_{i}"), + "status": "failed", + "error": str(e) + }) + + self.status = TaskStatus.COMPLETED if report["failed"] == 0 else TaskStatus.PAUSED + return report + + def _execute_point(self, point: dict) -> dict: + """执行单个点位的拍摄""" + point_name = point.get("name", "unknown") + logger.info(f"开始执行点位: {point_name}") + + result = { + "point_name": point_name, + "poses": [] + } + + # 1. AGV 移动到点位 + coords = point.get("coords", {}) + x, y = coords.get("x", 0), coords.get("y", 0) + logger.info(f"AGV 移动到 ({x}, {y})") + # TODO: 调用导航移动到目标点 + time.sleep(1) # 模拟移动 + + # 2. 执行该点位的所有姿态 + poses = point.get("poses", []) + for j, pose in enumerate(poses): + self.current_pose_index = j + pose_result = self._execute_pose(point, pose, j) + result["poses"].append(pose_result) + + # 如果是"两者都要"类型,需要按顺序执行两台机器 + if pose.get("type") == "both": + # 执行顺序由 pose.sequence 配置 + sequence = pose.get("sequence", ["front_first"]) + for step in sequence: + if step == "front": + self._capture_and_upload(point, pose, "front", j) + elif step == "back": + self._capture_and_upload(point, pose, "back", j) + else: + photo_type = pose.get("photo_type", "front") + self._capture_and_upload(point, pose, photo_type, j) + + result["status"] = "completed" + return result + + def _execute_pose(self, point: dict, pose: dict, pose_idx: int) -> dict: + """执行单个姿态的拍摄""" + photo_type = pose.get("photo_type", "front") + camera_source = pose.get("camera", "agv") # agv 或 arm + + # 如果需要机械臂运动 + arm_angles = pose.get("arm_angles", None) + if arm_angles and self.arm_client: + self.arm_client.set_angles(arm_angles, speed=pose.get("speed", 500)) + time.sleep(1) # 等待运动到位 + + return { + "pose_index": pose_idx, + "photo_type": photo_type, + "arm_angles": arm_angles, + "status": "ready" + } + + def _capture_and_upload(self, point: dict, pose: dict, photo_type: str, pose_idx: int): + """拍摄并上传""" + point_id = point.get("id", str(point)) + + # 确定 serialNumber + if photo_type == "front": + # 正面:从二维码获取 serialNumber + serial = self.qr_scanner.scan_with_retry(max_attempts=5, interval=0.5) + if not serial: + logger.warning(f"点位 {point.get('name')} 正面拍摄未扫描到二维码,跳过") + return + self.snapshot_serial_map[point_id] = serial + else: + # 背面:使用缓存的 serialNumber + serial = self.snapshot_serial_map.get(point_id) + if not serial: + logger.warning(f"点位 {point.get('name')} 背面拍摄但无缓存 serialNumber") + return + + # 拍摄图片(AGV 端摄像头) + frame = self.qr_scanner.read_frame() + if frame is None: + logger.error("摄像头读取失败") + return + + # 保存图片 + photo_dir = os.path.join(os.path.dirname(__file__), "..", "photos") + os.makedirs(photo_dir, exist_ok=True) + photo_path = os.path.join(photo_dir, f"{serial}_{photo_type}_{int(time.time())}.jpg") + import cv2 + cv2.imwrite(photo_path, frame) + + # 上传 + self.uploader.upload(photo_path, serial, pose_idx, photo_type) + logger.info(f"上传完成: {serial} {photo_type}") + + # ========== 状态查询 ========== + + def get_status(self) -> dict: + return { + "task_status": self.status.value, + "current_point": self.current_point_index, + "current_pose": self.current_pose_index, + "agv_connected": self.agv.is_connected(), + "arm_connected": self.arm_client is not None, + "camera_opened": self.qr_scanner._cap is not None and self.qr_scanner._cap.isOpened() + } + + def pause(self): + self.status = TaskStatus.PAUSED + + def resume(self): + self.status = TaskStatus.RUNNING + + def stop(self): + if self.arm_client: + self.arm_client.task_stop() + self.agv.stop() + self.status = TaskStatus.PENDING \ No newline at end of file diff --git a/agv_app/utils/qr_scanner.py b/agv_app/utils/qr_scanner.py new file mode 100644 index 0000000..402f8d5 --- /dev/null +++ b/agv_app/utils/qr_scanner.py @@ -0,0 +1,99 @@ +""" +二维码识别模块 - 使用 OpenCV 识别二维码获取 serialNumber +""" +import cv2 +import time +import logging +import numpy as np +from typing import Optional, Tuple + +logger = logging.getLogger(__name__) + +# 尝试导入二维码识别库 +try: + from pyzbar.pyzbar import decode as qr_decode + PYZBAR_AVAILABLE = True +except ImportError: + PYZBAR_AVAILABLE = False + logger.warning("pyzbar 未安装,尝试用 OpenCV 内置 QRCodeDetector") + + +class QRScanner: + """二维码扫描器""" + + def __init__(self, device_index: int = 0): + self.device_index = device_index + self._cap: Optional[cv2.VideoCapture] = None + self._qr_detector = cv2.QRCodeDetector() # OpenCV 内置二维码检测器 + + def open(self) -> bool: + """打开摄像头""" + try: + # 强制 V4L2 后端,获取标准彩色格式(与 test/server.py 一致) + self._cap = cv2.VideoCapture(self.device_index, cv2.CAP_V4L2) + if self._cap.isOpened(): + logger.info(f"摄像头 {self.device_index} 已打开 (V4L2)") + return True + else: + # fallback: 不指定后端 + self._cap = cv2.VideoCapture(self.device_index) + if self._cap.isOpened(): + logger.info(f"摄像头 {self.device_index} 已打开 (默认后端)") + return True + logger.error(f"无法打开摄像头 {self.device_index}") + return False + except Exception as e: + logger.error(f"摄像头打开失败: {e}") + return False + + def close(self): + if self._cap: + self._cap.release() + self._cap = None + + def read_frame(self) -> Optional[np.ndarray]: + """读取一帧""" + if not self._cap or not self._cap.isOpened(): + return None + ret, frame = self._cap.read() + if not ret: + return None + return frame + + def detect_qr(self, frame: np.ndarray) -> Optional[str]: + """从图像帧中检测二维码""" + if frame is None: + return None + try: + # OpenCV 内置二维码检测 + data, vertices, _ = self._qr_detector.detectAndDecode(frame) + if data and len(data) > 0: + return data.strip() + except Exception as e: + logger.debug(f"二维码检测失败: {e}") + return None + + def scan_once(self) -> Optional[str]: + """扫描一次(读取一帧并检测)""" + frame = self.read_frame() + return self.detect_qr(frame) + + def scan_with_retry(self, max_attempts: int = 5, interval: float = 0.5) -> Optional[str]: + """多次扫描直到成功或达到最大次数""" + for i in range(max_attempts): + result = self.scan_once() + if result: + return result + time.sleep(interval) + return None + + def get_preview_frame(self) -> Optional[np.ndarray]: + """获取预览帧(用于界面显示)""" + return self.read_frame() + + def __enter__(self): + self.open() + return self + + def __exit__(self, *args): + self.close() \ No newline at end of file diff --git a/arm_server/__init__.py b/arm_server/__init__.py new file mode 100644 index 0000000..ec8f415 --- /dev/null +++ b/arm_server/__init__.py @@ -0,0 +1 @@ +# 机械臂服务端 diff --git a/arm_server/arm_server.py b/arm_server/arm_server.py new file mode 100644 index 0000000..596c977 --- /dev/null +++ b/arm_server/arm_server.py @@ -0,0 +1,263 @@ +""" +机械臂服务端 - 机械臂端主程序 +运行在 10.247.46.165 上,端口 5002 (TCP) + 5003 (视频流) +通过 TCP Socket 接收 AGV 发来的指令,转发给 RoboFlow (630 Socket API) +同时通过 ffmpeg 提供 HTTP 视频流 +""" +import socket +import threading +import time +import logging +import os +import sys +import subprocess +from flask import Flask, Response, jsonify +from werkzeug.serving import make_server + +# 添加当前目录到路径 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + handlers=[ + logging.StreamHandler(), + logging.FileHandler(os.path.expanduser("~/work/arm_server/server.log")) + ] +) +logger = logging.getLogger("arm_server") + +# ========== Flask HTTP 服务器 - 视频流 (ffmpeg) ========== +arm_video_app = Flask(__name__) + +ARM_CAMERA_INDEX = 0 # 机械臂端摄像头设备号 +_ffmpeg_proc = None + + +@arm_video_app.route("/api/camera/preview") +def arm_camera_preview(): + """机械臂摄像头 MJPEG 流 (ffmpeg)""" + global _ffmpeg_proc + + def generate(): + global _ffmpeg_proc + # 启动 ffmpeg 进程(如果尚未运行) + if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None: + logger.info(f"启动 ffmpeg 视频流 (Video{ARM_CAMERA_INDEX})") + _ffmpeg_proc = subprocess.Popen( + [ + "ffmpeg", + "-f", "v4l2", + "-input_format", "mjpeg", + "-i", f"/dev/video{ARM_CAMERA_INDEX}", + "-vf", "rotate=PI", + "-q:v", "8", + "-f", "mjpeg", + "-" + ], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + time.sleep(0.5) # 等待 ffmpeg 初始化 + + try: + while True: + if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None: + break + jpeg = _ffmpeg_proc.stdout.read(65536) + if not jpeg: + break + yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n") + except Exception as e: + logger.error(f"视频流异常: {e}") + finally: + logger.info("视频流连接关闭") + + return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame") + + +@arm_video_app.route("/api/camera/status") +def arm_camera_status(): + """摄像头状态""" + global _ffmpeg_proc + running = _ffmpeg_proc is not None and _ffmpeg_proc.poll() is None + return jsonify({"opened": running}) + + +@arm_video_app.route("/api/camera/restart", methods=["POST"]) +def arm_camera_restart(): + """重启视频流""" + global _ffmpeg_proc + if _ffmpeg_proc: + _ffmpeg_proc.terminate() + _ffmpeg_proc = None + return jsonify({"ok": True}) + + +# ========== RoboFlow 630 Socket API 客户端 ========== +class RoboFlowClient: + """通过 Socket 连接 RoboFlow 630 机械臂控制盒""" + + def __init__(self, host: str = "127.0.0.1", port: int = 5001, timeout: float = 10): + self.host = host + self.port = port + self.timeout = timeout + self._sock: socket.socket = None + + def connect(self) -> bool: + try: + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.settimeout(self.timeout) + self._sock.connect((self.host, self.port)) + logger.info(f"已连接到 RoboFlow {self.host}:{self.port}") + return True + except Exception as e: + logger.error(f"连接 RoboFlow 失败: {e}") + return False + + def send_recv(self, cmd: str) -> str: + """发送命令并等待响应""" + if not self._sock: + raise ConnectionError("未连接到 RoboFlow") + try: + self._sock.sendall((cmd + "\n").encode("utf-8")) + resp = self._sock.recv(4096).decode("utf-8").strip() + return resp + except socket.timeout: + return "ERROR: timeout" + except Exception as e: + return f"ERROR: {e}" + + def close(self): + if self._sock: + self._sock.close() + self._sock = None + + def __enter__(self): + self.connect() + return self + + def __exit__(self, *args): + self.close() + + +# ========== TCP 服务器 - 接收 AGV 指令 ========== +class AGVCommandServer: + """TCP 服务器,接收 AGV 发来的指令""" + + def __init__(self, host: str = "0.0.0.0", port: int = 5002): + self.host = host + self.port = port + self._sock: socket.socket = None + self._running = False + self.roboflow: RoboFlowClient = None + self._connect_roboflow() + + def _connect_roboflow(self): + self.roboflow = RoboFlowClient() + if self.roboflow.connect(): + logger.info("RoboFlow 连接成功") + else: + logger.warning("RoboFlow 连接失败,服务将以 limited 模式运行") + + def start(self): + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._sock.bind((self.host, self.port)) + self._sock.listen(5) + self._running = True + logger.info(f"=" * 50) + logger.info(f"机械臂服务端已启动,监听 {self.host}:{self.port}") + logger.info(f"等待 AGV 连接...") + logger.info(f"=" * 50) + + while self._running: + try: + self._sock.settimeout(1.0) + try: + client_sock, addr = self._sock.accept() + logger.info(f"AGV 已连接: {addr}") + threading.Thread(target=self._handle_client, args=(client_sock,), daemon=True).start() + except socket.timeout: + continue + except Exception as e: + if self._running: + logger.error(f"服务器异常: {e}") + break + + def _handle_client(self, client_sock: socket.socket): + try: + client_sock.settimeout(30) + buffer = "" + while self._running: + try: + data = client_sock.recv(4096) + if not data: + break + buffer += data.decode("utf-8") + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.strip() + if not line: + continue + response = self._execute_command(line) + client_sock.sendall((response + "\n").encode("utf-8")) + logger.info(f"CMD: {line} → {response}") + except socket.timeout: + continue + except Exception as e: + logger.error(f"客户端处理异常: {e}") + finally: + client_sock.close() + logger.info("AGV 客户端已断开") + + def _execute_command(self, cmd: str) -> str: + if not self.roboflow or not self.roboflow._sock: + return f"ERROR: RoboFlow not connected" + try: + return self.roboflow.send_recv(cmd) + except Exception as e: + return f"ERROR: {e}" + + def stop(self): + self._running = False + if self._sock: + try: + self._sock.close() + except: + pass + if self.roboflow: + self.roboflow.close() + logger.info("机械臂服务端已停止") + + +# ========== 入口 ========== +def main(): + import signal + + server = AGVCommandServer(port=5002) + + # 启动 Flask 视频流服务(端口 5003) + arm_server_http = make_server("0.0.0.0", 5003, arm_video_app, threaded=True) + http_thread = threading.Thread(target=arm_server_http.serve_forever, daemon=True) + http_thread.start() + logger.info("机械臂视频流服务已启动: http://0.0.0.0:5003") + + def signal_handler(sig, frame): + logger.info("收到停止信号...") + global _ffmpeg_proc + if _ffmpeg_proc: + _ffmpeg_proc.terminate() + server.stop() + arm_server_http.shutdown() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + server.start() + + +if __name__ == "__main__": + main() diff --git a/arm_server/requirements.txt b/arm_server/requirements.txt new file mode 100644 index 0000000..d1e2182 --- /dev/null +++ b/arm_server/requirements.txt @@ -0,0 +1,3 @@ +# 机械臂端依赖(最少依赖) +# RoboFlow 已在树莓派上运行,此端仅做透传 +flask>=2.0 diff --git a/arm_server/start.sh b/arm_server/start.sh new file mode 100644 index 0000000..b360d00 --- /dev/null +++ b/arm_server/start.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# 启动机械臂服务端 + +cd ~/work/arm_server +python3 arm_server.py diff --git a/restart_agv.sh b/restart_agv.sh new file mode 100644 index 0000000..f694b36 --- /dev/null +++ b/restart_agv.sh @@ -0,0 +1,31 @@ +#!/bin/bash +cd /home/elephant/work/agv_app + +# 语法检查 +python3 -m py_compile app.py +if [ $? -ne 0 ]; then + echo "Syntax error!" + exit 1 +fi + +# 重启服务 +pkill -f "python.*app.py" 2>/dev/null +sleep 1 +nohup python3 app.py > app.log 2>&1 & +sleep 3 + +# 验证 +if ss -tlnp | grep 5000; then + echo "✓ 端口5000 正常" + # 测试机械臂单帧 + result=$(curl -s --max-time 5 http://127.0.0.1:5000/api/camera/arm_refresh | head -c 4) + echo -n "arm_refresh: " + if [ "$result" = "$(echo -en '\xff\xd8\xff\xe0')" ]; then + echo "JPEG OK ✓" + else + echo "返回: $(echo $result | xxd | head -1)" + fi +else + echo "✗ 启动失败" + tail -10 app.log +fi \ No newline at end of file diff --git a/shared/config.json b/shared/config.json new file mode 100644 index 0000000..3f58c59 --- /dev/null +++ b/shared/config.json @@ -0,0 +1,23 @@ +{ + "agv": { + "ip": "10.247.46.236", + "ssh_user": "elephant", + "ssh_password": "Elephant", + "map_file": "map.yaml", + "map_dir": "/home/elephant" + }, + "arm": { + "ip": "10.247.46.165", + "ssh_user": "pi", + "ssh_password": "elephant", + "socket_port": 5001, + "roboflow_host": "127.0.0.1", + "roboflow_port": 5001 + }, + "app": { + "upload_url": "https://ts.timeddd.com/prod-api/file/uploadImage", + "agv_control_port": 5000, + "arm_server_port": 5002, + "secret_key": "agv630_secret_key_2024" + } +} \ No newline at end of file diff --git a/start_agv.sh b/start_agv.sh new file mode 100644 index 0000000..7e825a4 --- /dev/null +++ b/start_agv.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# AGV 服务启动脚本 +cd /home/elephant/work/agv_app + +# 确保没有旧进程 +pkill -f "python.*app.py" 2>/dev/null +sleep 1 + +# 启动服务 +nohup python3 app.py > app.log 2>&1 & +PID=$! +echo "Started PID=$PID" + +sleep 3 + +# 验证 +if ss -tlnp | grep 5000; then + echo "✓ 端口 5000 监听正常" + curl -s http://127.0.0.1:5000/api/mission/state + echo "" +else + echo "✗ 端口 5000 未监听,检查日志:" + cat app.log +fi \ No newline at end of file