diff --git a/.gitignore b/.gitignore
index c861b2b..f0ef255 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,13 +18,8 @@ release/
logs/
data/temp/
-# Runtime outputs
-data/output/
-data/result/
-data/input/
-data/product_cache.db
-data/user_settings.json
-*.db
+# Runtime data (all runtime outputs, caches, databases)
+data/
# Claude Code / IDE
.claude/
@@ -34,6 +29,15 @@ data/user_settings.json
# Old project
wework_xiaoai_bot/
+# Node.js
+node_modules/
+
+# Frontend build output
+web/backend/static/
+
+# Screenshots (from testing)
+*.png
+
# OS/IDE
.DS_Store
Thumbs.db
diff --git a/config.ini b/config.ini
index 25e1ef9..589d7db 100644
--- a/config.ini
+++ b/config.ini
@@ -40,4 +40,9 @@ version = 2026.05.05.0239
base_url = https://gitea.94kan.cn
owner = houhuan
repo = yixuan-sync-data
-token = 50b61e43a141d606ae2529cd1755bc666d800e08
\ No newline at end of file
+token = 50b61e43a141d606ae2529cd1755bc666d800e08
+
+[WebAuth]
+username = admin
+password_hash = $2b$12$nllT8o1QIMfWKuTlpQI3G./E2NS.gqf0EHZyNkJ8gMpVa9grTXRoC
+
diff --git a/web/backend/__init__.py b/web/backend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web/backend/auth/__init__.py b/web/backend/auth/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web/backend/auth/dependencies.py b/web/backend/auth/dependencies.py
new file mode 100644
index 0000000..d9ddf90
--- /dev/null
+++ b/web/backend/auth/dependencies.py
@@ -0,0 +1,58 @@
+"""FastAPI auth dependencies"""
+
+from fastapi import Depends, HTTPException, status, Query, Request
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+
+from .jwt_handler import decode_token
+
+security = HTTPBearer()
+
+
+async def get_current_user(
+ credentials: HTTPAuthorizationCredentials = Depends(security),
+) -> dict:
+ try:
+ payload = decode_token(credentials.credentials)
+ username = payload.get("sub")
+ if username is None:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
+ return {"username": username}
+ except Exception:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭据")
+
+
+async def get_current_user_ws(token: str = Query(...)) -> dict:
+ """WebSocket auth via query parameter"""
+ try:
+ payload = decode_token(token)
+ username = payload.get("sub")
+ if username is None:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
+ return {"username": username}
+ except Exception:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭据")
+
+
+async def get_current_user_flexible(
+ request: Request,
+ credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)),
+ token: str = Query(None),
+) -> dict:
+ """Auth from header OR query param (for file downloads in browser)."""
+ token_str = None
+ if credentials:
+ token_str = credentials.credentials
+ elif token:
+ token_str = token
+
+ if not token_str:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="未提供认证凭据")
+
+ try:
+ payload = decode_token(token_str)
+ username = payload.get("sub")
+ if username is None:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
+ return {"username": username}
+ except Exception:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭据")
diff --git a/web/backend/auth/jwt_handler.py b/web/backend/auth/jwt_handler.py
new file mode 100644
index 0000000..c08fa0d
--- /dev/null
+++ b/web/backend/auth/jwt_handler.py
@@ -0,0 +1,19 @@
+"""JWT token creation and validation"""
+
+from datetime import datetime, timedelta, timezone
+from typing import Optional
+
+from jose import jwt, JWTError
+
+from ..config import get_or_generate_secret, JWT_ALGORITHM, JWT_EXPIRE_HOURS
+
+
+def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
+ to_encode = data.copy()
+ expire = datetime.now(timezone.utc) + (expires_delta or timedelta(hours=JWT_EXPIRE_HOURS))
+ to_encode.update({"exp": expire})
+ return jwt.encode(to_encode, get_or_generate_secret(), algorithm=JWT_ALGORITHM)
+
+
+def decode_token(token: str) -> dict:
+ return jwt.decode(token, get_or_generate_secret(), algorithms=[JWT_ALGORITHM])
diff --git a/web/backend/auth/router.py b/web/backend/auth/router.py
new file mode 100644
index 0000000..81051ea
--- /dev/null
+++ b/web/backend/auth/router.py
@@ -0,0 +1,89 @@
+"""Auth API endpoints"""
+
+import os
+import bcrypt
+from fastapi import APIRouter, HTTPException, Depends, status
+from pydantic import BaseModel
+
+from .jwt_handler import create_access_token
+from .dependencies import get_current_user
+
+router = APIRouter(prefix="/api/auth", tags=["auth"])
+
+# Default credentials (should be changed on first login)
+DEFAULT_USERNAME = "admin"
+DEFAULT_PASSWORD = "admin123"
+
+
+class LoginRequest(BaseModel):
+ username: str
+ password: str
+
+
+class LoginResponse(BaseModel):
+ access_token: str
+ token_type: str = "bearer"
+
+
+def _get_credentials() -> tuple[str, bytes]:
+ """Get username and password hash from config or defaults"""
+ try:
+ from app.config.settings import ConfigManager
+ cfg = ConfigManager()
+ username = cfg.get('WebAuth', 'username', fallback=DEFAULT_USERNAME)
+ pw_hash = cfg.get('WebAuth', 'password_hash', fallback='')
+ if not pw_hash:
+ # First run: store default password hash
+ pw_hash = bcrypt.hashpw(DEFAULT_PASSWORD.encode(), bcrypt.gensalt()).decode()
+ try:
+ cfg.update('WebAuth', 'username', DEFAULT_USERNAME)
+ cfg.update('WebAuth', 'password_hash', pw_hash)
+ cfg.save_config()
+ except Exception:
+ pass
+ return username, pw_hash.encode()
+ except Exception:
+ return DEFAULT_USERNAME, bcrypt.hashpw(DEFAULT_PASSWORD.encode(), bcrypt.gensalt())
+
+
+@router.post("/login", response_model=LoginResponse)
+async def login(req: LoginRequest):
+ stored_username, stored_hash = _get_credentials()
+
+ if req.username != stored_username:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误")
+
+ if not bcrypt.checkpw(req.password.encode(), stored_hash):
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误")
+
+ token = create_access_token({"sub": req.username})
+ return LoginResponse(access_token=token)
+
+
+@router.get("/me")
+async def me(current_user: dict = Depends(get_current_user)):
+ return current_user
+
+
+class ChangePasswordRequest(BaseModel):
+ old_password: str
+ new_password: str
+
+
+@router.post("/change-password")
+async def change_password(req: ChangePasswordRequest, current_user: dict = Depends(get_current_user)):
+ _, stored_hash = _get_credentials()
+
+ if not bcrypt.checkpw(req.old_password.encode(), stored_hash):
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="旧密码错误")
+
+ new_hash = bcrypt.hashpw(req.new_password.encode(), bcrypt.gensalt()).decode()
+ try:
+ from app.config.settings import ConfigManager
+ cfg = ConfigManager()
+ cfg.update('WebAuth', 'password_hash', new_hash)
+ cfg.save_config()
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"保存密码失败: {e}")
+
+ return {"message": "密码修改成功"}
diff --git a/web/backend/config.py b/web/backend/config.py
new file mode 100644
index 0000000..dc9034e
--- /dev/null
+++ b/web/backend/config.py
@@ -0,0 +1,40 @@
+"""Web-specific configuration"""
+
+import os
+import secrets
+
+# JWT
+JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "")
+JWT_ALGORITHM = "HS256"
+JWT_EXPIRE_HOURS = 24
+
+# File upload
+MAX_UPLOAD_SIZE = 50 * 1024 * 1024 # 50MB
+ALLOWED_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp'}
+ALLOWED_EXCEL_EXTENSIONS = {'.xlsx', '.xls'}
+ALLOWED_EXTENSIONS = ALLOWED_IMAGE_EXTENSIONS | ALLOWED_EXCEL_EXTENSIONS
+
+# CORS
+CORS_ORIGINS = os.getenv("CORS_ORIGINS", "*").split(",")
+
+# Auth rate limit
+LOGIN_RATE_LIMIT = 5 # per minute
+
+
+def get_or_generate_secret() -> str:
+ """Get JWT secret from env or auto-generate on first run"""
+ global JWT_SECRET_KEY
+ if not JWT_SECRET_KEY:
+ secret_file = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
+ 'data', '.jwt_secret'
+ )
+ if os.path.exists(secret_file):
+ with open(secret_file, 'r') as f:
+ JWT_SECRET_KEY = f.read().strip()
+ if not JWT_SECRET_KEY:
+ JWT_SECRET_KEY = secrets.token_urlsafe(48)
+ os.makedirs(os.path.dirname(secret_file), exist_ok=True)
+ with open(secret_file, 'w') as f:
+ f.write(JWT_SECRET_KEY)
+ return JWT_SECRET_KEY
diff --git a/web/backend/main.py b/web/backend/main.py
new file mode 100644
index 0000000..ec82082
--- /dev/null
+++ b/web/backend/main.py
@@ -0,0 +1,109 @@
+"""FastAPI application entry point for the web-based OCR order processing system."""
+
+import sys
+import os
+from contextlib import asynccontextmanager
+from pathlib import Path
+
+# Ensure app/ is importable
+_web_dir = Path(__file__).resolve().parent.parent # web/
+_project_root = _web_dir.parent # project root
+if str(_project_root) not in sys.path:
+ sys.path.insert(0, str(_project_root))
+
+from fastapi import FastAPI
+from fastapi.staticfiles import StaticFiles
+from fastapi.responses import FileResponse
+
+from .config import get_or_generate_secret # noqa: trigger secret generation
+from .services.task_manager import TaskManager
+from .services.db_pool import DBPool
+from .auth.router import router as auth_router
+from .routers.files import router as files_router
+from .routers.processing import router as processing_router
+from .routers.memory import router as memory_router
+from .routers.config_api import router as config_router
+from .routers.barcodes import router as barcodes_router
+from .routers.sync import router as sync_router
+from .routers.websocket import router as ws_router
+from .routers.logs import router as logs_router
+from .routers.tasks import router as tasks_router
+from .middleware.logging import LoggingMiddleware
+
+# Shared singletons
+task_manager = TaskManager()
+db_pool = DBPool()
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ """Initialize shared resources on startup."""
+ from app.config.settings import ConfigManager
+ ConfigManager()
+
+ # Initialize DB and cleanup old records
+ from .services.db_schema import init_db, cleanup_old_records
+ init_db()
+ cleanup_old_records()
+
+ # Wire up DB pool to task manager
+ task_manager.set_db_pool(db_pool)
+
+ app.state.task_manager = task_manager
+ app.state.db_pool = db_pool
+ yield
+
+
+app = FastAPI(
+ title="益选 OCR 订单处理系统",
+ version="1.0.0",
+ lifespan=lifespan,
+)
+
+# CORS
+from fastapi.middleware.cors import CORSMiddleware
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["http://localhost:5173", "http://127.0.0.1:5173", "http://localhost:8000"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# HTTP logging middleware (after CORS, before routes)
+app.add_middleware(LoggingMiddleware)
+
+# Make task_manager and db_pool accessible via request.state
+@app.middleware("http")
+async def inject_services(request, call_next):
+ request.state.task_manager = task_manager
+ request.state.db_pool = db_pool
+ return await call_next(request)
+
+
+# Mount routers
+app.include_router(auth_router)
+app.include_router(files_router)
+app.include_router(processing_router)
+app.include_router(memory_router)
+app.include_router(config_router)
+app.include_router(barcodes_router)
+app.include_router(sync_router)
+app.include_router(ws_router)
+app.include_router(logs_router)
+app.include_router(tasks_router)
+
+
+# Serve Vue SPA static files
+_static_dir = Path(__file__).resolve().parent / "static"
+if _static_dir.is_dir():
+ app.mount("/assets", StaticFiles(directory=str(_static_dir / "assets")), name="assets")
+
+ @app.get("/{full_path:path}")
+ async def serve_spa(full_path: str):
+ """Catch-all: serve index.html for Vue Router history mode."""
+ file_path = _static_dir / full_path
+ if file_path.is_file():
+ return FileResponse(str(file_path))
+ return FileResponse(str(_static_dir / "index.html"))
diff --git a/web/backend/routers/__init__.py b/web/backend/routers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web/backend/routers/barcodes.py b/web/backend/routers/barcodes.py
new file mode 100644
index 0000000..ff9b92a
--- /dev/null
+++ b/web/backend/routers/barcodes.py
@@ -0,0 +1,124 @@
+"""Barcode mapping CRUD endpoints."""
+
+import json
+from pathlib import Path
+from typing import Dict, Optional, List
+
+from fastapi import APIRouter, HTTPException, Depends
+from pydantic import BaseModel
+
+from ..auth.dependencies import get_current_user
+
+router = APIRouter(prefix="/api/barcodes", tags=["barcodes"])
+
+_project_root = Path(__file__).resolve().parent.parent.parent.parent
+_mappings_file = _project_root / "config" / "barcode_mappings.json"
+
+
+class BarcodeMapping(BaseModel):
+ barcode: str
+ target: str
+ description: Optional[str] = None
+
+
+class BarcodeMappingUpdate(BaseModel):
+ target: Optional[str] = None
+ description: Optional[str] = None
+
+
+def _load_mappings() -> Dict:
+ if not _mappings_file.is_file():
+ return {}
+ try:
+ return json.loads(_mappings_file.read_text(encoding="utf-8"))
+ except Exception:
+ return {}
+
+
+def _save_mappings(data: Dict):
+ _mappings_file.parent.mkdir(parents=True, exist_ok=True)
+ _mappings_file.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
+
+
+@router.get("")
+async def list_barcodes(
+ search: str = "",
+ current_user: dict = Depends(get_current_user),
+):
+ mappings = _load_mappings()
+ items = []
+ for barcode, info in mappings.items():
+ if isinstance(info, dict):
+ target = info.get("map_to", info.get("target", ""))
+ desc = info.get("description", "")
+ else:
+ target = str(info)
+ desc = ""
+ if search and search not in barcode and search not in target and search not in desc:
+ continue
+ items.append({"barcode": barcode, "target": target, "description": desc})
+ return {"items": items, "total": len(items)}
+
+
+@router.get("/{barcode}")
+async def get_barcode(
+ barcode: str,
+ current_user: dict = Depends(get_current_user),
+):
+ mappings = _load_mappings()
+ if barcode not in mappings:
+ raise HTTPException(404, f"未找到条码映射 {barcode}")
+ info = mappings[barcode]
+ if isinstance(info, dict):
+ return {"barcode": barcode, "target": info.get("map_to", info.get("target", "")), "description": info.get("description", "")}
+ return {"barcode": barcode, "target": str(info), "description": ""}
+
+
+@router.post("")
+async def create_barcode(
+ body: BarcodeMapping,
+ current_user: dict = Depends(get_current_user),
+):
+ mappings = _load_mappings()
+ if body.barcode in mappings:
+ raise HTTPException(409, f"条码 {body.barcode} 已存在")
+ mappings[body.barcode] = {"map_to": body.target, "description": body.description or ""}
+ _save_mappings(mappings)
+ return {"message": f"已创建映射 {body.barcode} → {body.target}"}
+
+
+@router.put("/{barcode}")
+async def update_barcode(
+ barcode: str,
+ body: BarcodeMappingUpdate,
+ current_user: dict = Depends(get_current_user),
+):
+ mappings = _load_mappings()
+ if barcode not in mappings:
+ raise HTTPException(404, f"未找到条码映射 {barcode}")
+
+ existing = mappings[barcode]
+ if not isinstance(existing, dict):
+ existing = {"map_to": str(existing), "description": ""}
+
+ if body.target is not None:
+ existing["map_to"] = body.target
+ if body.description is not None:
+ existing["description"] = body.description
+
+ mappings[barcode] = existing
+ _save_mappings(mappings)
+ return {"message": f"已更新映射 {barcode}"}
+
+
+@router.delete("/{barcode}")
+async def delete_barcode(
+ barcode: str,
+ current_user: dict = Depends(get_current_user),
+):
+ mappings = _load_mappings()
+ if barcode not in mappings:
+ raise HTTPException(404, f"未找到条码映射 {barcode}")
+ del mappings[barcode]
+ _save_mappings(mappings)
+ return {"message": f"已删除映射 {barcode}"}
diff --git a/web/backend/routers/config_api.py b/web/backend/routers/config_api.py
new file mode 100644
index 0000000..2aeecdb
--- /dev/null
+++ b/web/backend/routers/config_api.py
@@ -0,0 +1,98 @@
+"""Configuration read/write endpoints."""
+
+from typing import Dict, Optional, Any
+from fastapi import APIRouter, HTTPException, Depends
+from pydantic import BaseModel
+
+from ..auth.dependencies import get_current_user
+
+router = APIRouter(prefix="/api/config", tags=["config"])
+
+# Keys that should be masked in GET responses
+_SENSITIVE_KEYS = {"api_key", "secret_key", "token", "password", "api_secret", "access_key"}
+
+# Sections to expose (match actual config.ini)
+_ALLOWED_SECTIONS = {"API", "Paths", "Performance", "File", "Templates", "Gitea", "WebAuth"}
+
+
+class ConfigUpdate(BaseModel):
+ section: str
+ key: str
+ value: str
+
+
+class ConfigBulkUpdate(BaseModel):
+ updates: list[ConfigUpdate]
+
+
+def _get_config():
+ from app.config.settings import ConfigManager
+ return ConfigManager()
+
+
+def _mask_value(key: str, value: str) -> str:
+ if any(s in key.lower() for s in _SENSITIVE_KEYS):
+ if len(value) > 4:
+ return value[:2] + "*" * (len(value) - 4) + value[-2:]
+ return "****"
+ return value
+
+
+@router.get("")
+async def get_config(
+ section: Optional[str] = None,
+ current_user: dict = Depends(get_current_user),
+):
+ cfg = _get_config()
+ if section:
+ if section not in _ALLOWED_SECTIONS and section != "DEFAULT":
+ raise HTTPException(403, f"不允许访问配置节: {section}")
+ items = {}
+ for key, value in cfg.config.items(section):
+ items[key] = _mask_value(key, value)
+ return {"section": section, "items": items}
+
+ result = {}
+ for sec in _ALLOWED_SECTIONS:
+ try:
+ items = {}
+ for key, value in cfg.config.items(sec):
+ items[key] = _mask_value(key, value)
+ result[sec] = items
+ except Exception:
+ pass
+ return result
+
+
+@router.put("")
+async def update_config(
+ body: ConfigUpdate,
+ current_user: dict = Depends(get_current_user),
+):
+ if body.section not in _ALLOWED_SECTIONS:
+ raise HTTPException(403, f"不允许修改配置节: {body.section}")
+
+ cfg = _get_config()
+ try:
+ cfg.update(body.section, body.key, body.value)
+ cfg.save_config()
+ return {"message": f"已更新 [{body.section}] {body.key}"}
+ except Exception as e:
+ raise HTTPException(500, f"保存失败: {e}")
+
+
+@router.put("/bulk")
+async def bulk_update_config(
+ body: ConfigBulkUpdate,
+ current_user: dict = Depends(get_current_user),
+):
+ cfg = _get_config()
+ updated = []
+ for item in body.updates:
+ if item.section not in _ALLOWED_SECTIONS:
+ continue
+ cfg.update(item.section, item.key, item.value)
+ updated.append(f"[{item.section}] {item.key}")
+
+ cfg.save_config()
+ return {"message": f"已更新 {len(updated)} 项", "updated": updated}
diff --git a/web/backend/routers/logs.py b/web/backend/routers/logs.py
index b4b7f11..88c1636 100644
--- a/web/backend/routers/logs.py
+++ b/web/backend/routers/logs.py
@@ -49,7 +49,7 @@ def _count_http_logs(
row = conn.execute(
f"SELECT COUNT(*) as cnt FROM http_logs{where}", params
).fetchone()
- return row["cnt"] if row else 0
+ return row[0] if row else 0
finally:
conn.close()
diff --git a/web/backend/routers/memory.py b/web/backend/routers/memory.py
new file mode 100644
index 0000000..389c9e5
--- /dev/null
+++ b/web/backend/routers/memory.py
@@ -0,0 +1,165 @@
+"""Product memory CRUD endpoints."""
+
+from typing import Optional, List, Dict
+from pathlib import Path
+
+from fastapi import APIRouter, HTTPException, Depends, Query
+from pydantic import BaseModel
+
+from ..auth.dependencies import get_current_user
+
+router = APIRouter(prefix="/api/memory", tags=["memory"])
+
+_project_root = Path(__file__).resolve().parent.parent.parent.parent
+_db_path = str(_project_root / "data" / "product_cache.db")
+_excel_source = str(_project_root / "templates" / "商品资料.xlsx")
+
+
+class MemoryItem(BaseModel):
+ barcode: str
+ name: str
+ spec: Optional[str] = None
+ unit: Optional[str] = None
+ price: Optional[float] = None
+ confidence: int = 0
+ source: str = "ocr"
+ last_used: Optional[str] = None
+ use_count: int = 0
+
+
+class MemoryUpdate(BaseModel):
+ name: Optional[str] = None
+ spec: Optional[str] = None
+ unit: Optional[str] = None
+ price: Optional[float] = None
+ confidence: Optional[int] = None
+
+
+class MemoryListResponse(BaseModel):
+ items: List[MemoryItem]
+ total: int
+ page: int
+ page_size: int
+
+
+def _get_db():
+ from app.core.db.product_db import ProductDatabase
+ return ProductDatabase(_db_path, _excel_source)
+
+
+def _row_to_item(row: Dict) -> MemoryItem:
+ return MemoryItem(
+ barcode=row.get("barcode", ""),
+ name=row.get("name", ""),
+ spec=row.get("spec"),
+ unit=row.get("unit"),
+ price=row.get("price"),
+ confidence=row.get("confidence", 0),
+ source=row.get("source", "ocr"),
+ last_used=row.get("last_used"),
+ use_count=row.get("use_count", 0),
+ )
+
+
+@router.get("", response_model=MemoryListResponse)
+async def list_memory(
+ search: str = "",
+ page: int = Query(1, ge=1),
+ page_size: int = Query(50, ge=1, le=200),
+ current_user: dict = Depends(get_current_user),
+):
+ db = _get_db()
+ results = db.get_all_memories()
+
+ if search:
+ s = search.lower()
+ results = [r for r in results if s in r.get("barcode", "").lower() or s in r.get("name", "").lower()]
+
+ total = len(results)
+ start = (page - 1) * page_size
+ page_items = results[start:start + page_size]
+
+ return MemoryListResponse(
+ items=[_row_to_item(r) for r in page_items],
+ total=total,
+ page=page,
+ page_size=page_size,
+ )
+
+
+@router.get("/{barcode}")
+async def get_memory(
+ barcode: str,
+ current_user: dict = Depends(get_current_user),
+):
+ db = _get_db()
+ product = db.get_memory(barcode)
+ if not product:
+ raise HTTPException(404, f"未找到条码 {barcode} 的记忆记录")
+ return product
+
+
+@router.put("/{barcode}")
+async def update_memory(
+ barcode: str,
+ body: MemoryUpdate,
+ current_user: dict = Depends(get_current_user),
+):
+ db = _get_db()
+ existing = db.get_memory(barcode)
+ if not existing:
+ raise HTTPException(404, f"未找到条码 {barcode}")
+
+ update_data = body.model_dump(exclude_none=True)
+ if not update_data:
+ raise HTTPException(400, "没有提供更新数据")
+
+ db.update_memory(barcode, update_data)
+ return {"message": f"已更新 {barcode}", "updated_fields": list(update_data.keys())}
+
+
+@router.delete("/{barcode}")
+async def delete_memory(
+ barcode: str,
+ current_user: dict = Depends(get_current_user),
+):
+ db = _get_db()
+ existing = db.get_memory(barcode)
+ if not existing:
+ raise HTTPException(404, f"未找到条码 {barcode}")
+ db.delete_memory(barcode)
+ return {"message": f"已删除 {barcode}"}
+
+
+@router.post("/reimport")
+async def reimport_memory(
+ current_user: dict = Depends(get_current_user),
+):
+ db = _get_db()
+ try:
+ count = db.reimport()
+ return {"message": f"重新导入完成,共导入 {count} 条记录", "count": count}
+ except Exception as e:
+ raise HTTPException(500, f"导入失败: {e}")
+
+
+@router.get("/export/sync")
+async def export_memory(
+ current_user: dict = Depends(get_current_user),
+):
+ db = _get_db()
+ data = db.export_for_sync()
+ return {"data": data, "count": len(data)}
+
+
+@router.post("/import/sync")
+async def import_memory(
+ data: dict,
+ current_user: dict = Depends(get_current_user),
+):
+ db = _get_db()
+ try:
+ count = db.import_from_sync(data.get("data", []))
+ return {"message": f"导入完成,共 {count} 条", "count": count}
+ except Exception as e:
+ raise HTTPException(500, f"导入失败: {e}")
diff --git a/web/backend/routers/processing.py b/web/backend/routers/processing.py
new file mode 100644
index 0000000..7237b7c
--- /dev/null
+++ b/web/backend/routers/processing.py
@@ -0,0 +1,250 @@
+"""Processing endpoints: OCR, Excel conversion, merge, and full pipeline."""
+
+import os
+import sys
+import traceback
+from pathlib import Path
+from typing import Optional, List
+
+from fastapi import APIRouter, HTTPException, Depends, Request
+from pydantic import BaseModel
+
+from ..auth.dependencies import get_current_user
+from ..services.service_wrapper import ServiceWrapper
+
+router = APIRouter(prefix="/api/processing", tags=["processing"])
+
+_wrapper = ServiceWrapper(max_workers=3)
+
+_project_root = Path(__file__).resolve().parent.parent.parent.parent
+_input_dir = _project_root / "data" / "input"
+_output_dir = _project_root / "data" / "output"
+_result_dir = _project_root / "data" / "result"
+
+
+class PipelineRequest(BaseModel):
+ files: Optional[List[str]] = None # specific files, or None = all in input/
+ supplier: Optional[str] = None # force supplier type
+
+
+class TaskResponse(BaseModel):
+ task_id: str
+ status: str
+ message: str
+
+
+def _get_task_manager(request: Request):
+ return request.state.task_manager
+
+
+def _list_input_files(filter_ext: Optional[List[str]] = None) -> List[Path]:
+ if not _input_dir.is_dir():
+ return []
+ files = []
+ for f in sorted(_input_dir.iterdir()):
+ if f.is_file():
+ if filter_ext is None or f.suffix.lower() in filter_ext:
+ files.append(f)
+ return files
+
+
+@router.post("/ocr-batch", response_model=TaskResponse)
+async def ocr_batch(
+ request: Request,
+ current_user: dict = Depends(get_current_user),
+):
+ """Run OCR on all images in input/."""
+ tm = _get_task_manager(request)
+ task = tm.create_task("批量OCR识别")
+
+ image_exts = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'}
+ files = _list_input_files(filter_ext=list(image_exts))
+ if not files:
+ raise HTTPException(400, "input/ 目录中没有图片文件")
+
+ async def _run():
+ try:
+ from app.services.ocr_service import OCRService
+ svc = OCRService()
+ total = len(files)
+ for i, f in enumerate(files):
+ tm.update_progress(task.id, int((i / total) * 100), f"正在识别: {f.name}")
+ tm.add_log(task.id, f"[OCR] 处理 {f.name}")
+ try:
+ svc.process_single(str(f), str(_output_dir))
+ tm.add_log(task.id, f"[OCR] 完成: {f.name}")
+ except Exception as e:
+ tm.add_log(task.id, f"[OCR] 失败: {f.name} - {e}")
+ result_files = [f.name for f in _output_dir.iterdir() if f.is_file()]
+ tm.set_completed(task.id, result_files=result_files, message=f"OCR完成,共处理 {total} 个文件")
+ except Exception as e:
+ tm.set_failed(task.id, str(e))
+
+ import asyncio
+ asyncio.create_task(_run())
+
+ return TaskResponse(task_id=task.id, status="accepted", message="OCR任务已创建")
+
+
+@router.post("/excel", response_model=TaskResponse)
+async def process_excel(
+ request: Request,
+ body: PipelineRequest = PipelineRequest(),
+ current_user: dict = Depends(get_current_user),
+):
+ """Convert OCR output Excel files to standardized format."""
+ tm = _get_task_manager(request)
+ task = tm.create_task("Excel标准化处理")
+
+ excel_exts = {'.xls', '.xlsx'}
+ if body.files:
+ files = [_output_dir / f for f in body.files if (_output_dir / f).is_file()]
+ else:
+ files = _list_input_files(filter_ext=list(excel_exts))
+ if not files:
+ files = _list_input_files_from(_output_dir, filter_ext=list(excel_exts))
+
+ if not files:
+ raise HTTPException(400, "没有找到Excel文件")
+
+ async def _run():
+ try:
+ from app.services.order_service import OrderService
+ svc = OrderService()
+ total = len(files)
+ for i, f in enumerate(files):
+ tm.update_progress(task.id, int((i / total) * 100), f"正在处理: {f.name}")
+ tm.add_log(task.id, f"[Excel] 处理 {f.name}")
+ try:
+ svc.process_excel(str(f), str(_result_dir))
+ tm.add_log(task.id, f"[Excel] 完成: {f.name}")
+ except Exception as e:
+ tm.add_log(task.id, f"[Excel] 失败: {f.name} - {e}")
+ result_files = [f.name for f in _result_dir.iterdir() if f.is_file()]
+ tm.set_completed(task.id, result_files=result_files, message=f"Excel处理完成,共 {total} 个文件")
+ except Exception as e:
+ tm.set_failed(task.id, str(e))
+
+ import asyncio
+ asyncio.create_task(_run())
+
+ return TaskResponse(task_id=task.id, status="accepted", message="Excel处理任务已创建")
+
+
+@router.post("/merge", response_model=TaskResponse)
+async def merge_orders(
+ request: Request,
+ current_user: dict = Depends(get_current_user),
+):
+ """Merge all processed Excel files into a single purchase order."""
+ tm = _get_task_manager(request)
+ task = tm.create_task("合并采购单")
+
+ async def _run():
+ try:
+ from app.services.order_service import OrderService
+ svc = OrderService()
+ tm.update_progress(task.id, 20, "正在合并采购单...")
+ tm.add_log(task.id, "[合并] 开始合并")
+ result = svc.merge_orders(str(_result_dir))
+ tm.add_log(task.id, f"[合并] 完成: {result}")
+ tm.set_completed(task.id, result_files=[result] if result else [], message="合并完成")
+ except Exception as e:
+ tm.set_failed(task.id, str(e))
+
+ import asyncio
+ asyncio.create_task(_run())
+
+ return TaskResponse(task_id=task.id, status="accepted", message="合并任务已创建")
+
+
+@router.post("/pipeline", response_model=TaskResponse)
+async def full_pipeline(
+ request: Request,
+ body: PipelineRequest = PipelineRequest(),
+ current_user: dict = Depends(get_current_user),
+):
+ """Run the full pipeline: OCR → Excel → Merge."""
+ tm = _get_task_manager(request)
+ task = tm.create_task("一键全流程处理")
+
+ async def _run():
+ try:
+ # Step 1: OCR
+ tm.update_progress(task.id, 0, "步骤 1/3: OCR识别")
+ tm.add_log(task.id, "[Pipeline] 开始OCR识别")
+ from app.services.ocr_service import OCRService
+ ocr_svc = OCRService()
+
+ image_exts = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'}
+ images = _list_input_files(filter_ext=list(image_exts))
+ for i, f in enumerate(images):
+ pct = int((i / max(len(images), 1)) * 30)
+ tm.update_progress(task.id, pct, f"OCR: {f.name}")
+ try:
+ ocr_svc.process_single(str(f), str(_output_dir))
+ tm.add_log(task.id, f"[OCR] 完成: {f.name}")
+ except Exception as e:
+ tm.add_log(task.id, f"[OCR] 失败: {f.name} - {e}")
+
+ # Step 2: Excel conversion
+ tm.update_progress(task.id, 35, "步骤 2/3: Excel标准化")
+ tm.add_log(task.id, "[Pipeline] 开始Excel处理")
+ from app.services.order_service import OrderService
+ order_svc = OrderService()
+
+ excel_files = list(_output_dir.glob("*.xls")) + list(_output_dir.glob("*.xlsx"))
+ for i, f in enumerate(excel_files):
+ pct = 35 + int((i / max(len(excel_files), 1)) * 35)
+ tm.update_progress(task.id, pct, f"Excel: {f.name}")
+ try:
+ order_svc.process_excel(str(f), str(_result_dir))
+ tm.add_log(task.id, f"[Excel] 完成: {f.name}")
+ except Exception as e:
+ tm.add_log(task.id, f"[Excel] 失败: {f.name} - {e}")
+
+ # Step 3: Merge
+ tm.update_progress(task.id, 75, "步骤 3/3: 合并采购单")
+ tm.add_log(task.id, "[Pipeline] 开始合并")
+ try:
+ result = order_svc.merge_orders(str(_result_dir))
+ tm.add_log(task.id, f"[合并] 完成: {result}")
+ except Exception as e:
+ tm.add_log(task.id, f"[合并] 失败: {e}")
+ result = None
+
+ result_files = [f.name for f in _result_dir.iterdir() if f.is_file()]
+ tm.set_completed(task.id, result_files=result_files, message="全流程处理完成")
+ except Exception as e:
+ tb = traceback.format_exc()
+ tm.add_log(task.id, f"[错误] {tb}")
+ tm.set_failed(task.id, str(e))
+
+ import asyncio
+ asyncio.create_task(_run())
+
+ return TaskResponse(task_id=task.id, status="accepted", message="全流程任务已创建")
+
+
+@router.get("/status/{task_id}")
+async def get_task_status(
+ task_id: str,
+ request: Request,
+ current_user: dict = Depends(get_current_user),
+):
+ tm = _get_task_manager(request)
+ task = tm.get_task(task_id)
+ if not task:
+ raise HTTPException(404, "任务不存在")
+ return task.to_dict()
+
+
+def _list_input_files_from(directory: Path, filter_ext: List[str] = None) -> List[Path]:
+ if not directory.is_dir():
+ return []
+ files = []
+ for f in sorted(directory.iterdir()):
+ if f.is_file():
+ if filter_ext is None or f.suffix.lower() in filter_ext:
+ files.append(f)
+ return files
diff --git a/web/backend/routers/sync.py b/web/backend/routers/sync.py
new file mode 100644
index 0000000..2f70935
--- /dev/null
+++ b/web/backend/routers/sync.py
@@ -0,0 +1,93 @@
+"""Cloud sync endpoints (Gitea-based)."""
+
+from pathlib import Path
+
+from fastapi import APIRouter, HTTPException, Depends, Request
+from pydantic import BaseModel
+
+from ..auth.dependencies import get_current_user
+from ..services.task_manager import TaskManager
+
+router = APIRouter(prefix="/api/sync", tags=["sync"])
+
+_project_root = Path(__file__).resolve().parent.parent.parent.parent
+
+
+class SyncResponse(BaseModel):
+ task_id: str
+ status: str
+ message: str
+
+
+def _get_sync():
+ from app.core.utils.cloud_sync import GiteaSync
+ from app.config.settings import ConfigManager
+ cfg = ConfigManager()
+ return GiteaSync(cfg)
+
+
+@router.post("/push", response_model=SyncResponse)
+async def sync_push(
+ request: Request,
+ current_user: dict = Depends(get_current_user),
+):
+ tm = request.state.task_manager
+ task = tm.create_task("推送到云端")
+
+ async def _run():
+ try:
+ tm.update_progress(task.id, 10, "正在初始化同步...")
+ sync = _get_sync()
+ tm.update_progress(task.id, 30, "正在推送文件...")
+ tm.add_log(task.id, "[Push] 开始推送")
+ result = sync.push()
+ tm.add_log(task.id, f"[Push] 完成: {result}")
+ tm.set_completed(task.id, message="推送完成")
+ except Exception as e:
+ tm.set_failed(task.id, str(e))
+
+ import asyncio
+ asyncio.create_task(_run())
+ return SyncResponse(task_id=task.id, status="accepted", message="推送任务已创建")
+
+
+@router.post("/pull", response_model=SyncResponse)
+async def sync_pull(
+ request: Request,
+ current_user: dict = Depends(get_current_user),
+):
+ tm = request.state.task_manager
+ task = tm.create_task("从云端拉取")
+
+ async def _run():
+ try:
+ tm.update_progress(task.id, 10, "正在初始化同步...")
+ sync = _get_sync()
+ tm.update_progress(task.id, 30, "正在拉取文件...")
+ tm.add_log(task.id, "[Pull] 开始拉取")
+ result = sync.pull()
+ tm.add_log(task.id, f"[Pull] 完成: {result}")
+ tm.set_completed(task.id, message="拉取完成")
+ except Exception as e:
+ tm.set_failed(task.id, str(e))
+
+ import asyncio
+ asyncio.create_task(_run())
+ return SyncResponse(task_id=task.id, status="accepted", message="拉取任务已创建")
+
+
+@router.get("/status")
+async def sync_status(
+ current_user: dict = Depends(get_current_user),
+):
+ try:
+ from app.config.settings import ConfigManager
+ cfg = ConfigManager()
+ base_url = cfg.get("Gitea", "base_url", fallback="")
+ owner = cfg.get("Gitea", "owner", fallback="")
+ repo = cfg.get("Gitea", "repo", fallback="")
+ enabled = bool(base_url and owner and repo)
+ repo_url = f"{base_url}/{owner}/{repo}" if enabled else ""
+ return {"enabled": enabled, "repo_url": repo_url}
+ except Exception:
+ return {"enabled": False, "repo_url": ""}
diff --git a/web/backend/routers/websocket.py b/web/backend/routers/websocket.py
new file mode 100644
index 0000000..d7cdb0f
--- /dev/null
+++ b/web/backend/routers/websocket.py
@@ -0,0 +1,47 @@
+"""WebSocket endpoint for real-time task progress."""
+
+from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
+from ..auth.jwt_handler import decode_token
+from jose import JWTError
+
+router = APIRouter(tags=["websocket"])
+
+
+@router.websocket("/ws/task/{task_id}")
+async def task_websocket(
+ websocket: WebSocket,
+ task_id: str,
+ token: str = Query(...),
+):
+ """WebSocket for real-time task progress updates."""
+ try:
+ payload = decode_token(token)
+ username = payload.get("sub")
+ if not username:
+ await websocket.close(code=4001, reason="Invalid token")
+ return
+ except (JWTError, Exception):
+ await websocket.close(code=4001, reason="Invalid token")
+ return
+
+ await websocket.accept()
+
+ tm = websocket.app.state.task_manager
+ task = tm.get_task(task_id)
+ if not task:
+ await websocket.send_json({"error": "任务不存在"})
+ await websocket.close()
+ return
+
+ tm.subscribe(task_id, websocket)
+ await websocket.send_json(task.to_dict())
+
+ try:
+ while True:
+ data = await websocket.receive_text()
+ if data == "ping":
+ await websocket.send_text("pong")
+ except WebSocketDisconnect:
+ tm.unsubscribe(task_id, websocket)
+ except Exception:
+ tm.unsubscribe(task_id, websocket)
diff --git a/web/backend/schemas/__init__.py b/web/backend/schemas/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web/backend/services/__init__.py b/web/backend/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web/backend/services/db_pool.py b/web/backend/services/db_pool.py
new file mode 100644
index 0000000..85c2ac4
--- /dev/null
+++ b/web/backend/services/db_pool.py
@@ -0,0 +1,20 @@
+"""SQLite write serialization for async context"""
+
+import asyncio
+from typing import Callable, Any
+
+
+class DBPool:
+ """Serializes SQLite writes via asyncio.Lock. Reads are concurrent."""
+
+ def __init__(self):
+ self._write_lock = asyncio.Lock()
+
+ async def execute_write(self, fn: Callable, *args, **kwargs) -> Any:
+ async with self._write_lock:
+ loop = asyncio.get_event_loop()
+ return await loop.run_in_executor(None, lambda: fn(*args, **kwargs))
+
+ async def execute_read(self, fn: Callable, *args, **kwargs) -> Any:
+ loop = asyncio.get_event_loop()
+ return await loop.run_in_executor(None, lambda: fn(*args, **kwargs))
diff --git a/web/backend/services/service_wrapper.py b/web/backend/services/service_wrapper.py
new file mode 100644
index 0000000..ea44be3
--- /dev/null
+++ b/web/backend/services/service_wrapper.py
@@ -0,0 +1,19 @@
+"""Async wrapper for synchronous app/ services"""
+
+import asyncio
+from concurrent.futures import ThreadPoolExecutor
+from typing import Callable, Any
+
+
+class ServiceWrapper:
+ """Wraps synchronous services for async FastAPI endpoints."""
+
+ def __init__(self, max_workers: int = 3):
+ self._executor = ThreadPoolExecutor(max_workers=max_workers)
+
+ async def run_sync(self, fn: Callable, *args, **kwargs) -> Any:
+ loop = asyncio.get_event_loop()
+ return await loop.run_in_executor(
+ self._executor,
+ lambda: fn(*args, **kwargs)
+ )
diff --git a/web/frontend/index.html b/web/frontend/index.html
new file mode 100644
index 0000000..77259c5
--- /dev/null
+++ b/web/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ 益选 OCR 订单处理系统
+
+
+
+
+
+
diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json
new file mode 100644
index 0000000..853072b
--- /dev/null
+++ b/web/frontend/package-lock.json
@@ -0,0 +1,1929 @@
+{
+ "name": "xiaoaitext-web",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "xiaoaitext-web",
+ "version": "1.0.0",
+ "dependencies": {
+ "@element-plus/icons-vue": "^2.3.0",
+ "axios": "^1.6.0",
+ "element-plus": "^2.5.0",
+ "pinia": "^2.1.0",
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.0",
+ "typescript": "^5.3.0",
+ "vite": "^5.1.0",
+ "vue-tsc": "^2.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+ "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.npmjs.org/@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/@ctrl/tinycolor": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
+ "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@element-plus/icons-vue": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+ "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@popperjs/core": {
+ "name": "@sxzz/popperjs-es",
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+ "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
+ "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz",
+ "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz",
+ "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz",
+ "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz",
+ "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz",
+ "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz",
+ "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz",
+ "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz",
+ "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz",
+ "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz",
+ "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz",
+ "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz",
+ "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz",
+ "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz",
+ "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz",
+ "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz",
+ "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz",
+ "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz",
+ "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz",
+ "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz",
+ "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz",
+ "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz",
+ "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz",
+ "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz",
+ "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@vue/shared/-/shared-3.5.33.tgz",
+ "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
+ "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "12.0.0",
+ "@vueuse/shared": "12.0.0",
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz",
+ "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz",
+ "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
+ "license": "MIT",
+ "dependencies": {
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/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.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
+ "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/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.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.20",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
+ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+ "license": "MIT"
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/element-plus": {
+ "version": "2.13.7",
+ "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.7.tgz",
+ "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^4.2.0",
+ "@element-plus/icons-vue": "^2.3.2",
+ "@floating-ui/dom": "^1.0.1",
+ "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+ "@types/lodash": "^4.17.20",
+ "@types/lodash-es": "^4.17.12",
+ "@vueuse/core": "12.0.0",
+ "async-validator": "^4.2.5",
+ "dayjs": "^1.11.19",
+ "lodash": "^4.17.23",
+ "lodash-es": "^4.17.23",
+ "lodash-unified": "^1.0.3",
+ "memoize-one": "^6.0.0",
+ "normalize-wheel-es": "^1.2.0",
+ "vue-component-type-helpers": "^3.2.4"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/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/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/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/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/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.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-unified": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+ "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/lodash-es": "*",
+ "lodash": "*",
+ "lodash-es": "*"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/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/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/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.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "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/normalize-wheel-es": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/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.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/pinia": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
+ "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.3",
+ "vue-demi": "^0.14.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.4.4",
+ "vue": "^2.7.0 || ^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "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/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz",
+ "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==",
+ "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.3",
+ "@rollup/rollup-android-arm64": "4.60.3",
+ "@rollup/rollup-darwin-arm64": "4.60.3",
+ "@rollup/rollup-darwin-x64": "4.60.3",
+ "@rollup/rollup-freebsd-arm64": "4.60.3",
+ "@rollup/rollup-freebsd-x64": "4.60.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.3",
+ "@rollup/rollup-linux-arm64-musl": "4.60.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.3",
+ "@rollup/rollup-linux-loong64-musl": "4.60.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.3",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.3",
+ "@rollup/rollup-linux-x64-gnu": "4.60.3",
+ "@rollup/rollup-linux-x64-musl": "4.60.3",
+ "@rollup/rollup-openbsd-x64": "4.60.3",
+ "@rollup/rollup-openharmony-arm64": "4.60.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.3",
+ "@rollup/rollup-win32-x64-gnu": "4.60.3",
+ "@rollup/rollup-win32-x64-msvc": "4.60.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/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/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/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/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.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",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/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.npmjs.org/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-component-type-helpers": {
+ "version": "3.2.8",
+ "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.8.tgz",
+ "integrity": "sha512-9689efAXhN/EV86plgkL/XFiJSXhGtWPG6JDboZ+QnjlUWUUQrQ0ILKQtw4iQsuwIwu5k6Aw+JnehDe7161e7A==",
+ "license": "MIT"
+ },
+ "node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/vue-tsc": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/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"
+ }
+ }
+ }
+}
diff --git a/web/frontend/package.json b/web/frontend/package.json
new file mode 100644
index 0000000..5df930c
--- /dev/null
+++ b/web/frontend/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "xiaoaitext-web",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.0",
+ "pinia": "^2.1.0",
+ "axios": "^1.6.0",
+ "element-plus": "^2.5.0",
+ "@element-plus/icons-vue": "^2.3.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.0",
+ "typescript": "^5.3.0",
+ "vite": "^5.1.0",
+ "vue-tsc": "^2.0.0"
+ }
+}
diff --git a/web/frontend/src/App.vue b/web/frontend/src/App.vue
new file mode 100644
index 0000000..4767500
--- /dev/null
+++ b/web/frontend/src/App.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/api/index.ts b/web/frontend/src/api/index.ts
new file mode 100644
index 0000000..424e49e
--- /dev/null
+++ b/web/frontend/src/api/index.ts
@@ -0,0 +1,29 @@
+import axios from 'axios'
+
+const api = axios.create({
+ baseURL: '/api',
+ timeout: 30000,
+})
+
+// Request interceptor: attach JWT token
+api.interceptors.request.use((config) => {
+ const token = localStorage.getItem('token')
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+})
+
+// Response interceptor: handle 401
+api.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response?.status === 401) {
+ localStorage.removeItem('token')
+ window.location.href = '/login'
+ }
+ return Promise.reject(error)
+ }
+)
+
+export default api
diff --git a/web/frontend/src/env.d.ts b/web/frontend/src/env.d.ts
new file mode 100644
index 0000000..185515e
--- /dev/null
+++ b/web/frontend/src/env.d.ts
@@ -0,0 +1,4 @@
+declare module 'element-plus/dist/locale/zh-cn.mjs' {
+ const locale: any
+ export default locale
+}
diff --git a/web/frontend/src/main.ts b/web/frontend/src/main.ts
new file mode 100644
index 0000000..61e07e9
--- /dev/null
+++ b/web/frontend/src/main.ts
@@ -0,0 +1,17 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
+import './styles/global.css'
+
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+app.use(createPinia())
+app.use(router)
+app.use(ElementPlus, { locale: zhCn })
+
+app.mount('#app')
diff --git a/web/frontend/src/router/index.ts b/web/frontend/src/router/index.ts
new file mode 100644
index 0000000..7355f3c
--- /dev/null
+++ b/web/frontend/src/router/index.ts
@@ -0,0 +1,67 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import { useAuthStore } from '../stores/auth'
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes: [
+ {
+ path: '/login',
+ name: 'Login',
+ component: () => import('../views/Login.vue'),
+ meta: { requiresAuth: false },
+ },
+ {
+ path: '/',
+ component: () => import('../views/Layout.vue'),
+ meta: { requiresAuth: true },
+ children: [
+ {
+ path: '',
+ name: 'Dashboard',
+ component: () => import('../views/Dashboard.vue'),
+ },
+ {
+ path: 'memory',
+ name: 'Memory',
+ component: () => import('../views/Memory.vue'),
+ },
+ {
+ path: 'barcodes',
+ name: 'Barcodes',
+ component: () => import('../views/Barcodes.vue'),
+ },
+ {
+ path: 'config',
+ name: 'Config',
+ component: () => import('../views/Config.vue'),
+ },
+ {
+ path: 'sync',
+ name: 'Sync',
+ component: () => import('../views/Sync.vue'),
+ },
+ {
+ path: 'tasks',
+ name: 'Tasks',
+ component: () => import('../views/Tasks.vue'),
+ },
+ {
+ path: 'logs',
+ name: 'Logs',
+ component: () => import('../views/Logs.vue'),
+ },
+ ],
+ },
+ ],
+})
+
+router.beforeEach((to, from, next) => {
+ const authStore = useAuthStore()
+ if (to.meta.requiresAuth !== false && !authStore.isAuthenticated) {
+ next('/login')
+ } else {
+ next()
+ }
+})
+
+export default router
diff --git a/web/frontend/src/stores/auth.ts b/web/frontend/src/stores/auth.ts
new file mode 100644
index 0000000..44737a5
--- /dev/null
+++ b/web/frontend/src/stores/auth.ts
@@ -0,0 +1,34 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import api from '../api'
+
+export const useAuthStore = defineStore('auth', () => {
+ const token = ref(localStorage.getItem('token') || '')
+ const username = ref('')
+
+ const isAuthenticated = computed(() => !!token.value)
+
+ async function login(user: string, password: string) {
+ const res = await api.post('/auth/login', { username: user, password })
+ token.value = res.data.access_token
+ localStorage.setItem('token', token.value)
+ username.value = user
+ }
+
+ function logout() {
+ token.value = ''
+ username.value = ''
+ localStorage.removeItem('token')
+ }
+
+ async function fetchUser() {
+ try {
+ const res = await api.get('/auth/me')
+ username.value = res.data.username
+ } catch {
+ logout()
+ }
+ }
+
+ return { token, username, isAuthenticated, login, logout, fetchUser }
+})
diff --git a/web/frontend/src/stores/processing.ts b/web/frontend/src/stores/processing.ts
new file mode 100644
index 0000000..b13e293
--- /dev/null
+++ b/web/frontend/src/stores/processing.ts
@@ -0,0 +1,94 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import api from '../api'
+
+export interface TaskInfo {
+ task_id: string
+ name: string
+ status: string
+ progress: number
+ message: string
+ result_files: string[]
+ error: string | null
+ log_lines: string[]
+}
+
+export const useProcessingStore = defineStore('processing', () => {
+ const currentTask = ref(null)
+ const tasks = ref([])
+ const logs = ref([])
+
+ let ws: WebSocket | null = null
+
+ function connectWebSocket(taskId: string) {
+ disconnectWebSocket()
+
+ const token = localStorage.getItem('token')
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
+ const host = window.location.host
+ const url = `${protocol}//${host}/ws/task/${taskId}?token=${token}`
+
+ ws = new WebSocket(url)
+
+ ws.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data)
+ currentTask.value = data
+ logs.value = data.log_lines || []
+
+ // Update in tasks list
+ const idx = tasks.value.findIndex(t => t.task_id === data.task_id)
+ if (idx >= 0) {
+ tasks.value[idx] = data
+ } else {
+ tasks.value.unshift(data)
+ }
+
+ // Auto-disconnect on completion
+ if (data.status === 'completed' || data.status === 'failed') {
+ setTimeout(() => disconnectWebSocket(), 2000)
+ }
+ } catch {}
+ }
+
+ ws.onerror = () => {
+ console.error('WebSocket error')
+ }
+
+ ws.onclose = () => {
+ ws = null
+ }
+ }
+
+ function disconnectWebSocket() {
+ if (ws) {
+ ws.close()
+ ws = null
+ }
+ }
+
+ async function startTask(endpoint: string, body?: any) {
+ const res = await api.post(endpoint, body || {})
+ const taskId = res.data.task_id
+ currentTask.value = {
+ task_id: taskId,
+ name: res.data.message || '',
+ status: 'pending',
+ progress: 0,
+ message: '',
+ result_files: [],
+ error: null,
+ log_lines: [],
+ }
+ logs.value = []
+ connectWebSocket(taskId)
+ return taskId
+ }
+
+ async function pollTaskStatus(taskId: string) {
+ const res = await api.get(`/processing/status/${taskId}`)
+ return res.data
+ }
+
+ return { currentTask, tasks, logs, connectWebSocket, disconnectWebSocket, startTask, pollTaskStatus }
+})
diff --git a/web/frontend/src/styles/global.css b/web/frontend/src/styles/global.css
new file mode 100644
index 0000000..9d0a12e
--- /dev/null
+++ b/web/frontend/src/styles/global.css
@@ -0,0 +1,172 @@
+/* ═══════════════════════════════════════════
+ 益选 OCR — Industrial Command Center Theme
+ ═══════════════════════════════════════════ */
+
+@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,500;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap');
+
+:root {
+ /* Core palette */
+ --bg-dark: #0f1117;
+ --bg-sidebar: #161922;
+ --bg-card: #ffffff;
+ --bg-page: #f0f2f5;
+ --bg-hover: #f7f8fa;
+
+ /* Amber accent system */
+ --amber-50: #fff8e1;
+ --amber-100: #ffecb3;
+ --amber-400: #ffca28;
+ --amber-500: #ffc107;
+ --amber-600: #ffb300;
+
+ /* Semantic */
+ --primary: #2563eb;
+ --primary-hover: #1d4ed8;
+ --success: #10b981;
+ --warning: #f59e0b;
+ --danger: #ef4444;
+ --info: #6366f1;
+
+ /* Text */
+ --text-primary: #111827;
+ --text-secondary: #6b7280;
+ --text-muted: #9ca3af;
+ --text-inverse: #e5e7eb;
+ --text-sidebar: #94a3b8;
+ --text-sidebar-active: #ffffff;
+
+ /* Borders & shadows */
+ --border-light: #e5e7eb;
+ --border-subtle: #f3f4f6;
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.06);
+ --shadow-lg: 0 8px 24px rgba(0,0,0,0.08);
+ --shadow-glow: 0 0 20px rgba(255,193,7,0.15);
+
+ /* Typography */
+ --font-sans: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
+ --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
+
+ /* Transitions */
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: var(--font-sans);
+ background: var(--bg-page);
+ color: var(--text-primary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* Element Plus overrides */
+.el-card {
+ border: 1px solid var(--border-light) !important;
+ border-radius: 12px !important;
+ box-shadow: var(--shadow-sm) !important;
+ transition: box-shadow 0.2s var(--ease-out) !important;
+}
+
+.el-card:hover {
+ box-shadow: var(--shadow-md) !important;
+}
+
+.el-button--primary {
+ background: var(--primary) !important;
+ border-color: var(--primary) !important;
+ font-weight: 500;
+ border-radius: 8px;
+ transition: all 0.2s var(--ease-out);
+}
+
+.el-button--primary:hover {
+ background: var(--primary-hover) !important;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(37,99,235,0.3);
+}
+
+.el-table {
+ --el-table-border-color: var(--border-light);
+ --el-table-header-bg-color: #fafbfc;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.el-table th.el-table__cell {
+ font-weight: 600;
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--text-secondary);
+}
+
+.el-progress-bar__outer {
+ border-radius: 6px;
+}
+
+.el-progress-bar__inner {
+ border-radius: 6px;
+ transition: width 0.4s var(--ease-out);
+}
+
+.el-tag {
+ border-radius: 6px;
+ font-weight: 500;
+}
+
+/* Scrollbar */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #d1d5db;
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #9ca3af;
+}
+
+/* Animations */
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(12px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes shimmer {
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
+}
+
+@keyframes pulse-glow {
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(255,193,7,0); }
+ 50% { box-shadow: 0 0 16px 4px rgba(255,193,7,0.15); }
+}
+
+.animate-in {
+ animation: fadeInUp 0.4s var(--ease-out) both;
+}
+
+.animate-in-delay-1 { animation-delay: 0.05s; }
+.animate-in-delay-2 { animation-delay: 0.1s; }
+.animate-in-delay-3 { animation-delay: 0.15s; }
+.animate-in-delay-4 { animation-delay: 0.2s; }
diff --git a/web/frontend/src/views/Barcodes.vue b/web/frontend/src/views/Barcodes.vue
new file mode 100644
index 0000000..3bad38f
--- /dev/null
+++ b/web/frontend/src/views/Barcodes.vue
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+
+
+ {{ items.length }}
+ 映射规则
+
+
+
+
+
+
+
+
条码映射管理
+
+
+
+
+
+
+ 刷新
+ 新增映射
+
+
+
+
+
+
+ {{ row.barcode }}
+
+
+
+
+
+
+
+
+
+ {{ row.target }}
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/views/Config.vue b/web/frontend/src/views/Config.vue
new file mode 100644
index 0000000..e545e6a
--- /dev/null
+++ b/web/frontend/src/views/Config.vue
@@ -0,0 +1,245 @@
+
+
+
+
+
+
系统配置
+
+ 保存所有修改
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/views/Dashboard.vue b/web/frontend/src/views/Dashboard.vue
new file mode 100644
index 0000000..09e5679
--- /dev/null
+++ b/web/frontend/src/views/Dashboard.vue
@@ -0,0 +1,719 @@
+
+
+
+
+
+
+
+
+
+ {{ stat.value }}
+ {{ stat.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
拖拽文件到此处或 点击选择
+
支持 JPG / PNG / BMP / XLS / XLSX
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ f.name }}
+
{{ fmtSize(f.size) }}
+
+
+
+
+
+
+
+
+
+
+
处理操作
+
+
+
+
+
+
+
+
+
+
+
+
+
处理结果
+ {{ resultFiles.length }} 个文件
+
+
+
+
+
+
+
{{ f.name }}
+
{{ fmtSize(f.size) }}
+
+ 下载
+
+
+
+
+
+
+
+
+
+
+
+
处理进度
+
+ {{ statusText }}
+
+
+
+
+
+
+
{{ currentTask.progress }}%
+
+
{{ currentTask.message }}
+
+
+
+
+
+
+
+
处理日志
+ 清空
+
+
+
+
+ {{ fmtTime(i) }}
+ {{ line }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/views/Layout.vue b/web/frontend/src/views/Layout.vue
new file mode 100644
index 0000000..031a116
--- /dev/null
+++ b/web/frontend/src/views/Layout.vue
@@ -0,0 +1,379 @@
+
+
+
+
+
+
+
+
+
{{ pageTitle }}
+
+
+
+
+
{{ (authStore.username || 'U')[0].toUpperCase() }}
+
{{ authStore.username || '用户' }}
+
+
+
+
+
+ 修改密码
+
+
+ 退出登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认修改
+
+
+
+
+
+
+
diff --git a/web/frontend/src/views/Login.vue b/web/frontend/src/views/Login.vue
new file mode 100644
index 0000000..9d5eeb8
--- /dev/null
+++ b/web/frontend/src/views/Login.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ loading ? '登录中...' : '登 录' }}
+
+
+
+
+
+ 默认账号 admin / admin123
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/views/Logs.vue b/web/frontend/src/views/Logs.vue
new file mode 100644
index 0000000..efacd5a
--- /dev/null
+++ b/web/frontend/src/views/Logs.vue
@@ -0,0 +1,302 @@
+
+
+
+
+
+
+
+
+
+ {{ logStats.today_count }}
+ 今日请求
+
+
+
+
+
+
+
+ {{ logStats.error_count }}
+ 错误数
+
+
+
+
+
+
+
+ {{ logStats.avg_duration_ms }}ms
+ 平均耗时
+
+
+
+
+
+
+
+ {{ logStats.error_rate }}%
+ 错误率
+
+
+
+
+
+
+
+
请求日志
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 刷新
+
+
+
+
+
+
+ {{ formatTime(row.timestamp) }}
+
+
+
+
+ {{ row.method }}
+
+
+
+
+
+ {{ row.status_code }}
+
+
+
+
+ {{ row.duration_ms?.toFixed(0) }}ms
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/views/Memory.vue b/web/frontend/src/views/Memory.vue
new file mode 100644
index 0000000..fc5d7a8
--- /dev/null
+++ b/web/frontend/src/views/Memory.vue
@@ -0,0 +1,380 @@
+
+
+
+
+
+
+
+
+
+ {{ total }}
+ 总记录数
+
+
+
+
+
+
+
+ {{ highConfidence }}
+ 高置信度
+
+
+
+
+
+
+
+ {{ lowConfidence }}
+ 低置信度
+
+
+
+
+
+
+
+
商品记忆库
+
+
+
+
+
+
+ 刷新
+ 重新导入
+
+
+
+
+
+
+ {{ row.barcode }}
+
+
+
+
+
+
+
+ {{ row.price != null ? row.price.toFixed(4) : '-' }}
+
+
+
+
+
+ {{ row.confidence }}
+
+
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/views/Sync.vue b/web/frontend/src/views/Sync.vue
new file mode 100644
index 0000000..236f51f
--- /dev/null
+++ b/web/frontend/src/views/Sync.vue
@@ -0,0 +1,371 @@
+
+
+
+
+
+
云端同步
+ 刷新状态
+
+
+
+
+
+
+
+
+ 已连接
+ {{ syncStatus.repo_url }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ currentTask.message }}
+
+
+
+
+
+
+
+
+
云端同步未启用
+
请在系统配置页面设置 Gitea 相关参数以启用同步功能。
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/views/Tasks.vue b/web/frontend/src/views/Tasks.vue
new file mode 100644
index 0000000..7559170
--- /dev/null
+++ b/web/frontend/src/views/Tasks.vue
@@ -0,0 +1,432 @@
+
+
+
+
+
+
+
+
+
+ {{ taskStats.total }}
+ 总任务数
+
+
+
+
+
+
+
+ {{ taskStats.completed }}
+ 成功
+
+
+
+
+
+
+
+ {{ taskStats.failed }}
+ 失败
+
+
+
+
+
+
+
+ {{ taskStats.running }}
+ 运行中
+
+
+
+
+
+
+
+
任务历史
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 刷新
+
+
+
+
+
+
+ {{ row.id }}
+
+
+
+
+
+ {{ statusLabel(row.status) }}
+
+
+
+
+
+
+
+
+
+
+ {{ formatTime(row.created_at) }}
+
+
+
+
+ 详情
+ 重试
+
+
+
+
+
+
+
+
+
+
+
+ 关闭
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/vite-env.d.ts b/web/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..323c78a
--- /dev/null
+++ b/web/frontend/src/vite-env.d.ts
@@ -0,0 +1,7 @@
+///
+
+declare module '*.vue' {
+ import type { DefineComponent } from 'vue'
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/web/frontend/tsconfig.json b/web/frontend/tsconfig.json
new file mode 100644
index 0000000..072649b
--- /dev/null
+++ b/web/frontend/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "preserve",
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/web/frontend/tsconfig.node.json b/web/frontend/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/web/frontend/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/web/frontend/vite.config.ts b/web/frontend/vite.config.ts
new file mode 100644
index 0000000..f325c96
--- /dev/null
+++ b/web/frontend/vite.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ plugins: [vue()],
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8000',
+ changeOrigin: true,
+ },
+ '/ws': {
+ target: 'ws://localhost:8000',
+ ws: true,
+ },
+ },
+ },
+ build: {
+ outDir: '../backend/static',
+ emptyOutDir: true,
+ },
+})
diff --git a/web/requirements-web.txt b/web/requirements-web.txt
new file mode 100644
index 0000000..3235396
--- /dev/null
+++ b/web/requirements-web.txt
@@ -0,0 +1,9 @@
+fastapi>=0.109.0
+uvicorn[standard]>=0.27.0
+python-jose[cryptography]>=3.3.0
+bcrypt>=4.1.0
+python-multipart>=0.0.6
+aiofiles>=23.2.0
+websockets>=12.0
+pydantic>=2.5.0
+werkzeug>=3.0.0