From c559bef3ece8d011e3335426ecf02abf64c7fa84 Mon Sep 17 00:00:00 2001
From: wangyuxiang <476939672@qq.com>
Date: Sat, 25 Apr 2026 13:45:40 +0800
Subject: [PATCH 1/6] feat: add Vue admin UI and account workflow improvements
---
.env.example | 1 +
.gitignore | 2 +-
README.md | 11 +-
app/config.py | 8 +
app/main.py | 5 +
app/services/payment_service.py | 239 +--
app/services/scheduler_service.py | 47 +-
app/web/routes.py | 14 +-
start.bat | 17 +
web/ACCEPTANCE.md | 41 +
web/MIGRATION.md | 32 +
web/README.md | 29 +
web/index.html | 13 +
web/package-lock.json | 1707 ++++++++++++++++++++
web/package.json | 23 +
web/src/App.vue | 94 ++
web/src/DESIGN_SYSTEM.md | 41 +
web/src/components/AccountContextModal.vue | 45 +
web/src/components/AccountTable.vue | 168 ++
web/src/components/AppShell.vue | 31 +
web/src/components/DashboardStats.vue | 29 +
web/src/components/ImportAccountModal.vue | 67 +
web/src/components/QrPreview.vue | 28 +
web/src/components/ScheduleEditor.vue | 36 +
web/src/components/StatusBanner.vue | 24 +
web/src/composables/useDashboard.ts | 182 +++
web/src/env.d.ts | 5 +
web/src/locales/zhCN.ts | 95 ++
web/src/main.ts | 43 +
web/src/services/API_CONTRACT.md | 35 +
web/src/services/api.ts | 66 +
web/src/styles.css | 533 ++++++
web/src/types/api.ts | 122 ++
web/tsconfig.json | 20 +
web/tsconfig.node.json | 11 +
web/vite.config.ts | 16 +
36 files changed, 3780 insertions(+), 100 deletions(-)
create mode 100644 web/ACCEPTANCE.md
create mode 100644 web/MIGRATION.md
create mode 100644 web/README.md
create mode 100644 web/index.html
create mode 100644 web/package-lock.json
create mode 100644 web/package.json
create mode 100644 web/src/App.vue
create mode 100644 web/src/DESIGN_SYSTEM.md
create mode 100644 web/src/components/AccountContextModal.vue
create mode 100644 web/src/components/AccountTable.vue
create mode 100644 web/src/components/AppShell.vue
create mode 100644 web/src/components/DashboardStats.vue
create mode 100644 web/src/components/ImportAccountModal.vue
create mode 100644 web/src/components/QrPreview.vue
create mode 100644 web/src/components/ScheduleEditor.vue
create mode 100644 web/src/components/StatusBanner.vue
create mode 100644 web/src/composables/useDashboard.ts
create mode 100644 web/src/env.d.ts
create mode 100644 web/src/locales/zhCN.ts
create mode 100644 web/src/main.ts
create mode 100644 web/src/services/API_CONTRACT.md
create mode 100644 web/src/services/api.ts
create mode 100644 web/src/styles.css
create mode 100644 web/src/types/api.ts
create mode 100644 web/tsconfig.json
create mode 100644 web/tsconfig.node.json
create mode 100644 web/vite.config.ts
diff --git a/.env.example b/.env.example
index b1c37d5..cdbafa2 100644
--- a/.env.example
+++ b/.env.example
@@ -5,6 +5,7 @@ BIGMODEL_API_BASE=https://www.bigmodel.cn/api
BIGMODEL_ORIGIN=https://www.bigmodel.cn
BIGMODEL_REFERER=https://www.bigmodel.cn/glm-coding
BROWSER_IMPERSONATE=chrome124
+BOOTSTRAP_FINGERPRINT_MAX_RETRIES=99
REQUEST_TIMEOUT_SECONDS=20
DEFAULT_LANGUAGE=zh-CN
TENCENT_CAPTCHA_DOMAIN=https://turing.captcha.qcloud.com
diff --git a/.gitignore b/.gitignore
index 0cb1150..c64ad37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,4 @@ data/sessions/*.json
data/tdc_cache/
node_modules/
-
+web/dist/
diff --git a/README.md b/README.md
index b7988c1..015ca57 100644
--- a/README.md
+++ b/README.md
@@ -158,7 +158,13 @@
### 6. 同步并换指纹
-点击 `同步并换指纹` 后,会先给该账号分配一个新的账号级伪装指纹,再重新同步账号上下文和套餐。
+点击 `同步并换指纹` 后,会按“换指纹 -> 同步账号上下文 -> 同步套餐”的顺序执行。
+
+如果同步失败,后端会继续换下一个指纹并重试,直到同步成功或达到最大重试次数。
+
+默认最大重试次数:
+
+- `BOOTSTRAP_FINGERPRINT_MAX_RETRIES=99`
这适合在上游风控、链路异常、套餐状态异常时主动切换一套新的网络指纹继续尝试。
@@ -380,6 +386,7 @@ BIGMODEL_API_BASE=https://www.bigmodel.cn/api
BIGMODEL_ORIGIN=https://www.bigmodel.cn
BIGMODEL_REFERER=https://www.bigmodel.cn/glm-coding
BROWSER_IMPERSONATE=chrome124
+BOOTSTRAP_FINGERPRINT_MAX_RETRIES=99
REQUEST_TIMEOUT_SECONDS=20
DEFAULT_LANGUAGE=zh-CN
TENCENT_CAPTCHA_DOMAIN=https://turing.captcha.qcloud.com
@@ -412,6 +419,7 @@ RUNTIME_LOG_RETENTION_DAYS=7
| `BIGMODEL_ORIGIN` | `https://www.bigmodel.cn` | BigModel 请求头 `Origin` 默认值 |
| `BIGMODEL_REFERER` | `https://www.bigmodel.cn/glm-coding` | BigModel 请求头 `Referer` 默认值 |
| `BROWSER_IMPERSONATE` | `chrome124` | `curl-cffi` 的全局兜底 `impersonate` 值;账号实际请求优先用账号自己的随机 `browser_impersonate` |
+| `BOOTSTRAP_FINGERPRINT_MAX_RETRIES` | `99` | 点击“同步并换指纹”时的最大尝试次数;每轮先换一个账号级指纹,再完整同步上下文和套餐,失败才进入下一轮 |
| `REQUEST_TIMEOUT_SECONDS` | `20` | 上游 HTTP 请求超时时间,单位秒 |
| `DEFAULT_LANGUAGE` | `zh-CN` | 默认请求语言,会写入 `Accept-Language` 和 `Set-Language` |
| `TENCENT_CAPTCHA_DOMAIN` | `https://turing.captcha.qcloud.com` | 腾讯验证码域名 |
@@ -432,6 +440,7 @@ RUNTIME_LOG_RETENTION_DAYS=7
- `BROWSER_IMPERSONATE` 现在主要是全局兜底值和 transport 展示值
- 真正运行时优先用账号自己的 `browser_impersonate`
- 账号级 `browser_impersonate` 在首次导入账号时随机分配为 `chrome / edge / firefox`
+- `BOOTSTRAP_FINGERPRINT_MAX_RETRIES` 小于 `1` 时会自动按 `1` 处理,避免配置错误导致完全不尝试
- 如果你把 `TENCENT_OCR_WORKERS` 配得太高,OCR 并发会更猛,但内存占用也会跟着往上窜,别一上来就梭哈
## 已知说明
diff --git a/app/config.py b/app/config.py
index 30b3c40..dd614c1 100644
--- a/app/config.py
+++ b/app/config.py
@@ -28,6 +28,7 @@ class Settings:
bigmodel_origin: str
bigmodel_referer: str
browser_impersonate: str
+ bootstrap_fingerprint_max_retries: int
request_timeout_seconds: float
default_language: str
tencent_captcha_domain: str
@@ -66,6 +67,13 @@ def get_settings() -> Settings:
bigmodel_origin=os.getenv("BIGMODEL_ORIGIN", "https://www.bigmodel.cn").rstrip("/"),
bigmodel_referer=os.getenv("BIGMODEL_REFERER", "https://www.bigmodel.cn/glm-coding").strip(),
browser_impersonate=os.getenv("BROWSER_IMPERSONATE", "chrome124").strip() or "chrome124",
+ bootstrap_fingerprint_max_retries=max(
+ 1,
+ _parse_int(
+ os.getenv("BOOTSTRAP_FINGERPRINT_MAX_RETRIES", "99"),
+ field_name="BOOTSTRAP_FINGERPRINT_MAX_RETRIES",
+ ),
+ ),
request_timeout_seconds=_parse_float(
os.getenv("REQUEST_TIMEOUT_SECONDS", "20"),
field_name="REQUEST_TIMEOUT_SECONDS",
diff --git a/app/main.py b/app/main.py
index 21dcd39..e0a35ee 100644
--- a/app/main.py
+++ b/app/main.py
@@ -3,8 +3,10 @@
from __future__ import annotations
import logging
+from pathlib import Path
from fastapi import FastAPI
+from fastapi.staticfiles import StaticFiles
from app.errors import install_exception_handlers
from app.runtime_logging import configure_logging, get_runtime_log_service
@@ -26,6 +28,9 @@ def create_app() -> FastAPI:
redoc_url="/redoc",
)
install_exception_handlers(app)
+ dist_assets = Path(__file__).resolve().parents[1] / "web" / "dist" / "assets"
+ if dist_assets.exists():
+ app.mount("/assets", StaticFiles(directory=str(dist_assets)), name="spa-assets")
app.include_router(router)
@app.on_event("startup")
diff --git a/app/services/payment_service.py b/app/services/payment_service.py
index b72b58f..97992d4 100644
--- a/app/services/payment_service.py
+++ b/app/services/payment_service.py
@@ -13,6 +13,7 @@
from app.clients.bigmodel_client import BigModelClient, get_bigmodel_client
from app.clients.tencent_captcha_client import TencentCaptchaClient, get_tencent_captcha_client
+from app.config import get_settings
from app.errors import BadRequestError, GlmDeskError, UpstreamRequestError
from app.models import (
AccountDetailResponse,
@@ -92,6 +93,7 @@ def __init__(
self.ocr_service = ocr_service or get_ocr_service()
self.tdc_service = tdc_service or get_tdc_service()
self.runtime_logs = runtime_log_service or get_runtime_log_service()
+ self.settings = get_settings()
def health_payload(self) -> dict[str, Any]:
return {
@@ -131,111 +133,160 @@ def bootstrap_account(
action="bootstrap_account",
source="refresh_fingerprint" if refresh_fingerprint else "manual",
)
- try:
- if refresh_fingerprint:
- rotated = self.state_service.rotate_browser_impersonate(account_id)
- self.runtime_logs.log_event(
- flow,
- stage="fingerprint",
- status="rotated",
- message="账号指纹已刷新",
- details={"browser_impersonate": rotated.browser_impersonate},
+ assert flow is not None
+ max_attempts = self.settings.bootstrap_fingerprint_max_retries if refresh_fingerprint else 1
+ for attempt in range(1, max_attempts + 1):
+ try:
+ detail = self._bootstrap_account_once(
+ account_id,
+ refresh_fingerprint=refresh_fingerprint,
+ flow=flow,
+ attempt=attempt,
+ max_attempts=max_attempts,
)
- account = self.state_service.get_account(account_id)
- session = self.state_service.load_session(account_id)
+ if own_flow:
+ self.runtime_logs.finish_run(
+ flow,
+ status="success",
+ message="账号同步完成",
+ details={
+ "attempt": attempt,
+ "max_attempts": max_attempts,
+ "product_count": len(detail.session.products),
+ "selected_product_id": detail.session.selected_product_id,
+ "purchase_mode": detail.session.purchase_mode,
+ },
+ )
+ return detail
+ except Exception as exc:
+ if attempt < max_attempts:
+ self.runtime_logs.log_event(
+ flow,
+ stage="bootstrap_retry",
+ status="retry",
+ message=f"账号同步失败,准备更换指纹重试({attempt}/{max_attempts}):{exc}",
+ details={
+ "attempt": attempt,
+ "max_attempts": max_attempts,
+ "error": exc.__class__.__name__,
+ },
+ level=logging.WARNING,
+ )
+ continue
+ account = self.state_service.get_account(account_id)
+ account.account_status = "error"
+ account.account_status_message = str(exc)
+ account.account_checked_at = utc_now_iso()
+ self.state_service.update_account(account)
+ if own_flow:
+ self.runtime_logs.finish_run(
+ flow,
+ status="failed",
+ message=f"账号同步失败:{exc}",
+ details={
+ "attempt": attempt,
+ "max_attempts": max_attempts,
+ "error": exc.__class__.__name__,
+ },
+ level=logging.ERROR,
+ )
+ raise
+ raise RuntimeError("bootstrap retry loop exited unexpectedly")
+ def _bootstrap_account_once(
+ self,
+ account_id: str,
+ *,
+ refresh_fingerprint: bool,
+ flow: FlowRun,
+ attempt: int,
+ max_attempts: int,
+ ) -> AccountDetailResponse:
+ if refresh_fingerprint:
+ rotated = self.state_service.rotate_browser_impersonate(account_id)
self.runtime_logs.log_event(
flow,
- stage="get_customer_info",
- status="started",
- message="开始同步账号上下文",
- details={"browser_impersonate": account.browser_impersonate},
- )
- user_info_result = self.bigmodel_client.get_customer_info(account, session)
- user_info = self._ensure_dict(user_info_result.data, label="getCustomerInfo.data")
- organizations = self._extract_organizations(user_info)
- org_id, project_id = self._choose_org_and_project(
- organizations=organizations,
- preferred_org_id=account.org_id or session.org_id,
- preferred_project_id=account.project_id or session.project_id,
+ stage="fingerprint",
+ status="rotated",
+ message="账号指纹已刷新",
+ details={
+ "attempt": attempt,
+ "max_attempts": max_attempts,
+ "browser_impersonate": rotated.browser_impersonate,
+ },
)
+ account = self.state_service.get_account(account_id)
+ session = self.state_service.load_session(account_id)
- session.org_id = org_id
- session.project_id = project_id
- session.customer_number = str(user_info.get("customerNumber") or "")
- session.customer_name = str(user_info.get("customerName") or "")
- session.user_info = user_info
- session.organizations = organizations
+ self.runtime_logs.log_event(
+ flow,
+ stage="get_customer_info",
+ status="started",
+ message="开始同步账号上下文",
+ details={
+ "attempt": attempt,
+ "max_attempts": max_attempts,
+ "browser_impersonate": account.browser_impersonate,
+ },
+ )
+ user_info_result = self.bigmodel_client.get_customer_info(account, session)
+ user_info = self._ensure_dict(user_info_result.data, label="getCustomerInfo.data")
+ organizations = self._extract_organizations(user_info)
+ org_id, project_id = self._choose_org_and_project(
+ organizations=organizations,
+ preferred_org_id=account.org_id or session.org_id,
+ preferred_project_id=account.project_id or session.project_id,
+ )
- if not session.customer_number:
- raise UpstreamRequestError(
- "getCustomerInfo 没返回 customerNumber,后面 create-sign 根本没法拼。",
- details={"payload": user_info_result.raw},
- )
+ session.org_id = org_id
+ session.project_id = project_id
+ session.customer_number = str(user_info.get("customerNumber") or "")
+ session.customer_name = str(user_info.get("customerName") or "")
+ session.user_info = user_info
+ session.organizations = organizations
- self.runtime_logs.log_event(
- flow,
- stage="get_customer_info",
- status="success",
- message="账号上下文同步成功",
- details={
- "customer_number": session.customer_number,
- "customer_name": session.customer_name,
- "org_id": org_id,
- "project_id": project_id,
- "organization_count": len(organizations),
- },
+ if not session.customer_number:
+ raise UpstreamRequestError(
+ "getCustomerInfo 没返回 customerNumber,后面 create-sign 根本没法拼。",
+ details={"payload": user_info_result.raw},
)
- account.org_id = org_id
- account.project_id = project_id
- account.last_bootstrap_at = utc_now_iso()
- self.state_service.update_account(account)
+ self.runtime_logs.log_event(
+ flow,
+ stage="get_customer_info",
+ status="success",
+ message="账号上下文同步成功",
+ details={
+ "customer_number": session.customer_number,
+ "customer_name": session.customer_name,
+ "org_id": org_id,
+ "project_id": project_id,
+ "organization_count": len(organizations),
+ },
+ )
- products = self.load_products(
- account_id,
- invitation_code=account.invitation_code,
- account=account,
- session=session,
- flow=flow,
- )
- session.products = products
- if not session.selected_product_id:
- session.selected_product_id = DEFAULT_PRODUCT_ID
- self.state_service.save_session(session)
- account = self.state_service.get_account(account_id)
- account.account_status = "valid"
- account.account_status_message = "同步上下文和套餐成功"
- account.account_checked_at = utc_now_iso()
- self.state_service.update_account(account)
- detail = self.state_service.get_account_detail(account_id)
- if own_flow:
- self.runtime_logs.finish_run(
- flow,
- status="success",
- message="账号同步完成",
- details={
- "product_count": len(products),
- "selected_product_id": detail.session.selected_product_id,
- "purchase_mode": detail.session.purchase_mode,
- },
- )
- return detail
- except Exception as exc:
- account = self.state_service.get_account(account_id)
- account.account_status = "error"
- account.account_status_message = str(exc)
- account.account_checked_at = utc_now_iso()
- self.state_service.update_account(account)
- if own_flow:
- self.runtime_logs.finish_run(
- flow,
- status="failed",
- message=f"账号同步失败:{exc}",
- details={"error": exc.__class__.__name__},
- level=logging.ERROR,
- )
- raise
+ account.org_id = org_id
+ account.project_id = project_id
+ account.last_bootstrap_at = utc_now_iso()
+ self.state_service.update_account(account)
+
+ products = self.load_products(
+ account_id,
+ invitation_code=account.invitation_code,
+ account=account,
+ session=session,
+ flow=flow,
+ )
+ session.products = products
+ if not session.selected_product_id:
+ session.selected_product_id = DEFAULT_PRODUCT_ID
+ self.state_service.save_session(session)
+ account = self.state_service.get_account(account_id)
+ account.account_status = "valid"
+ account.account_status_message = "同步上下文和套餐成功"
+ account.account_checked_at = utc_now_iso()
+ self.state_service.update_account(account)
+ return self.state_service.get_account_detail(account_id)
def load_products(
self,
diff --git a/app/services/scheduler_service.py b/app/services/scheduler_service.py
index 10fa03c..cf1f0dd 100644
--- a/app/services/scheduler_service.py
+++ b/app/services/scheduler_service.py
@@ -29,12 +29,14 @@ def __init__(self) -> None:
self._stop_event = threading.Event()
self._thread: threading.Thread | None = None
self._running_accounts: set[str] = set()
+ self._flow_accounts: set[str] = set()
self._pause_requested: set[str] = set()
self._lock = threading.Lock()
def start(self) -> None:
if self._thread and self._thread.is_alive():
return
+ self._clear_stale_schedule_statuses()
self._stop_event.clear()
self._thread = threading.Thread(target=self._run_loop, name="glm-desk-scheduler", daemon=True)
self._thread.start()
@@ -55,6 +57,27 @@ def stop(self) -> None:
message="调度器已停止",
)
+ def _clear_stale_schedule_statuses(self) -> None:
+ stale_statuses = {"running", "pause_requested"}
+ with self._lock:
+ active_flow_accounts = set(self._flow_accounts)
+ for public_account in self.state_service.list_accounts():
+ if public_account.id in active_flow_accounts:
+ continue
+ account = self.state_service.get_account(public_account.id)
+ if str(account.last_schedule_status or "").lower() not in stale_statuses:
+ continue
+ account.last_schedule_status = "paused"
+ account.last_schedule_message = "服务重启后任务已停止,请重新启动"
+ self.state_service.update_account(account)
+ self.runtime_logs.log_account_event(
+ account_id=account.id,
+ action="run_payment_flow",
+ stage="scheduler",
+ status="stale_cleared",
+ message="已清理服务重启遗留的运行状态",
+ )
+
def _run_loop(self) -> None:
while not self._stop_event.is_set():
try:
@@ -155,6 +178,7 @@ def start_account_flow(self, account_id: str, *, source: str = "manual") -> dict
return {"started": False, "status": "running"}
self._pause_requested.discard(account_id)
self._running_accounts.add(account_id)
+ self._flow_accounts.add(account_id)
account = self.state_service.get_account(account_id)
account.last_scheduled_run_at = utc_now_iso()
account.last_schedule_status = "running"
@@ -178,9 +202,25 @@ def start_account_flow(self, account_id: str, *, source: str = "manual") -> dict
def request_pause(self, account_id: str) -> dict[str, object]:
with self._lock:
- if account_id not in self._running_accounts:
- return {"paused": False, "status": "idle"}
- self._pause_requested.add(account_id)
+ is_flow_running = account_id in self._flow_accounts
+ if is_flow_running:
+ self._pause_requested.add(account_id)
+ if not is_flow_running:
+ account = self.state_service.get_account(account_id)
+ stale_statuses = {"running", "pause_requested"}
+ if str(account.last_schedule_status or "").lower() in stale_statuses:
+ account.last_schedule_status = "paused"
+ account.last_schedule_message = "任务不在当前进程运行,已清理陈旧运行状态"
+ self.state_service.update_account(account)
+ self.runtime_logs.log_account_event(
+ account_id=account_id,
+ action="run_payment_flow",
+ stage="pause",
+ status="stale_cleared",
+ message="暂停时发现任务不在当前进程运行,已清理陈旧状态",
+ )
+ return {"paused": False, "status": "paused", "stale_cleared": True}
+ return {"paused": False, "status": "idle"}
account = self.state_service.get_account(account_id)
account.last_schedule_status = "pause_requested"
account.last_schedule_message = "暂停请求已提交"
@@ -245,6 +285,7 @@ def _run_account_flow(self, account_id: str, source: str) -> None:
finally:
with self._lock:
self._running_accounts.discard(account_id)
+ self._flow_accounts.discard(account_id)
self._pause_requested.discard(account_id)
diff --git a/app/web/routes.py b/app/web/routes.py
index 256fa06..8ce34b7 100644
--- a/app/web/routes.py
+++ b/app/web/routes.py
@@ -6,7 +6,7 @@
from typing import Any
from fastapi import APIRouter, Query, Request
-from fastapi.responses import HTMLResponse
+from fastapi.responses import FileResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from app.models import (
@@ -23,10 +23,22 @@
router = APIRouter()
payment_service = get_payment_service()
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates"))
+SPA_INDEX = Path(__file__).resolve().parents[2] / "web" / "dist" / "index.html"
@router.get("/", response_class=HTMLResponse)
def index(request: Request, ic: str = Query(default="")):
+ if SPA_INDEX.exists():
+ return FileResponse(SPA_INDEX)
+ return render_legacy_index(request, ic)
+
+
+@router.get("/legacy", response_class=HTMLResponse)
+def legacy_index(request: Request, ic: str = Query(default="")):
+ return render_legacy_index(request, ic)
+
+
+def render_legacy_index(request: Request, ic: str = ""):
return templates.TemplateResponse(
request=request,
name="index.html",
diff --git a/start.bat b/start.bat
index 242757e..3de5078 100644
--- a/start.bat
+++ b/start.bat
@@ -29,6 +29,23 @@ if not exist .venv\Scripts\python.exe (
echo [glmDesk] Syncing Python dependencies...
.venv\Scripts\python.exe -m pip install -r requirements.txt
+if exist web\package.json (
+ where npm >nul 2>&1
+ if errorlevel 1 (
+ echo [glmDesk] npm not found; skipping Vue frontend build and using legacy page fallback.
+ ) else (
+ if not exist web\node_modules (
+ echo [glmDesk] Installing Vue frontend dependencies...
+ pushd web
+ call npm install
+ popd
+ )
+ echo [glmDesk] Building Vue frontend...
+ pushd web
+ call npm run build
+ popd
+ )
+)
echo [glmDesk] Starting FastAPI server...
.venv\Scripts\python.exe -m uvicorn app.main:app --host %APP_HOST% --port %APP_PORT% --reload
diff --git a/web/ACCEPTANCE.md b/web/ACCEPTANCE.md
new file mode 100644
index 0000000..7eac85f
--- /dev/null
+++ b/web/ACCEPTANCE.md
@@ -0,0 +1,41 @@
+# Frontend Migration Acceptance Checklist
+
+## Functional Regression
+
+- [ ] Dashboard loads `/healthz`, `/api/accounts`, and per-account details.
+- [ ] Import account submits label, token, and invitation code.
+- [ ] Refresh button reloads dashboard state.
+- [ ] Account name opens context modal with account, session, and latest task JSON.
+- [ ] Product selector persists `selected_product_id` through `PATCH /api/accounts/{id}`.
+- [ ] Schedule switch and time input persist `schedule_enabled` and `scheduled_start_time`.
+- [ ] Sync fingerprint calls `POST /api/accounts/{id}/bootstrap?refresh_fingerprint=true`.
+- [ ] Run and pause call their existing flow-control endpoints.
+- [ ] Delete requires confirmation and calls the existing delete endpoint.
+- [ ] Latest QR image displays when `tasks[0].qr_base64` exists.
+- [ ] Empty state appears when there are no accounts.
+
+## Build And Runtime
+
+- [x] `npm install` succeeds in `web/`.
+- [x] `npm run typecheck` succeeds.
+- [x] `npm run build` succeeds.
+- [x] `python -m py_compile app/web/routes.py app/main.py` succeeds.
+- [x] FastAPI TestClient returns 200 for `/` and `/legacy`.
+- [x] FastAPI mounts `/assets` when `web/dist/assets` exists.
+
+## UI/UX
+
+- [x] Uses a data-dense dashboard layout rather than copied legacy CSS.
+- [x] Uses Naive UI components for cards, buttons, forms, modal, table shell, switch, select, tags, alerts, spinner, and popconfirm.
+- [x] Has visible focus states for keyboard users.
+- [x] Uses loading state for table and per-action buttons.
+- [x] Uses horizontal overflow containment for narrow tables.
+- [x] Respects `prefers-reduced-motion`.
+
+## Risks
+
+- Current refresh keeps the legacy N+1 request pattern. Add `GET /api/dashboard` if account counts grow.
+- `start.bat` now runs npm install/build when `web/package.json` exists; first startup will be slower.
+- UI 文案已集中到 `web/src/locales/zhCN.ts`,Naive UI 通过 `zhCN/dateZhCN` 完成本地化配置。后续新增页面应优先复用该文案模块,避免继续散落硬编码字符串。
+- `web/dist` is ignored because it is generated. Build before production-like serving or let `start.bat` build it.
+- Legacy page should remain until real account flows are manually compared.
\ No newline at end of file
diff --git a/web/MIGRATION.md b/web/MIGRATION.md
new file mode 100644
index 0000000..2c0af7d
--- /dev/null
+++ b/web/MIGRATION.md
@@ -0,0 +1,32 @@
+# Frontend Migration Rollout
+
+## Current Rollout State
+
+The Vue SPA is implemented in parallel with the legacy Jinja page.
+
+- `/` serves `web/dist/index.html` when the frontend has been built.
+- `/legacy` always serves the previous Jinja template.
+- If `web/dist/index.html` is missing, `/` falls back to the legacy template automatically.
+- `/api/*` routes remain unchanged.
+
+## Local Development
+
+1. Start FastAPI as usual on `127.0.0.1:8787`.
+2. In a second terminal, run `cd web && npm run dev`.
+3. Open `http://127.0.0.1:5173` for the Vue SPA.
+4. Use `http://127.0.0.1:8787/legacy` to compare the old page.
+
+## Production-like Local Startup
+
+`start.bat` now installs frontend dependencies if needed, builds `web/dist`, and starts FastAPI. This keeps the normal one-click Windows startup path while allowing automatic legacy fallback if npm is unavailable.
+
+## Cutover Strategy
+
+1. Keep the legacy template for at least one validation cycle.
+2. Compare key flows in `/` and `/legacy`: import, refresh, product select, schedule update, sync, run, pause, delete, context modal, QR display.
+3. If the SPA fails, remove `web/dist` or open `/legacy` to continue using the old page.
+4. After validation, remove the legacy Jinja template and route in a separate cleanup change.
+
+## Deferred Backend Optimization
+
+The SPA intentionally preserves the old refresh behavior: `GET /api/accounts` followed by concurrent `GET /api/accounts/{id}` calls. Once the UI is stable, add a backend aggregate endpoint such as `GET /api/dashboard` to reduce N+1 polling pressure.
\ No newline at end of file
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..a1e19d0
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,29 @@
+# GLM Desk Web
+
+Vue 3 + Vite + TypeScript frontend for the FastAPI GLM Desk service.
+
+## Development
+
+```powershell
+cd web
+npm install
+npm run dev
+```
+
+Vite proxies `/api` and `/healthz` to `http://127.0.0.1:8787`.
+
+## Production Build
+
+```powershell
+cd web
+npm run build
+```
+
+The FastAPI app serves `web/dist/index.html` at `/` when the build exists. The legacy Jinja page remains available at `/legacy`.
+
+## Design Notes
+
+- UI pattern: data-dense operations dashboard.
+- Component library: Naive UI, registered on demand to keep the bundle smaller.
+- Accessibility: visible focus states, semantic buttons, loading states, and a mobile-safe horizontal table region.
+- Fallback: if `web/dist/index.html` does not exist, FastAPI automatically renders the legacy Jinja template.
\ No newline at end of file
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..3512256
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ GLM Desk
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/package-lock.json b/web/package-lock.json
new file mode 100644
index 0000000..c357bb4
--- /dev/null
+++ b/web/package-lock.json
@@ -0,0 +1,1707 @@
+{
+ "name": "glm-desk-web",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "glm-desk-web",
+ "version": "0.1.0",
+ "dependencies": {
+ "@vicons/ionicons5": "^0.13.0",
+ "naive-ui": "^2.42.0",
+ "vue": "^3.5.13"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.2.1",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.7",
+ "vue-tsc": "^2.2.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@css-render/plugin-bem": {
+ "version": "0.15.14",
+ "resolved": "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz",
+ "integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "css-render": "~0.15.14"
+ }
+ },
+ "node_modules/@css-render/vue3-ssr": {
+ "version": "0.15.14",
+ "resolved": "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz",
+ "integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.0.11"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
+ "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
+ "license": "MIT"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@juggle/resize-observer": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
+ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.24",
+ "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz",
+ "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.12",
+ "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@vicons/ionicons5": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz",
+ "integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.33.tgz",
+ "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.2",
+ "@vue/shared": "3.5.33",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz",
+ "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.33",
+ "@vue/shared": "3.5.33"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz",
+ "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.2",
+ "@vue/compiler-core": "3.5.33",
+ "@vue/compiler-dom": "3.5.33",
+ "@vue/compiler-ssr": "3.5.33",
+ "@vue/shared": "3.5.33",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.10",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz",
+ "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.33",
+ "@vue/shared": "3.5.33"
+ }
+ },
+ "node_modules/@vue/compiler-vue2": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz",
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/compiler-vue2": "^2.7.16",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^1.0.3",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.33.tgz",
+ "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.33"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.33.tgz",
+ "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.33",
+ "@vue/shared": "3.5.33"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz",
+ "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.33",
+ "@vue/runtime-core": "3.5.33",
+ "@vue/shared": "3.5.33",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.33.tgz",
+ "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.33",
+ "@vue/shared": "3.5.33"
+ },
+ "peerDependencies": {
+ "vue": "3.5.33"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.33.tgz",
+ "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==",
+ "license": "MIT"
+ },
+ "node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async-validator": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
+ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/css-render": {
+ "version": "0.15.14",
+ "resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz",
+ "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "~0.8.0",
+ "csstype": "~3.0.5"
+ }
+ },
+ "node_modules/css-render/node_modules/csstype": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz",
+ "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/date-fns-tz": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
+ "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "date-fns": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/evtd": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz",
+ "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/naive-ui": {
+ "version": "2.44.1",
+ "resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.44.1.tgz",
+ "integrity": "sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@css-render/plugin-bem": "^0.15.14",
+ "@css-render/vue3-ssr": "^0.15.14",
+ "@types/lodash": "^4.17.20",
+ "@types/lodash-es": "^4.17.12",
+ "async-validator": "^4.2.5",
+ "css-render": "^0.15.14",
+ "csstype": "^3.1.3",
+ "date-fns": "^4.1.0",
+ "date-fns-tz": "^3.2.0",
+ "evtd": "^0.2.4",
+ "highlight.js": "^11.8.0",
+ "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
+ "seemly": "^0.3.10",
+ "treemate": "^0.3.11",
+ "vdirs": "^0.1.8",
+ "vooks": "^0.2.12",
+ "vueuc": "^0.4.65"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/seemly": {
+ "version": "0.3.10",
+ "resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz",
+ "integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/treemate": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz",
+ "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
+ "license": "MIT"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vdirs": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz",
+ "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==",
+ "license": "MIT",
+ "dependencies": {
+ "evtd": "^0.2.2"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.11"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vooks": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz",
+ "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "evtd": "^0.2.2"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.33",
+ "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.33.tgz",
+ "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.33",
+ "@vue/compiler-sfc": "3.5.33",
+ "@vue/runtime-dom": "3.5.33",
+ "@vue/server-renderer": "3.5.33",
+ "@vue/shared": "3.5.33"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-tsc": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz",
+ "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.15",
+ "@vue/language-core": "2.2.12"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ },
+ "node_modules/vueuc": {
+ "version": "0.4.65",
+ "resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.65.tgz",
+ "integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@css-render/vue3-ssr": "^0.15.10",
+ "@juggle/resize-observer": "^3.3.1",
+ "css-render": "^0.15.10",
+ "evtd": "^0.2.4",
+ "seemly": "^0.3.6",
+ "vdirs": "^0.1.4",
+ "vooks": "^0.2.4"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.11"
+ }
+ }
+ }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..c5f5ca0
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "glm-desk-web",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 127.0.0.1 --port 5173",
+ "build": "vue-tsc --noEmit && vite build",
+ "preview": "vite preview --host 127.0.0.1 --port 4173",
+ "typecheck": "vue-tsc --noEmit"
+ },
+ "dependencies": {
+ "@vicons/ionicons5": "^0.13.0",
+ "naive-ui": "^2.42.0",
+ "vue": "^3.5.13"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.2.1",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.7",
+ "vue-tsc": "^2.2.0"
+ }
+}
\ No newline at end of file
diff --git a/web/src/App.vue b/web/src/App.vue
new file mode 100644
index 0000000..774ba0e
--- /dev/null
+++ b/web/src/App.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/DESIGN_SYSTEM.md b/web/src/DESIGN_SYSTEM.md
new file mode 100644
index 0000000..140180c
--- /dev/null
+++ b/web/src/DESIGN_SYSTEM.md
@@ -0,0 +1,41 @@
+# GLM Desk Design System
+
+Source: `ui-ux-pro-max` data-dense operations dashboard recommendation.
+
+## Product Pattern
+
+GLM Desk is an operations dashboard, not a marketing page. The interface prioritizes fast scanning, dense account rows, clear action states, and QR visibility.
+
+## Tokens
+
+- Background: layered warm/cool radial gradients over `#f8fafc`.
+- Panel: `rgba(255, 255, 255, 0.88)` with `0 24px 70px rgba(15, 23, 42, 0.1)`.
+- Ink: `#172033` for primary text.
+- Muted: `#64748b` for secondary text.
+- Accent: `#c99724`, keeping the old gold direction.
+- Support blue: `#3b82f6` for operational status surfaces.
+- Warm: `#f97316` for QR/payment attention.
+- Radius: 14px controls, 20-28px panels.
+
+## Layout
+
+- `AppShell` owns the hero command bar and primary actions.
+- `DashboardStats` creates three quick KPI cards: accounts, running flows, QR-ready rows.
+- `AccountTable` keeps dense row operations on desktop.
+- Mobile and narrow windows use a focusable horizontal table region to avoid viewport breakage.
+- `ImportAccountModal` and `AccountContextModal` keep destructive/long-form content out of the main table.
+
+## States
+
+- Loading: table content is wrapped in `NSpin`.
+- Empty: `NEmpty` is rendered when no accounts exist.
+- Error/success: `StatusBanner` uses `NAlert` and receives all async errors from `useDashboard`.
+- Pending actions: per-action loading keys disable visual ambiguity during import, sync, delete, run, and pause.
+
+## Accessibility
+
+- Primary interactive elements are real buttons, not clickable divs.
+- Focus rings are visible for keyboard users.
+- Table overflow container has `role="region"`, an accessible label, and `tabindex="0"`.
+- QR image uses descriptive alt text and lazy loading.
+- Motion is wrapped in `prefers-reduced-motion: no-preference`.
\ No newline at end of file
diff --git a/web/src/components/AccountContextModal.vue b/web/src/components/AccountContextModal.vue
new file mode 100644
index 0000000..936f342
--- /dev/null
+++ b/web/src/components/AccountContextModal.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ {{ copy.contextModal.label }}{{ detail.account.label }}
+ {{ copy.contextModal.customer }}{{ detail.session.customer_number || "-" }}
+ {{ copy.contextModal.name }}{{ detail.session.customer_name || "-" }}
+ {{ copy.contextModal.status }}{{ detail.account.account_status || copy.contextModal.unchecked }}
+ {{ copy.contextModal.schedule }}{{ detail.account.schedule_enabled ? detail.account.scheduled_start_time : copy.contextModal.disabled }}
+ {{ copy.contextModal.browser }}{{ detail.account.browser_impersonate || "-" }}
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/components/AccountTable.vue b/web/src/components/AccountTable.vue
new file mode 100644
index 0000000..d3970f8
--- /dev/null
+++ b/web/src/components/AccountTable.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+ {{ copy.table.title }}
+ {{ copy.table.subtitle(details.length) }}
+
+
+
+
+
+
+
+ {{ copy.table.columns.account }}
+ {{ copy.table.columns.product }}
+ {{ copy.table.columns.schedule }}
+ {{ copy.table.columns.status }}
+ {{ copy.table.columns.latestQr }}
+ {{ copy.table.columns.actions }}
+
+
+
+
+
+
+
+
+
+ {{ displayMode(detail) }} / {{ detail.account.browser_impersonate || copy.table.browserPending }}
+
+
+
+
+
+ emit('updateSchedule', id, enabled, time)"
+ />
+
+
+
+
+ {{ detail.account.account_status || copy.table.unchecked }}
+
+ {{ detail.account.last_schedule_status || detail.account.account_status_message || copy.table.noRecentEvent }}
+ {{ copy.table.scheduleState }}: {{ scheduleStateText(detail) }}
+
+ {{ copy.table.bizId }}: {{ latestTask(detail)?.biz_id }}
+
+
+
+
+
+
+
+ {{ isRunning(detail) ? copy.table.pause : copy.table.run }}
+
+
+ {{ copy.table.syncFingerprint }}
+
+
+
+
+ {{ copy.table.delete }}
+
+
+ {{ copy.table.deleteConfirm }}
+
+
+
+
+
+
+
diff --git a/web/src/components/AppShell.vue b/web/src/components/AppShell.vue
new file mode 100644
index 0000000..2c2d1a5
--- /dev/null
+++ b/web/src/components/AppShell.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ {{ copy.app.title }}
+ {{ copy.app.eyebrow }}
+
+
+ {{ health?.transport || copy.app.transportPending }}
+ {{ copy.app.refresh }}
+ {{ copy.app.importAccount }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/components/DashboardStats.vue b/web/src/components/DashboardStats.vue
new file mode 100644
index 0000000..ceee40f
--- /dev/null
+++ b/web/src/components/DashboardStats.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+ {{ copy.stats.accounts }}
+ {{ accountsTotal }}
+ {{ copy.stats.accountsHint }}
+
+
+ {{ copy.stats.running }}
+ {{ runningTotal }}
+ {{ copy.stats.runningHint }}
+
+
+ {{ copy.stats.qrReady }}
+ {{ qrTotal }}
+ {{ copy.stats.qrReadyHint }}
+
+
+
\ No newline at end of file
diff --git a/web/src/components/ImportAccountModal.vue b/web/src/components/ImportAccountModal.vue
new file mode 100644
index 0000000..0f41a98
--- /dev/null
+++ b/web/src/components/ImportAccountModal.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ copy.importModal.cancel }}
+
+ {{ copy.importModal.submit }}
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/components/QrPreview.vue b/web/src/components/QrPreview.vue
new file mode 100644
index 0000000..bfb20dd
--- /dev/null
+++ b/web/src/components/QrPreview.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
![]()
+
{{ copy.qr.empty }}
+
{{ paymentSummary() }}
+
+
diff --git a/web/src/components/ScheduleEditor.vue b/web/src/components/ScheduleEditor.vue
new file mode 100644
index 0000000..74fd1b6
--- /dev/null
+++ b/web/src/components/ScheduleEditor.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/components/StatusBanner.vue b/web/src/components/StatusBanner.vue
new file mode 100644
index 0000000..16a05ca
--- /dev/null
+++ b/web/src/components/StatusBanner.vue
@@ -0,0 +1,24 @@
+
+
+
+
+ {{ banner.text }}
+
+
\ No newline at end of file
diff --git a/web/src/composables/useDashboard.ts b/web/src/composables/useDashboard.ts
new file mode 100644
index 0000000..652d047
--- /dev/null
+++ b/web/src/composables/useDashboard.ts
@@ -0,0 +1,182 @@
+import { computed, onBeforeUnmount, ref } from "vue";
+import { zhCN as copy } from "../locales/zhCN";
+import { api } from "../services/api";
+import type {
+ AccountDetailResponse,
+ AccountImportPayload,
+ AccountPreferencesPayload,
+ HealthPayload
+} from "../types/api";
+
+export type BannerTone = "success" | "warning" | "error" | "info";
+
+export interface StatusBanner {
+ tone: BannerTone;
+ text: string;
+}
+
+const POLL_INTERVAL_MS = 5000;
+
+export function useDashboard() {
+ const details = ref([]);
+ const health = ref(null);
+ const loading = ref(false);
+ const actionKey = ref("");
+ const banner = ref(null);
+ let pollTimer: number | undefined;
+ let qrReminderReady = false;
+ let knownQrTaskKeys = new Set();
+
+ const accountsTotal = computed(() => details.value.length);
+ const runningTotal = computed(
+ () =>
+ details.value.filter(({ account }) =>
+ ["running", "pause_requested"].includes(String(account.last_schedule_status || "").toLowerCase())
+ ).length
+ );
+ const qrTotal = computed(() => details.value.filter(({ tasks }) => Boolean(tasks?.[0]?.qr_base64)).length);
+
+ function setBanner(text: string, tone: BannerTone = "success") {
+ banner.value = { text, tone };
+ }
+
+ function clearBanner() {
+ banner.value = null;
+ }
+
+ function qrTaskKey(detail: AccountDetailResponse) {
+ const task = detail.tasks?.[0];
+ if (!task?.qr_base64) {
+ return "";
+ }
+ return `${detail.account.id}:${task.id || task.biz_id || task.updated_at || ""}`;
+ }
+
+ function applyQrGeneratedReminder(nextDetails: AccountDetailResponse[]) {
+ const currentKeys = new Set();
+ const newQrDetails: AccountDetailResponse[] = [];
+ const shouldNotify = qrReminderReady && nextDetails.length > 1;
+
+ for (const detail of nextDetails) {
+ const key = qrTaskKey(detail);
+ if (!key) {
+ continue;
+ }
+ currentKeys.add(key);
+ if (shouldNotify && !knownQrTaskKeys.has(key)) {
+ newQrDetails.push(detail);
+ }
+ }
+
+ knownQrTaskKeys = currentKeys;
+ if (!qrReminderReady) {
+ qrReminderReady = true;
+ return false;
+ }
+ if (newQrDetails.length === 0) {
+ return false;
+ }
+
+ const labels = newQrDetails.map((detail) => detail.account.label || detail.account.id);
+ const firstTask = newQrDetails[0]?.tasks?.[0];
+ const text =
+ newQrDetails.length === 1
+ ? copy.feedback.qrGenerated(labels[0], firstTask?.biz_id || "")
+ : copy.feedback.qrGeneratedBatch(newQrDetails.length, labels.slice(0, 3).join("、"));
+ setBanner(text, "warning");
+ return true;
+ }
+
+ async function refreshDashboard(silent = false) {
+ if (!silent) {
+ loading.value = true;
+ }
+ try {
+ const [healthPayload, accounts] = await Promise.all([api.health(), api.listAccounts()]);
+ const detailPayloads = await Promise.all(accounts.map((account) => api.getAccount(account.id)));
+ health.value = healthPayload;
+ details.value = detailPayloads;
+ const qrReminderShown = applyQrGeneratedReminder(detailPayloads);
+ if (!silent && !qrReminderShown) {
+ setBanner(copy.feedback.dashboardRefreshed, "success");
+ }
+ } catch (error) {
+ setBanner(error instanceof Error ? error.message : copy.feedback.dashboardRefreshFailed, "error");
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ function startPolling() {
+ stopPolling();
+ pollTimer = window.setInterval(() => {
+ void refreshDashboard(true);
+ }, POLL_INTERVAL_MS);
+ }
+
+ function stopPolling() {
+ if (pollTimer) {
+ window.clearInterval(pollTimer);
+ pollTimer = undefined;
+ }
+ }
+
+ async function runAction(key: string, successText: string, action: () => Promise) {
+ actionKey.value = key;
+ try {
+ await action();
+ setBanner(successText, "success");
+ await refreshDashboard(true);
+ } catch (error) {
+ setBanner(error instanceof Error ? error.message : copy.feedback.operationFailed, "error");
+ } finally {
+ actionKey.value = "";
+ }
+ }
+
+ async function importAccount(payload: AccountImportPayload) {
+ await runAction("import", copy.feedback.accountImported, () => api.importAccount(payload));
+ }
+
+ async function updatePreferences(accountId: string, payload: AccountPreferencesPayload) {
+ await runAction(`prefs:${accountId}`, copy.feedback.preferencesSaved, () => api.updateAccount(accountId, payload));
+ }
+
+ async function syncAccount(accountId: string) {
+ await runAction(`sync:${accountId}`, copy.feedback.accountSynced, () => api.bootstrapAccount(accountId, true));
+ }
+
+ async function deleteAccount(accountId: string) {
+ await runAction(`delete:${accountId}`, copy.feedback.accountDeleted, () => api.deleteAccount(accountId));
+ }
+
+ async function runAccount(accountId: string) {
+ await runAction(`run:${accountId}`, copy.feedback.paymentStarted, () => api.runAccount(accountId));
+ }
+
+ async function pauseAccount(accountId: string) {
+ await runAction(`pause:${accountId}`, copy.feedback.pauseRequested, () => api.pauseAccount(accountId));
+ }
+
+ onBeforeUnmount(stopPolling);
+
+ return {
+ actionKey,
+ accountsTotal,
+ banner,
+ clearBanner,
+ deleteAccount,
+ details,
+ health,
+ importAccount,
+ loading,
+ pauseAccount,
+ qrTotal,
+ refreshDashboard,
+ runningTotal,
+ runAccount,
+ startPolling,
+ syncAccount,
+ updatePreferences
+ };
+}
diff --git a/web/src/env.d.ts b/web/src/env.d.ts
new file mode 100644
index 0000000..98a5402
--- /dev/null
+++ b/web/src/env.d.ts
@@ -0,0 +1,5 @@
+declare module "*.vue" {
+ import type { DefineComponent } from "vue";
+ const component: DefineComponent