From 8b55b9cf550439c858a51e2860dda1cbf7911374 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 10 Oct 2026 22:43:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E4=B8=8A=E7=99=BB=E5=BD=95=E5=92=8C?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90=EF=BC=8C=E5=B9=B6=E7=94=A8?= =?UTF-8?q?=20Vue=20=E6=8E=A7=E5=88=B6=E5=8F=B0=E5=8C=BA=E5=88=86=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=B8=8E=E7=AE=A1=E7=90=86=E5=91=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .gitignore | 1 + app/api.py | 233 +++-- app/config.py | 5 + app/deps.py | 36 + app/main.py | 48 +- app/models.py | 17 + app/schema_upgrade.py | 35 + app/schemas.py | 59 +- app/security.py | 87 ++ app/services/auth.py | 103 +++ app/services/portal.py | 90 ++ app/services/registry.py | 22 +- deploy/manage.sh | 15 + frontend/index.html | 15 + frontend/package-lock.json | 1285 ++++++++++++++++++++++++++ frontend/package.json | 17 + frontend/src/App.vue | 3 + frontend/src/api.js | 45 + frontend/src/components/Shell.vue | 79 ++ frontend/src/format.js | 36 + frontend/src/main.js | 6 + frontend/src/router/index.js | 23 + frontend/src/styles.css | 182 ++++ frontend/src/views/AdminOverview.vue | 170 ++++ frontend/src/views/Dashboard.vue | 133 +++ frontend/src/views/Login.vue | 102 ++ frontend/vite.config.js | 16 + requirements.txt | 1 + tests/test_core_flow.py | 83 +- web/app.js | 498 ---------- web/assets/index-CZ0L8T-X.js | 26 + web/assets/index-mWM9aOGS.css | 1 + web/index.html | 76 +- web/styles.css | 359 ------- 说明.md | 45 +- 部署.md | 2 +- 36 files changed, 2947 insertions(+), 1007 deletions(-) create mode 100644 app/deps.py create mode 100644 app/schema_upgrade.py create mode 100644 app/security.py create mode 100644 app/services/auth.py create mode 100644 app/services/portal.py create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api.js create mode 100644 frontend/src/components/Shell.vue create mode 100644 frontend/src/format.js create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/styles.css create mode 100644 frontend/src/views/AdminOverview.vue create mode 100644 frontend/src/views/Dashboard.vue create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/vite.config.js delete mode 100644 web/app.js create mode 100644 web/assets/index-CZ0L8T-X.js create mode 100644 web/assets/index-mWM9aOGS.css delete mode 100644 web/styles.css diff --git a/.gitignore b/.gitignore index 4153ad9..9e314cb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ venv/ .env *.db .pytest_cache/ +node_modules/ diff --git a/app/api.py b/app/api.py index 5ce6d17..10e6e80 100644 --- a/app/api.py +++ b/app/api.py @@ -5,25 +5,34 @@ from decimal import Decimal from typing import Annotated from fastapi import APIRouter, Depends -from sqlalchemy import func, select +from sqlalchemy import case, func, select from sqlalchemy.orm import Session, selectinload from app.clock import utcnow from app.database import get_db -from app.models import ComputeNode, ComputeTask, NodeStatus, NodeYield, User, WithdrawalLog, WithdrawalStatus, YieldStatus -from app.money import money, to_decimal +from app.deps import AdminUser, CurrentUser +from app.errors import AppError +from app.models import ComputeNode, ComputeTask, User, UserRole, WithdrawalLog, WithdrawalStatus +from app.money import money from app.schemas import ( + AdminUserOut, BalanceOut, ContributionOut, + DashboardOut, HeartbeatIn, HeartbeatOut, + LoginIn, + MeNodeCreate, + MeWithdrawalCreate, NodeCreate, NodeOut, NodeView, + RegisterIn, SettleOut, SummaryOut, TaskOut, TaskReportIn, + TokenOut, UserCreate, UserOut, WithdrawalConfirm, @@ -33,8 +42,18 @@ from app.schemas import ( WithdrawalView, YieldOut, ) +from app.security import create_access_token +from app.services.auth import authenticate, register_account from app.services.common import get_node_by_sn, get_user_by_wallet from app.services.heartbeat import record_heartbeat +from app.services.portal import ( + authorized_amount, + live_nodes, + node_counts, + pending_withdrawal_amount, + settled_points, + user_rows, +) from app.services.registry import register_node, register_user from app.services.tasks import list_pending_tasks, report_task from app.services.withdrawals import available_balance, confirm_withdrawal, reject_withdrawal, request_withdrawal @@ -44,35 +63,90 @@ router = APIRouter(prefix="/api/v1") Db = Annotated[Session, Depends(get_db)] +@router.post("/auth/register", response_model=TokenOut) +def register(body: RegisterIn, db: Db) -> TokenOut: + user = register_account(db, body.wallet_address, body.username, body.password) + return _token(user) + + +@router.post("/auth/login", response_model=TokenOut) +def login(body: LoginIn, db: Db) -> TokenOut: + user = authenticate(db, body.wallet_address, body.username, body.password) + return _token(user) + + +@router.get("/me", response_model=UserOut) +def me(user: CurrentUser) -> User: + return user + + +@router.get("/me/dashboard", response_model=DashboardOut) +def my_dashboard(user: CurrentUser, db: Db) -> DashboardOut: + nodes = live_nodes(db, user.id) + withdrawals = db.scalars( + select(WithdrawalLog).where(WithdrawalLog.user_id == user.id).order_by(WithdrawalLog.id.desc()).limit(20) + ).all() + today = money(sum((item["today_points"] for item in nodes), Decimal("0"))) + return DashboardOut( + wallet_address=user.wallet_address, + username=user.username, + role=user.role, + available_points=available_balance(db, user.id), + settled_points=settled_points(db, user.id), + today_points=today, + nodes=[NodeView.model_validate(item) for item in nodes], + withdrawals=[WithdrawalOut.model_validate(item) for item in withdrawals], + ) + + +@router.post("/me/nodes", response_model=NodeOut) +def create_my_node(body: MeNodeCreate, user: CurrentUser, db: Db) -> NodeOut: + return register_node(db, user.wallet_address, body.sn, body.purchase_amount, body.callback_url) + + +@router.post("/me/withdrawals", response_model=WithdrawalOut) +def create_my_withdrawal(body: MeWithdrawalCreate, user: CurrentUser, db: Db) -> WithdrawalOut: + row, remaining = request_withdrawal(db, user.wallet_address, body.amount, body.asset_symbol) + return WithdrawalOut.model_validate(row).model_copy(update={"available_after": remaining}) + + +@router.get("/admin/summary", response_model=SummaryOut) +def admin_summary(_: AdminUser, db: Db) -> SummaryOut: + return _summary(db) + + +@router.get("/admin/users", response_model=list[AdminUserOut]) +def admin_users(_: AdminUser, db: Db) -> list[dict]: + return user_rows(db) + + +@router.get("/admin/nodes", response_model=list[NodeView]) +def admin_nodes(_: AdminUser, db: Db) -> list[NodeView]: + return [NodeView.model_validate(item) for item in live_nodes(db)] + + +@router.get("/admin/withdrawals", response_model=list[WithdrawalView]) +def admin_withdrawals(_: AdminUser, db: Db) -> list[WithdrawalView]: + return [_withdrawal_view(row) for row in _withdrawal_rows(db)] + + +@router.post("/admin/withdrawals/{withdrawal_id}/confirm", response_model=WithdrawalOut) +def admin_confirm(withdrawal_id: int, body: WithdrawalConfirm, _: AdminUser, db: Db) -> WithdrawalOut: + return confirm_withdrawal(db, withdrawal_id, body.tx_hash) + + +@router.post("/admin/withdrawals/{withdrawal_id}/reject", response_model=WithdrawalOut) +def admin_reject(withdrawal_id: int, body: WithdrawalReject, _: AdminUser, db: Db) -> WithdrawalOut: + return reject_withdrawal(db, withdrawal_id, body.note) + + @router.get("/summary", response_model=SummaryOut) -def summary(db: Db) -> SummaryOut: - users = db.scalar(select(func.count()).select_from(User)) or 0 - nodes = db.scalar(select(func.count()).select_from(ComputeNode)) or 0 - online = db.scalar( - select(func.count()).select_from(ComputeNode).where(ComputeNode.status == NodeStatus.ONLINE) - ) or 0 - authorized = _sum(db.scalars(select(ComputeNode.purchase_amount)).all()) - settled = _sum( - db.scalars(select(NodeYield.points_delta).where(NodeYield.status == YieldStatus.SETTLED)).all() - ) - pending = _sum( - db.scalars( - select(WithdrawalLog.amount).where(WithdrawalLog.status == WithdrawalStatus.PENDING) - ).all() - ) - return SummaryOut( - users=users, - nodes=nodes, - online=online, - offline=nodes - online, - authorized_amount=authorized, - settled_points=settled, - pending_withdrawals=pending, - ) +def summary(_: AdminUser, db: Db) -> SummaryOut: + return _summary(db) @router.get("/users", response_model=list[UserOut]) -def list_users(db: Db) -> list[User]: +def list_users(_: AdminUser, db: Db) -> list[User]: return list(db.scalars(select(User).order_by(User.id.desc())).all()) @@ -82,21 +156,19 @@ def create_user(body: UserCreate, db: Db) -> UserOut: @router.get("/nodes", response_model=list[NodeView]) -def list_nodes(db: Db) -> list[NodeView]: - rows = db.scalars( - select(ComputeNode).options(selectinload(ComputeNode.user)).order_by(ComputeNode.id.desc()) - ).all() - return [_node_view(row) for row in rows] +def list_nodes(_: AdminUser, db: Db) -> list[NodeView]: + return [NodeView.model_validate(item) for item in live_nodes(db)] @router.post("/nodes", response_model=NodeOut) -def create_node(body: NodeCreate, db: Db) -> NodeOut: +def create_node(body: NodeCreate, user: CurrentUser, db: Db) -> NodeOut: + _must_own_wallet(user, body.wallet_address) return register_node(db, body.wallet_address, body.sn, body.purchase_amount, body.callback_url) @router.get("/nodes/{sn}", response_model=NodeOut) -def get_node(sn: str, db: Db) -> NodeOut: - return get_node_by_sn(db, sn) +def get_node(sn: str, user: CurrentUser, db: Db) -> NodeOut: + return _owned_node(db, user, sn) @router.post("/node/heartbeat", response_model=HeartbeatOut) @@ -121,14 +193,14 @@ def report(body: TaskReportIn, db: Db) -> TaskOut: @router.get("/nodes/{sn}/contribution", response_model=ContributionOut) -def contribution(sn: str, yield_date: date, db: Db) -> ContributionOut: - node = get_node_by_sn(db, sn) +def contribution(sn: str, yield_date: date, user: CurrentUser, db: Db) -> ContributionOut: + node = _owned_node(db, user, sn) computed = compute_node_yield(db, node, yield_date) return ContributionOut(sn=node.sn, **computed.__dict__) @router.post("/yield/settle", response_model=SettleOut) -def settle(db: Db, yield_date: date | None = None) -> SettleOut: +def settle(_: AdminUser, db: Db, yield_date: date | None = None) -> SettleOut: target = yield_date or (utcnow().date() - timedelta(days=1)) summary = settle_yield_date(db, target) return SettleOut( @@ -141,52 +213,91 @@ def settle(db: Db, yield_date: date | None = None) -> SettleOut: @router.get("/nodes/{sn}/yields", response_model=list[YieldOut]) -def yields(sn: str, db: Db) -> list[YieldOut]: +def yields(sn: str, user: CurrentUser, db: Db) -> list[YieldOut]: + _owned_node(db, user, sn) return list_node_yields(db, sn) @router.get("/withdrawals", response_model=list[WithdrawalView]) -def list_withdrawals(db: Db) -> list[WithdrawalView]: - rows = db.scalars( - select(WithdrawalLog).options(selectinload(WithdrawalLog.user)).order_by(WithdrawalLog.id.desc()) - ).all() - return [ - WithdrawalView( - **WithdrawalOut.model_validate(row).model_dump(), - wallet_address=row.user.wallet_address, - ) - for row in rows - ] +def list_withdrawals(_: AdminUser, db: Db) -> list[WithdrawalView]: + return [_withdrawal_view(row) for row in _withdrawal_rows(db)] @router.get("/users/{wallet_address}/balance", response_model=BalanceOut) -def balance(wallet_address: str, db: Db) -> BalanceOut: - user = get_user_by_wallet(db, wallet_address) - return BalanceOut(wallet_address=user.wallet_address, available_points=available_balance(db, user.id)) +def balance(wallet_address: str, user: CurrentUser, db: Db) -> BalanceOut: + _must_own_wallet(user, wallet_address) + owner = get_user_by_wallet(db, wallet_address) + return BalanceOut(wallet_address=owner.wallet_address, available_points=available_balance(db, owner.id)) @router.post("/withdrawals", response_model=WithdrawalOut) -def create_withdrawal(body: WithdrawalCreate, db: Db) -> WithdrawalOut: +def create_withdrawal(body: WithdrawalCreate, user: CurrentUser, db: Db) -> WithdrawalOut: + _must_own_wallet(user, body.wallet_address) row, remaining = request_withdrawal(db, body.wallet_address, body.amount, body.asset_symbol) return WithdrawalOut.model_validate(row).model_copy(update={"available_after": remaining}) @router.post("/withdrawals/{withdrawal_id}/confirm", response_model=WithdrawalOut) -def confirm(withdrawal_id: int, body: WithdrawalConfirm, db: Db) -> WithdrawalOut: +def confirm(withdrawal_id: int, body: WithdrawalConfirm, _: AdminUser, db: Db) -> WithdrawalOut: return confirm_withdrawal(db, withdrawal_id, body.tx_hash) @router.post("/withdrawals/{withdrawal_id}/reject", response_model=WithdrawalOut) -def reject(withdrawal_id: int, body: WithdrawalReject, db: Db) -> WithdrawalOut: +def reject(withdrawal_id: int, body: WithdrawalReject, _: AdminUser, db: Db) -> WithdrawalOut: return reject_withdrawal(db, withdrawal_id, body.note) -def _node_view(node: ComputeNode) -> NodeView: - return NodeView(**NodeOut.model_validate(node).model_dump(), wallet_address=node.user.wallet_address) +def _summary(db: Session) -> SummaryOut: + users = db.scalar(select(func.count()).select_from(User)) or 0 + nodes, online = node_counts(db) + return SummaryOut( + users=users, + nodes=nodes, + online=online, + offline=nodes - online, + authorized_amount=authorized_amount(db), + settled_points=settled_points(db), + pending_withdrawals=pending_withdrawal_amount(db), + ) -def _sum(values: list[object]) -> Decimal: - return money(sum((to_decimal(item) for item in values), Decimal("0"))) +def _token(user: User) -> TokenOut: + return TokenOut( + access_token=create_access_token(user), + role=user.role, + wallet_address=user.wallet_address, + username=user.username, + ) + + +def _must_own_wallet(user: User, wallet_address: str) -> None: + if user.role != UserRole.ADMIN and user.wallet_address != wallet_address.strip(): + raise AppError("只能操作自己的钱包", 403) + + +def _owned_node(db: Session, user: User, sn: str) -> ComputeNode: + node = get_node_by_sn(db, sn) + if user.role != UserRole.ADMIN and node.user_id != user.id: + raise AppError("无权查看该节点", 403) + return node + + +def _withdrawal_rows(db: Session) -> list[WithdrawalLog]: + pending_first = case((WithdrawalLog.status == WithdrawalStatus.PENDING, 0), else_=1) + return list( + db.scalars( + select(WithdrawalLog) + .options(selectinload(WithdrawalLog.user)) + .order_by(pending_first, WithdrawalLog.id.desc()) + ).all() + ) + + +def _withdrawal_view(row: WithdrawalLog) -> WithdrawalView: + return WithdrawalView( + **WithdrawalOut.model_validate(row).model_dump(), + wallet_address=row.user.wallet_address, + ) def _task_out(task: ComputeTask) -> TaskOut: diff --git a/app/config.py b/app/config.py index 4bfd59a..0d5b042 100644 --- a/app/config.py +++ b/app/config.py @@ -24,6 +24,11 @@ class Settings(BaseSettings): yield_minute: int = 5 license_months: int = 12 disable_scheduler: bool = False + jwt_secret: str = "" + jwt_expire_minutes: int = 60 * 24 + admin_username: str = "admin" + admin_password: str = "" + admin_wallet: str = "admin-root" model_config = SettingsConfigDict(env_file=".env", extra="ignore") diff --git a/app/deps.py b/app/deps.py new file mode 100644 index 0000000..bd8e645 --- /dev/null +++ b/app/deps.py @@ -0,0 +1,36 @@ +"""从 Authorization 头解析当前用户,并区分管理员。""" + +from typing import Annotated + +from fastapi import Depends +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from sqlalchemy.orm import Session + +from app.database import get_db +from app.errors import AppError +from app.models import User, UserRole +from app.security import read_user_id + +_bearer = HTTPBearer(auto_error=False) + + +def get_current_user( + credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)], + db: Annotated[Session, Depends(get_db)], +) -> User: + if credentials is None or credentials.scheme.lower() != "bearer": + raise AppError("未登录", 401) + user = db.get(User, read_user_id(credentials.credentials)) + if user is None: + raise AppError("登录已失效", 401) + return user + + +def require_admin(user: Annotated[User, Depends(get_current_user)]) -> User: + if user.role != UserRole.ADMIN: + raise AppError("需要管理员权限", 403) + return user + + +CurrentUser = Annotated[User, Depends(get_current_user)] +AdminUser = Annotated[User, Depends(require_admin)] diff --git a/app/main.py b/app/main.py index dc91df6..8c5dbf1 100644 --- a/app/main.py +++ b/app/main.py @@ -12,9 +12,12 @@ from fastapi.staticfiles import StaticFiles import app.models # noqa: F401 注册全部表 from app.api import router -from app.database import Base, engine +from app.database import Base, engine, session_scope from app.errors import AppError from app.scheduler import start_scheduler, stop_scheduler +from app.schema_upgrade import upgrade_schema +from app.security import ensure_jwt_secret +from app.services.auth import ensure_admin logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") @@ -22,6 +25,10 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name @asynccontextmanager async def lifespan(_: FastAPI): Base.metadata.create_all(bind=engine) + upgrade_schema() + with session_scope() as session: + ensure_jwt_secret(session) + ensure_admin(session) start_scheduler() try: yield @@ -33,19 +40,36 @@ WEB_DIR = Path(__file__).resolve().parent.parent / "web" app = FastAPI(title="HyperNode-AI", version="1.0.0", lifespan=lifespan) app.include_router(router) -app.mount("/static", StaticFiles(directory=WEB_DIR), name="static") - - -@app.get("/") -def console() -> FileResponse: - return FileResponse(WEB_DIR / "index.html") - - -@app.exception_handler(AppError) -async def handle_app_error(_: Request, exc: AppError) -> JSONResponse: - return JSONResponse(status_code=exc.status_code, content={"detail": exc.message}) +_assets = WEB_DIR / "assets" +if _assets.is_dir(): + app.mount("/assets", StaticFiles(directory=_assets), name="assets") @app.get("/health") def health() -> dict[str, str]: return {"status": "ok"} + + +@app.get("/") +def console() -> FileResponse: + return _spa("") + + +@app.get("/{full_path:path}") +def console_routes(full_path: str) -> FileResponse: + return _spa(full_path) + + +def _spa(full_path: str) -> FileResponse: + root = WEB_DIR.resolve() + index = root / "index.html" + if full_path: + candidate = (root / full_path).resolve() + if candidate.is_file() and (candidate == root or root in candidate.parents): + return FileResponse(candidate) + return FileResponse(index) + + +@app.exception_handler(AppError) +async def handle_app_error(_: Request, exc: AppError) -> JSONResponse: + return JSONResponse(status_code=exc.status_code, content={"detail": exc.message}) diff --git a/app/models.py b/app/models.py index dc0220a..6bef6dd 100644 --- a/app/models.py +++ b/app/models.py @@ -98,6 +98,11 @@ class PushStatus(str, enum.Enum): FAILED = "failed" +class UserRole(str, enum.Enum): + USER = "user" + ADMIN = "admin" + + class User(Base): """用户。一个钱包地址对应一名用户,可绑定多台算力节点。""" @@ -105,6 +110,9 @@ class User(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) wallet_address: Mapped[str] = mapped_column(String(128), unique=True, index=True) + username: Mapped[str | None] = mapped_column(String(32), unique=True, index=True) + password_hash: Mapped[str | None] = mapped_column(String(255)) + role: Mapped[UserRole] = mapped_column(_enum(UserRole), default=UserRole.USER, index=True) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=utcnow) nodes: Mapped[list[ComputeNode]] = relationship(back_populates="user") @@ -241,6 +249,15 @@ class WithdrawalLog(Base): user: Mapped[User] = relationship(back_populates="withdrawals") +class AppSetting(Base): + """进程重启后仍要保留的运行时密钥,例如未写入环境变量的 JWT 密钥。""" + + __tablename__ = "app_settings" + + key: Mapped[str] = mapped_column(String(64), primary_key=True) + value: Mapped[str] = mapped_column(Text) + + class SchedulerLock(Base): """定时任务锁。锁到期后其他实例才能接手,进程崩溃不会永久占住任务。""" diff --git a/app/schema_upgrade.py b/app/schema_upgrade.py new file mode 100644 index 0000000..8587011 --- /dev/null +++ b/app/schema_upgrade.py @@ -0,0 +1,35 @@ +"""给已经上线的库补上认证字段。create_all 不会修改旧表。""" + +from sqlalchemy import inspect, text + +from app.database import engine + + +def upgrade_schema() -> None: + inspector = inspect(engine) + if "users" not in inspector.get_table_names(): + return + columns = {column["name"] for column in inspector.get_columns("users")} + statements: list[str] = [] + if "username" not in columns: + statements.append("ALTER TABLE users ADD COLUMN username VARCHAR(32)") + if "password_hash" not in columns: + statements.append("ALTER TABLE users ADD COLUMN password_hash VARCHAR(255)") + if "role" not in columns: + statements.append("ALTER TABLE users ADD COLUMN role VARCHAR(16) DEFAULT 'user' NOT NULL") + if not statements and _has_username_index(inspector): + return + with engine.begin() as conn: + for statement in statements: + conn.execute(text(statement)) + if not _has_username_index(inspector): + conn.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS uq_users_username ON users (username)")) + + +def _has_username_index(inspector) -> bool: + if "users" not in inspector.get_table_names(): + return False + for index in inspector.get_indexes("users"): + if index.get("column_names") == ["username"] and index.get("unique"): + return True + return False diff --git a/app/schemas.py b/app/schemas.py index 67fb7bc..f0c9caf 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -8,10 +8,11 @@ from typing import Annotated from pydantic import BaseModel, ConfigDict, Field, PlainSerializer -from app.models import NodeStatus, TaskStatus, WithdrawalStatus, YieldStatus +from app.models import NodeStatus, TaskStatus, UserRole, WithdrawalStatus, YieldStatus SN = Annotated[str, Field(min_length=4, max_length=64, pattern=r"^[A-Za-z0-9_-]+$")] WALLET = Annotated[str, Field(min_length=4, max_length=128, pattern=r"^[A-Za-z0-9:_-]+$")] +USERNAME = Annotated[str, Field(min_length=3, max_length=32, pattern=r"^[A-Za-z0-9_]+$")] MoneyText = Annotated[Decimal, PlainSerializer(lambda value: format(value, "f"), return_type=str)] @@ -26,9 +27,41 @@ class UserCreate(APIModel): class UserOut(APIModel): id: int wallet_address: str + username: str | None = None + role: UserRole = UserRole.USER created_at: datetime +class RegisterIn(APIModel): + wallet_address: WALLET + username: USERNAME | None = None + password: str | None = Field(default=None, max_length=72) + + +class LoginIn(APIModel): + wallet_address: str | None = Field(default=None, max_length=128) + username: str | None = Field(default=None, max_length=32) + password: str | None = Field(default=None, max_length=72) + + +class TokenOut(APIModel): + access_token: str + token_type: str = "bearer" + role: UserRole + wallet_address: str + username: str | None = None + + +class AdminUserOut(APIModel): + id: int + wallet_address: str + username: str | None + role: UserRole + created_at: datetime + node_count: int + available_points: MoneyText + + class NodeCreate(APIModel): wallet_address: WALLET sn: SN @@ -51,6 +84,8 @@ class NodeOut(APIModel): class NodeView(NodeOut): wallet_address: str + online_rate: MoneyText = Decimal("0") + today_points: MoneyText = Decimal("0") class HeartbeatIn(APIModel): @@ -134,6 +169,17 @@ class WithdrawalCreate(APIModel): asset_symbol: str = Field(default="POINT", min_length=1, max_length=32) +class MeWithdrawalCreate(APIModel): + amount: Decimal = Field(gt=0, max_digits=24, decimal_places=8) + asset_symbol: str = Field(default="POINT", min_length=1, max_length=32) + + +class MeNodeCreate(APIModel): + sn: SN + purchase_amount: Decimal = Field(gt=0, max_digits=24, decimal_places=8) + callback_url: str | None = None + + class WithdrawalConfirm(APIModel): tx_hash: str = Field(min_length=6, max_length=128, pattern=r"^[A-Za-z0-9]+$") @@ -172,3 +218,14 @@ class SummaryOut(APIModel): authorized_amount: MoneyText settled_points: MoneyText pending_withdrawals: MoneyText + + +class DashboardOut(APIModel): + wallet_address: str + username: str | None + role: UserRole + available_points: MoneyText + settled_points: MoneyText + today_points: MoneyText + nodes: list[NodeView] + withdrawals: list[WithdrawalOut] diff --git a/app/security.py b/app/security.py new file mode 100644 index 0000000..862b853 --- /dev/null +++ b/app/security.py @@ -0,0 +1,87 @@ +"""密码哈希与 JWT。密钥优先用环境变量,否则写入 app_settings,重启后仍然有效。""" + +from __future__ import annotations + +import hashlib +import hmac +import secrets +from base64 import urlsafe_b64decode, urlsafe_b64encode +from datetime import timedelta + +import jwt +from sqlalchemy.orm import Session + +from app.clock import utcnow +from app.config import settings +from app.errors import AppError +from app.models import AppSetting, User + +_secret: str | None = None +_ITERATIONS = 200_000 + + +def hash_password(password: str) -> str: + salt = secrets.token_bytes(16) + digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, _ITERATIONS) + return f"pbkdf2_sha256${_ITERATIONS}${_b64(salt)}${_b64(digest)}" + + +def verify_password(password: str, stored: str | None) -> bool: + if not stored: + return False + try: + algo, rounds, salt_b64, digest_b64 = stored.split("$", 3) + if algo != "pbkdf2_sha256": + return False + got = hashlib.pbkdf2_hmac("sha256", password.encode(), _unb64(salt_b64), int(rounds)) + return hmac.compare_digest(got, _unb64(digest_b64)) + except (ValueError, TypeError): + return False + + +def ensure_jwt_secret(session: Session) -> None: + global _secret + if settings.jwt_secret: + _secret = settings.jwt_secret + return + row = session.get(AppSetting, "jwt_secret") + if row is None: + row = AppSetting(key="jwt_secret", value=secrets.token_hex(32)) + session.add(row) + session.flush() + _secret = row.value + + +def create_access_token(user: User) -> str: + payload = { + "sub": str(user.id), + "role": user.role.value, + "exp": utcnow() + timedelta(minutes=settings.jwt_expire_minutes), + } + return jwt.encode(payload, _require_secret(), algorithm="HS256") + + +def read_user_id(token: str) -> int: + try: + payload = jwt.decode(token, _require_secret(), algorithms=["HS256"]) + except jwt.PyJWTError as exc: + raise AppError("登录已失效", 401) from exc + subject = payload.get("sub") + if not subject: + raise AppError("登录已失效", 401) + return int(subject) + + +def _require_secret() -> str: + if not _secret: + raise AppError("认证尚未初始化", 500) + return _secret + + +def _b64(raw: bytes) -> str: + return urlsafe_b64encode(raw).decode().rstrip("=") + + +def _unb64(text: str) -> bytes: + padding = "=" * (-len(text) % 4) + return urlsafe_b64decode(text + padding) diff --git a/app/services/auth.py b/app/services/auth.py new file mode 100644 index 0000000..807354d --- /dev/null +++ b/app/services/auth.py @@ -0,0 +1,103 @@ +"""注册、登录,以及首次启动时创建管理员。""" + +from __future__ import annotations + +import logging +import secrets + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.clock import utcnow +from app.config import settings +from app.errors import AppError +from app.models import User, UserRole +from app.security import hash_password, verify_password +from app.services.registry import register_user + +logger = logging.getLogger(__name__) + + +def register_account( + session: Session, + wallet_address: str, + username: str | None, + password: str | None, +) -> User: + name = _clean_username(username) + secret = (password or "").strip() + if name and not secret: + raise AppError("设置用户名时必须填写密码") + if secret and not name: + raise AppError("设置密码时必须填写用户名") + if secret and len(secret) < 6: + raise AppError("密码至少 6 位") + return register_user( + session, + wallet_address, + username=name, + password_hash=hash_password(secret) if secret else None, + ) + + +def authenticate( + session: Session, + wallet_address: str | None, + username: str | None, + password: str | None, +) -> User: + name = _clean_username(username) + wallet = (wallet_address or "").strip() + secret = (password or "").strip() + if name and secret: + user = session.scalar(select(User).where(User.username == name)) + if user is None or not verify_password(secret, user.password_hash): + raise AppError("账号或密码错误", 401) + return user + if wallet and secret: + user = session.scalar(select(User).where(User.wallet_address == wallet)) + if user is None or not verify_password(secret, user.password_hash): + raise AppError("账号或密码错误", 401) + return user + if wallet: + user = session.scalar(select(User).where(User.wallet_address == wallet)) + if user is None: + raise AppError("钱包未注册", 401) + if user.role == UserRole.ADMIN: + raise AppError("管理员请使用账号密码登录", 403) + return user + raise AppError("请填写钱包地址,或账号和密码") + + +def ensure_admin(session: Session) -> None: + username = _clean_username(settings.admin_username) or "admin" + existing = session.scalar(select(User).where(User.username == username)) + if existing is not None: + if existing.role != UserRole.ADMIN: + existing.role = UserRole.ADMIN + session.flush() + return + generated = not settings.admin_password + password = settings.admin_password or secrets.token_urlsafe(12) + wallet = settings.admin_wallet.strip() or "admin-root" + if session.scalar(select(User).where(User.wallet_address == wallet)) is not None: + wallet = f"{wallet}-{secrets.token_hex(3)}" + session.add( + User( + wallet_address=wallet, + username=username, + password_hash=hash_password(password), + role=UserRole.ADMIN, + created_at=utcnow(), + ) + ) + session.flush() + if generated: + logger.warning("已创建管理员 %s,初始密码:%s", username, password) + + +def _clean_username(username: str | None) -> str | None: + if username is None: + return None + cleaned = username.strip().lower() + return cleaned or None diff --git a/app/services/portal.py b/app/services/portal.py new file mode 100644 index 0000000..9da4438 --- /dev/null +++ b/app/services/portal.py @@ -0,0 +1,90 @@ +"""控制台要看的实时快照。今日积分按产出公式预览,不写入流水。""" + +from __future__ import annotations + +from decimal import Decimal + +from sqlalchemy import func, select +from sqlalchemy.orm import Session, selectinload + +from app.clock import utcnow +from app.models import ComputeNode, NodeStatus, NodeYield, User, WithdrawalLog, WithdrawalStatus, YieldStatus +from app.money import money, to_decimal +from app.services.withdrawals import available_balance +from app.services.yield_engine import compute_node_yield + + +def settled_points(session: Session, user_id: int | None = None) -> Decimal: + query = select(NodeYield.points_delta).where(NodeYield.status == YieldStatus.SETTLED) + if user_id is not None: + query = query.where(NodeYield.user_id == user_id) + rows = session.scalars(query).all() + return money(sum((to_decimal(item) for item in rows), Decimal("0"))) + + +def pending_withdrawal_amount(session: Session) -> Decimal: + rows = session.scalars( + select(WithdrawalLog.amount).where(WithdrawalLog.status == WithdrawalStatus.PENDING) + ).all() + return money(sum((to_decimal(item) for item in rows), Decimal("0"))) + + +def authorized_amount(session: Session) -> Decimal: + rows = session.scalars(select(ComputeNode.purchase_amount)).all() + return money(sum((to_decimal(item) for item in rows), Decimal("0"))) + + +def node_counts(session: Session) -> tuple[int, int]: + nodes = session.scalar(select(func.count()).select_from(ComputeNode)) or 0 + online = session.scalar( + select(func.count()).select_from(ComputeNode).where(ComputeNode.status == NodeStatus.ONLINE) + ) or 0 + return nodes, online + + +def live_nodes(session: Session, user_id: int | None = None) -> list[dict]: + query = select(ComputeNode).options(selectinload(ComputeNode.user)).order_by(ComputeNode.id.desc()) + if user_id is not None: + query = query.where(ComputeNode.user_id == user_id) + today = utcnow().date() + snapshots = [] + for node in session.scalars(query).all(): + computed = compute_node_yield(session, node, today) + snapshots.append( + { + "id": node.id, + "sn": node.sn, + "user_id": node.user_id, + "wallet_address": node.user.wallet_address, + "purchase_amount": node.purchase_amount, + "license_months": node.license_months, + "licensed_at": node.licensed_at, + "license_expires_at": node.license_expires_at, + "status": node.status, + "last_heartbeat_at": node.last_heartbeat_at, + "callback_url": node.callback_url, + "online_rate": computed.online_rate, + "today_points": computed.points_delta if computed.eligible else Decimal("0"), + } + ) + return snapshots + + +def user_rows(session: Session) -> list[dict]: + counts = dict( + session.execute(select(ComputeNode.user_id, func.count()).group_by(ComputeNode.user_id)).all() + ) + rows = [] + for user in session.scalars(select(User).order_by(User.id.desc())).all(): + rows.append( + { + "id": user.id, + "wallet_address": user.wallet_address, + "username": user.username, + "role": user.role, + "created_at": user.created_at, + "node_count": int(counts.get(user.id, 0)), + "available_points": available_balance(session, user.id), + } + ) + return rows diff --git a/app/services/registry.py b/app/services/registry.py index 8f24cc4..94e2001 100644 --- a/app/services/registry.py +++ b/app/services/registry.py @@ -12,22 +12,36 @@ from sqlalchemy.orm import Session from app.clock import add_months, utcnow from app.config import settings from app.errors import AppError -from app.models import ComputeNode, NodeStatus, User +from app.models import ComputeNode, NodeStatus, User, UserRole from app.money import money from app.services.common import get_user_by_wallet -def register_user(session: Session, wallet_address: str) -> User: +def register_user( + session: Session, + wallet_address: str, + username: str | None = None, + password_hash: str | None = None, + role: UserRole = UserRole.USER, +) -> User: wallet = wallet_address.strip() if session.scalar(select(User).where(User.wallet_address == wallet)) is not None: raise AppError("钱包地址已注册", 409) - user = User(wallet_address=wallet, created_at=utcnow()) + if username and session.scalar(select(User).where(User.username == username)) is not None: + raise AppError("用户名已占用", 409) + user = User( + wallet_address=wallet, + username=username, + password_hash=password_hash, + role=role, + created_at=utcnow(), + ) session.add(user) try: session.flush() except IntegrityError as exc: session.rollback() - raise AppError("钱包地址已注册", 409) from exc + raise AppError("钱包地址或用户名已注册", 409) from exc return user diff --git a/deploy/manage.sh b/deploy/manage.sh index 7410663..4e1d543 100644 --- a/deploy/manage.sh +++ b/deploy/manage.sh @@ -21,6 +21,7 @@ SERVICE_NAME="hypernode" BIND_HOST="0.0.0.0" BIND_PORT="33333" ENV_FILE="${INSTALL_ROOT}/.env" +GENERATED_ADMIN_PASSWORD="" _script_src="${BASH_SOURCE[0]:-}" if [[ -n "${_script_src}" && -f "${_script_src}" ]]; then @@ -132,6 +133,10 @@ write_env() { log "保留已有配置 ${ENV_FILE}" return 0 fi + local jwt_secret admin_password + jwt_secret="$(openssl rand -hex 32)" + admin_password="$(openssl rand -hex 12)" + GENERATED_ADMIN_PASSWORD="${admin_password}" log "写入 ${ENV_FILE}" cat >"${ENV_FILE}" <:${BIND_PORT}/" echo "接口文档:http://<服务器 IP>:${BIND_PORT}/docs" + if [[ -n "${GENERATED_ADMIN_PASSWORD}" ]]; then + echo "管理员账号:admin" + echo "管理员密码:${GENERATED_ADMIN_PASSWORD}" + echo "密码写在 ${ENV_FILE},请立刻记下。" + fi echo "程序目录:${INSTALL_ROOT}" echo "数据库:${DATA_DIR}/hypernode.db" echo "日志:journalctl -u ${SERVICE_NAME}.service -f" diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0de89fb --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + HyperNode · 算力控制面 + + + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..3038e57 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1285 @@ +{ + "name": "hypernode-console", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hypernode-console", + "dependencies": { + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^5.4.11" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.5.tgz", + "integrity": "sha512-MrfgRQSpD5hkkRg6XI16B6EDLb+6+xp8xje6bpdSwkYw+qxrBKbUVrRAQu9h82UT+QumSIVQ8HXsXjPd2dlhqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.5.tgz", + "integrity": "sha512-+uu2p/R9Tzn8GpoQyX1pAo5dmKT1DhXevfaCzqzmPFhnLjHLWw7uWkaVGlxmhwYoRiYYhsGfRcD5cTBpL7rgSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.5.tgz", + "integrity": "sha512-17dys+YEmzwzzFVHQAVDONg5FALXY9kd99HFEpfk4gJVHYxoh5bGea3D4t1fXXnwkTcOe25wPyd6nWS0I8IdLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.5.tgz", + "integrity": "sha512-wg0zQEKkEvFZf1UaaX1UELOoPs//OSKUG7D+yITuqOsAvza7LvHTv5Wu4nmMfzNG7EYI2jp2PJC7Id6OETmsYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.5.tgz", + "integrity": "sha512-CoJmWcpEMuwin+TZ/ev/H9uDyF/uxUKQSWXdmIPn/UBx/uGV9+4ebMp0FwnlhWR9PE1AdzYsKf3bZevhif38+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.5.tgz", + "integrity": "sha512-PxZnLAaTUAL5xG0W2BXLeYPKrK2b3LIASV2/e/Kf7PZvSLI1iAQIm9B9aJD+YyFIT7hc6qaMV3+OOO35R28LfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.5.tgz", + "integrity": "sha512-uPo3IvnONjhbFXwWgroBeFwOicuhT7ZwQrnzkzmOtBJzeQbkUJUE1SJDfDI31ijQ5+ZUIA6sPrdUMxTSnxUWAg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.5.tgz", + "integrity": "sha512-ywI8Jyuc4LyHYTjC0mw4yA1ulvZBkHusdpWnoiEBsmBuHvSnt90btTBGCZ+s/2y+YNNfVAEKy33nPo5UI9zTmg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.5.tgz", + "integrity": "sha512-tmuXJoQ/3ovSr637aykZMdQteU/CB2APAz2CrvsbibuoHZi1BS1Db13cVViBdEARGwXCLn1DSJ8wsjoThLJPGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.5.tgz", + "integrity": "sha512-5FMjSYNcUOHx7oW8Qu9KB6jGPpvF/8sG7+BDHwU/Ihh1/6FMJ9ySmsFG4tJ1qbNRuP8OLzCBDso4sF0mQaLuFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.5.tgz", + "integrity": "sha512-K8ZJ+neLDbUkIrL8dqF/5LtuVCrKwo27kREgOlxjBEjlOXwHvDnHLU8Z/UKv/B49qi7e8hsSDOyoJwhwqhvX4w==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.5.tgz", + "integrity": "sha512-NlVxsq7PFPv0DBKGuNWRDKixQYQCjM53zl04mxEcKMBmJbCBIUm1JdPfIWDRHyOOFzzYPmLQzfeEwh4716ayQg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.5.tgz", + "integrity": "sha512-ARhRHJYs5eCFet5azfrHD97vOkD8OzNW2RSMlvo5CcBqXaTOLfp0OHDtUQh7/K5AL5fUYoFp2fgC9KDg4vVYMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.5.tgz", + "integrity": "sha512-atSgreM5C7jpxd9aEXN+6vJ5wTd05iYw+S3FiQpw7VsoEl8B90llbq35ARON+GOHj5QlVQEq2vOKWCHTob/BSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.5.tgz", + "integrity": "sha512-vmlmwBFGnjVWxSRoVabNkyYuscSmg52Pd0fM8jEbNT3Jmka4A6HcB84x4uAmuumznWfBVaxKOqU6jbEgomWKCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.5.tgz", + "integrity": "sha512-hhJrP6S9WkbsbcHgUkvCBS5Cn8IdsG8XjlLZyxjnv9+NdoYfbbZE4TWJYr8apdaj1M5t5Y1id2StnEslxpNFyA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.5.tgz", + "integrity": "sha512-NNVCyN9pGrwFmQa4bf+Cnf5aVH3g6ZpnlOg1Cs/H9swIHT9bRixijfzUqyEIWm1WtiAuR1LJ+rJH6zpBm8kgQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.5.tgz", + "integrity": "sha512-dR9nd6OKnvSS60Ifz/L7Mj5zzYyoqyRn2NwTuaOONKaa3xsVGXyp8YusMbvFyEOR94DxnwgcbgsFqkHd29ZSzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.5.tgz", + "integrity": "sha512-1tzsMjaqhsh3bAO6SJiJqh8hIr9sSPRd+i2vPPxJHod8/XYnf9TEnK0z/u3r1z1AkI347X0KbRSg9dWcxeGahw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.5.tgz", + "integrity": "sha512-01mWMQblKoXXsmk28KXtpbA96w7g/aOWUoV3AC16fdHdqkykGalqyyM+5u8M7uP5/MEMiiPv69Hv7iv/hEH/dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.5.tgz", + "integrity": "sha512-trdnMPb/DNm4V/CgsaoGgr90HfQQ071wfK0KoIs4YpLvjBgVQGXhGabg2Z6nK4EcYNilrb9ZZPcsP6+zpJIdxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.5.tgz", + "integrity": "sha512-x2LxZlfdfsw99T6/qb9EqE/Qi84JWzibjyOJyhJsgFJy6CafaTiqIXCCnslCGrV5XWNh7fwrS3FbRENcuKnooA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.5.tgz", + "integrity": "sha512-/Qd/9aDQIo0+jmItgBmPIs8195YVbDxRRApn47GyufgbutCDOh7eP5vSKKlI4QNFeaI+wKQAkSMsPnF3EuMpaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.5.tgz", + "integrity": "sha512-ngUJ/XvOWrqbd2IMsfXfh2xXFLTP3sY7sCKvxbGvYdBZ1KmXFd3ewDZ9n3/GNzAUgzRuhLT/UvJRivbNoAQRBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.5.tgz", + "integrity": "sha512-IhV6ew/DuzUWwHaYmlh7MrPCV3lKFUq/amTEwysi4r/mwNPAEfAMh5dYWeTSPTeMy2pFHGMEWYtPkHzfX3tG5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.43.tgz", + "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.43", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz", + "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz", + "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.43", + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-ssr": "3.5.43", + "@vue/shared": "3.5.43", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.28", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz", + "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.43.tgz", + "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.43.tgz", + "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz", + "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/runtime-core": "3.5.43", + "@vue/shared": "3.5.43", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.43.tgz", + "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.43.tgz", + "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "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.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.64.5", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.64.5.tgz", + "integrity": "sha512-EAP5ZHcz2ijbgCWwPkpI7zh6bfsri2Ez5FtCy2dCp1k62duXzfG7DLGGh7RU2QQXWwavT+u4qpvDF/TWOiiGTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.5", + "@rollup/rollup-android-arm64": "4.64.5", + "@rollup/rollup-darwin-arm64": "4.64.5", + "@rollup/rollup-darwin-x64": "4.64.5", + "@rollup/rollup-freebsd-arm64": "4.64.5", + "@rollup/rollup-freebsd-x64": "4.64.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.5", + "@rollup/rollup-linux-arm-musleabihf": "4.64.5", + "@rollup/rollup-linux-arm64-gnu": "4.64.5", + "@rollup/rollup-linux-arm64-musl": "4.64.5", + "@rollup/rollup-linux-loong64-gnu": "4.64.5", + "@rollup/rollup-linux-loong64-musl": "4.64.5", + "@rollup/rollup-linux-ppc64-gnu": "4.64.5", + "@rollup/rollup-linux-ppc64-musl": "4.64.5", + "@rollup/rollup-linux-riscv64-gnu": "4.64.5", + "@rollup/rollup-linux-riscv64-musl": "4.64.5", + "@rollup/rollup-linux-s390x-gnu": "4.64.5", + "@rollup/rollup-linux-x64-gnu": "4.64.5", + "@rollup/rollup-linux-x64-musl": "4.64.5", + "@rollup/rollup-openbsd-x64": "4.64.5", + "@rollup/rollup-openharmony-arm64": "4.64.5", + "@rollup/rollup-win32-arm64-msvc": "4.64.5", + "@rollup/rollup-win32-ia32-msvc": "4.64.5", + "@rollup/rollup-win32-x64-gnu": "4.64.5", + "@rollup/rollup-win32-x64-msvc": "4.64.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/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/vue": { + "version": "3.5.43", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.43.tgz", + "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-sfc": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/server-renderer": "3.5.43", + "@vue/shared": "3.5.43" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/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" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..88cc1ff --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,17 @@ +{ + "name": "hypernode-console", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^5.4.11" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..98240ae --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..e5a1190 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,45 @@ +const KEYS = ["token", "role", "wallet", "username"]; + +export function session() { + return { + token: localStorage.getItem("token") || "", + role: localStorage.getItem("role") || "", + wallet: localStorage.getItem("wallet") || "", + username: localStorage.getItem("username") || "", + }; +} + +export function saveSession(payload) { + localStorage.setItem("token", payload.access_token); + localStorage.setItem("role", payload.role); + localStorage.setItem("wallet", payload.wallet_address); + localStorage.setItem("username", payload.username || ""); +} + +export function clearSession() { + KEYS.forEach((key) => localStorage.removeItem(key)); +} + +export async function api(path, options = {}) { + const headers = { ...(options.headers || {}) }; + if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json"; + const token = localStorage.getItem("token"); + if (token) headers.Authorization = `Bearer ${token}`; + const response = await fetch(`/api/v1${path}`, { ...options, headers }); + const text = await response.text(); + const data = text ? JSON.parse(text) : null; + if (response.status === 401) { + clearSession(); + if (location.pathname !== "/login") location.assign("/login"); + throw new Error(messageOf(data) || "登录已失效"); + } + if (!response.ok) throw new Error(messageOf(data) || "请求失败"); + return data; +} + +function messageOf(data) { + if (!data) return ""; + if (typeof data.detail === "string") return data.detail; + if (Array.isArray(data.detail)) return data.detail.map((item) => item.msg).filter(Boolean).join(";"); + return ""; +} diff --git a/frontend/src/components/Shell.vue b/frontend/src/components/Shell.vue new file mode 100644 index 0000000..7b1a18b --- /dev/null +++ b/frontend/src/components/Shell.vue @@ -0,0 +1,79 @@ + + + diff --git a/frontend/src/format.js b/frontend/src/format.js new file mode 100644 index 0000000..84db48e --- /dev/null +++ b/frontend/src/format.js @@ -0,0 +1,36 @@ +export function money(value) { + const [whole, fraction = ""] = String(value ?? "0").split("."); + const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + const trimmed = fraction.replace(/0+$/, ""); + return trimmed ? `${grouped}.${trimmed}` : grouped; +} + +export function pct(value) { + const number = Number(value) * 100; + if (!Number.isFinite(number)) return "0%"; + return `${number.toFixed(number >= 10 ? 0 : 1)}%`; +} + +export function when(value) { + if (!value) return "—"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "—"; + return new Intl.DateTimeFormat("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }).format(date); +} + +export const statusText = { + online: "在线", + offline: "离线", + pending: "待审核", + completed: "已打款", + rejected: "已拒绝", + failed: "失败", + settled: "已入账", + user: "用户", + admin: "管理员", +}; diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..4ab9f8f --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,6 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import { router } from "./router"; +import "./styles.css"; + +createApp(App).use(router).mount("#app"); diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..1e694d2 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,23 @@ +import { createRouter, createWebHistory } from "vue-router"; +import Login from "../views/Login.vue"; +import Dashboard from "../views/Dashboard.vue"; +import AdminOverview from "../views/AdminOverview.vue"; +import { session } from "../api"; + +export const router = createRouter({ + history: createWebHistory(), + routes: [ + { path: "/", redirect: () => (session().role === "admin" ? "/admin" : "/dashboard") }, + { path: "/login", component: Login }, + { path: "/dashboard", component: Dashboard, meta: { auth: true } }, + { path: "/admin", component: AdminOverview, meta: { auth: true, admin: true } }, + ], +}); + +router.beforeEach((to) => { + const current = session(); + if (to.meta.auth && !current.token) return "/login"; + if (to.meta.admin && current.role !== "admin") return "/dashboard"; + if (to.path === "/login" && current.token) return current.role === "admin" ? "/admin" : "/dashboard"; + return true; +}); diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..4570a5c --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,182 @@ +:root { + --bg: #08090d; + --ink: #f4efe6; + --muted: #a3988a; + --gold: #e0c07a; + --line: rgba(224, 192, 122, 0.18); + --panel: rgba(16, 18, 24, 0.82); + --online: #8ed0b5; + --offline: #d08a78; + --serif: "Fraunces", Palatino, serif; + --sans: "Manrope", "Segoe UI", sans-serif; + --mono: "IBM Plex Mono", ui-monospace, monospace; +} + +* { box-sizing: border-box; } +html, body, #app { min-height: 100%; } +body { + margin: 0; + color: var(--ink); + font-family: var(--sans); + background: + radial-gradient(980px 520px at 0% -10%, rgba(224, 192, 122, 0.16), transparent 58%), + radial-gradient(760px 480px at 100% 0%, rgba(126, 164, 196, 0.12), transparent 52%), + var(--bg); +} +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(244, 239, 230, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(244, 239, 230, 0.035) 1px, transparent 1px); + background-size: 84px 84px; + mask-image: radial-gradient(circle at 20% 10%, #000 0%, transparent 72%); +} +button, input { font: inherit; color: inherit; } +button { cursor: pointer; } +h1, h2, p { margin: 0; } +a { color: inherit; } + +.gate { + min-height: 100vh; + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: 48px; + align-items: center; + padding: 64px 8vw; +} +.gate-copy h1 { + margin: 12px 0 18px; + font-family: var(--serif); + font-size: clamp(52px, 6vw, 88px); + font-weight: 560; + letter-spacing: -0.04em; + line-height: 0.95; +} +.gate-copy p:last-child { max-width: 420px; color: var(--muted); font-size: 18px; line-height: 1.6; } +.eyebrow { + color: var(--gold); + letter-spacing: 0.18em; + text-transform: uppercase; + font-size: 12px; +} +.gate-card, .panel, .metric, .plate, .table-wrap { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 24px; + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35); +} +.gate-card { display: grid; gap: 14px; padding: 28px; } +.switch { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } +.switch button, .texty, .ghost { + border: 1px solid transparent; + background: transparent; + color: var(--muted); + border-radius: 999px; + padding: 10px 14px; +} +.switch button.on { color: var(--ink); background: rgba(224, 192, 122, 0.1); border-color: var(--line); } +.gold { + border: 0; + border-radius: 999px; + padding: 12px 18px; + background: linear-gradient(180deg, #f0d7a2, #c4964e); + color: #24180b; + font-weight: 700; +} +.gold.wide { width: 100%; margin-top: 4px; } +.gold:disabled { opacity: 0.6; } +label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } +input { + width: 100%; + border: 1px solid var(--line); + background: rgba(8, 9, 13, 0.55); + border-radius: 12px; + padding: 12px 14px; + outline: none; +} +input:focus { border-color: var(--gold); } +.hint, .sub { color: var(--muted); font-size: 13px; line-height: 1.5; } +.form-error { color: #e7b2a6; font-size: 14px; } +.banner { margin-bottom: 14px; } +.banner.ok { color: var(--online); } + +.shell { min-height: 100vh; display: grid; grid-template-columns: 248px 1fr; } +.rail { + position: sticky; + top: 0; + height: 100vh; + padding: 28px 20px; + display: flex; + flex-direction: column; + border-right: 1px solid var(--line); + background: rgba(8, 9, 13, 0.72); +} +.brand { display: flex; gap: 12px; align-items: center; text-decoration: none; } +.mark { + width: 44px; + height: 44px; + display: grid; + place-items: center; + color: var(--gold); + border: 1px solid var(--line); + border-radius: 14px; +} +.brand strong { display: block; font-family: var(--serif); font-size: 22px; font-weight: 560; } +.brand em { display: block; margin-top: 2px; color: var(--muted); font-style: normal; font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; } +.nav { display: grid; gap: 6px; margin-top: 36px; } +.nav a { text-decoration: none; color: var(--muted); border-radius: 12px; padding: 12px 14px; } +.nav a.router-link-active { color: var(--ink); background: rgba(224, 192, 122, 0.08); box-shadow: inset 2px 0 0 var(--gold); } +.rail-foot { margin-top: auto; display: flex; justify-content: space-between; align-items: center; gap: 8px; } +.who { color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; } +.stage { padding: 28px 32px 64px; } +.top { display: flex; justify-content: space-between; align-items: end; gap: 16px; } +.top h1 { margin-top: 8px; font-family: var(--serif); font-size: 56px; font-weight: 560; letter-spacing: -0.04em; } +.clock span { display: block; color: var(--muted); font-size: 11px; letter-spacing: 0.14em; } +.clock strong { font-family: var(--mono); font-size: 20px; font-weight: 500; } +.view { margin-top: 28px; } + +.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; } +.metric { padding: 22px; } +.metric span { color: var(--muted); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; } +.metric strong { display: block; margin-top: 12px; font-family: var(--serif); font-size: 40px; font-weight: 560; letter-spacing: -0.04em; } +.metric em { display: block; margin-top: 8px; color: var(--muted); font-style: normal; font-size: 13px; } +.section-title { margin: 28px 0 14px; font-family: var(--serif); font-weight: 560; font-size: 28px; } +.board { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; margin-top: 16px; } +.plate { padding: 18px; } +.plate header { display: flex; justify-content: space-between; align-items: center; gap: 8px; } +.plate header strong { font-family: var(--mono); font-size: 14px; } +.wallet { margin-top: 8px; color: var(--muted); font-family: var(--mono); font-size: 12px; } +.rate { position: relative; overflow: hidden; margin: 14px 0; padding: 8px 10px; border-radius: 999px; background: rgba(255, 255, 255, 0.03); color: var(--muted); font-size: 12px; } +.rate b { position: absolute; inset: 0 auto 0 0; background: rgba(224, 192, 122, 0.18); } +.plate dl { display: grid; gap: 8px; margin: 0; } +.plate dl div { display: flex; justify-content: space-between; gap: 12px; font-size: 14px; } +.plate dt { color: var(--muted); } +.plate dd { margin: 0; } +.pill { display: inline-flex; align-items: center; gap: 6px; border-radius: 999px; padding: 4px 10px; border: 1px solid var(--line); font-size: 12px; } +.pill i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; } +.pill.online, .pill.completed { color: var(--online); } +.pill.offline, .pill.rejected, .pill.failed { color: var(--offline); } +.pill.pending { color: var(--gold); } +.forms { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; align-items: start; } +.panel { display: grid; gap: 12px; padding: 22px; } +.panel h2 { font-family: var(--serif); font-size: 28px; font-weight: 560; } +.table-wrap { overflow: auto; margin-top: 8px; } +table { width: 100%; border-collapse: collapse; } +th, td { padding: 14px 16px; text-align: left; border-bottom: 1px solid rgba(224, 192, 122, 0.08); font-size: 14px; } +th { color: var(--muted); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; } +td input { width: 180px; padding: 8px 10px; } +.actions { display: flex; gap: 8px; } +.mono { font-family: var(--mono); font-size: 12px; } +.empty { padding: 28px 16px; color: var(--muted); } +.mini { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 8px; } +.mini li { display: flex; justify-content: space-between; gap: 8px; font-size: 13px; } + +@media (max-width: 1100px) { + .metrics, .forms, .gate { grid-template-columns: 1fr; } + .shell { grid-template-columns: 1fr; } + .rail { position: relative; height: auto; } + .top h1 { font-size: 40px; } +} diff --git a/frontend/src/views/AdminOverview.vue b/frontend/src/views/AdminOverview.vue new file mode 100644 index 0000000..c5194cf --- /dev/null +++ b/frontend/src/views/AdminOverview.vue @@ -0,0 +1,170 @@ + + + diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..279178c --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,133 @@ + + + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..50531b6 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,102 @@ + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..b6c10dd --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + plugins: [vue()], + build: { + outDir: "../web", + emptyOutDir: true, + }, + server: { + proxy: { + "/api": "http://127.0.0.1:8000", + "/health": "http://127.0.0.1:8000", + }, + }, +}); diff --git a/requirements.txt b/requirements.txt index cd1c077..ab4eff1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ pydantic>=2.6 pydantic-settings>=2.2 httpx>=0.27 tzdata>=2024.1 +PyJWT>=2.8 diff --git a/tests/test_core_flow.py b/tests/test_core_flow.py index 8331f29..243cb4a 100644 --- a/tests/test_core_flow.py +++ b/tests/test_core_flow.py @@ -13,6 +13,10 @@ from pathlib import Path _db_path = Path(tempfile.gettempdir()) / "depin_core_flow.db" os.environ["DATABASE_URL"] = "sqlite:///" + _db_path.as_posix() os.environ["DISABLE_SCHEDULER"] = "1" +os.environ["JWT_SECRET"] = "test-secret-test-secret-test-secret" +os.environ["ADMIN_USERNAME"] = "admin" +os.environ["ADMIN_PASSWORD"] = "test-admin-pass" +os.environ["ADMIN_WALLET"] = "admin-root" from fastapi.testclient import TestClient from sqlalchemy import select @@ -129,7 +133,11 @@ class CoreFlowTest(unittest.TestCase): session.add(self._task(miss.id, TaskStatus.FAILED, day_start)) session.commit() - settled = self.client.post("/api/v1/yield/settle", params={"yield_date": yesterday.isoformat()}) + settled = self.client.post( + "/api/v1/yield/settle", + headers=self._admin_headers(), + params={"yield_date": yesterday.isoformat()}, + ) self.assertEqual(settled.status_code, 200, settled.text) self.assertEqual(settled.json()["created"], 3) @@ -153,11 +161,19 @@ class CoreFlowTest(unittest.TestCase): self.assertEqual(miss_row.points_delta, Decimal("0.70000000")) self.assertEqual(miss_row.compute_output, Decimal("255.50000000")) - again = self.client.post("/api/v1/yield/settle", params={"yield_date": yesterday.isoformat()}) + again = self.client.post( + "/api/v1/yield/settle", + headers=self._admin_headers(), + params={"yield_date": yesterday.isoformat()}, + ) self.assertEqual(again.json()["created"], 0) self.assertEqual(again.json()["skipped"], 3) - future = self.client.post("/api/v1/yield/settle", params={"yield_date": "2999-01-01"}) + future = self.client.post( + "/api/v1/yield/settle", + headers=self._admin_headers(), + params={"yield_date": "2999-01-01"}, + ) self.assertEqual(future.status_code, 400, future.text) def test_open_interval_stops_at_last_heartbeat(self) -> None: @@ -173,7 +189,11 @@ class CoreFlowTest(unittest.TestCase): session.add(UptimeInterval(node_id=node.id, started_at=day_start, ended_at=None)) session.commit() - settled = self.client.post("/api/v1/yield/settle", params={"yield_date": yesterday.isoformat()}) + settled = self.client.post( + "/api/v1/yield/settle", + headers=self._admin_headers(), + params={"yield_date": yesterday.isoformat()}, + ) self.assertEqual(settled.status_code, 200, settled.text) row = self._yield_row("NODE-HALF") self.assertEqual(row.online_seconds, 12 * 60 * 60) @@ -205,17 +225,24 @@ class CoreFlowTest(unittest.TestCase): day_start = utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=1) self._register("0xpay", "NODE-PAY", "365") self._cover_full_day("NODE-PAY", day_start, day_start + timedelta(days=1)) - settled = self.client.post("/api/v1/yield/settle", params={"yield_date": yesterday.isoformat()}) + payer = self._auth("0xpay") + settled = self.client.post( + "/api/v1/yield/settle", + headers=self._admin_headers(), + params={"yield_date": yesterday.isoformat()}, + ) self.assertEqual(settled.status_code, 200, settled.text) too_much = self.client.post( "/api/v1/withdrawals", + headers=payer, json={"wallet_address": "0xpay", "amount": "10"}, ) self.assertEqual(too_much.status_code, 400, too_much.text) created = self.client.post( "/api/v1/withdrawals", + headers=payer, json={"wallet_address": "0xpay", "amount": "0.4", "asset_symbol": "POINT"}, ) self.assertEqual(created.status_code, 200, created.text) @@ -224,26 +251,70 @@ class CoreFlowTest(unittest.TestCase): blocked = self.client.post( "/api/v1/withdrawals", + headers=payer, json={"wallet_address": "0xpay", "amount": "0.7"}, ) self.assertEqual(blocked.status_code, 400, blocked.text) confirmed = self.client.post( f"/api/v1/withdrawals/{withdrawal_id}/confirm", + headers=self._admin_headers(), json={"tx_hash": "0xabc123"}, ) self.assertEqual(confirmed.status_code, 200, confirmed.text) self.assertEqual(confirmed.json()["status"], "completed") self.assertEqual(confirmed.json()["tx_hash"], "0xabc123") - balance = self.client.get("/api/v1/users/0xpay/balance") + balance = self.client.get("/api/v1/users/0xpay/balance", headers=self._auth("0xpay")) self.assertEqual(balance.json()["available_points"], "0.60000000") + def test_roles_split_user_and_admin(self) -> None: + registered = self.client.post( + "/api/v1/auth/register", + json={"wallet_address": "0xuser01", "username": "miner", "password": "secret1"}, + ) + self.assertEqual(registered.status_code, 200, registered.text) + self.assertEqual(registered.json()["role"], "user") + token = registered.json()["access_token"] + user_headers = {"Authorization": f"Bearer {token}"} + + by_wallet = self.client.post("/api/v1/auth/login", json={"wallet_address": "0xuser01"}) + self.assertEqual(by_wallet.status_code, 200, by_wallet.text) + by_password = self.client.post( + "/api/v1/auth/login", + json={"username": "miner", "password": "secret1"}, + ) + self.assertEqual(by_password.status_code, 200, by_password.text) + + denied = self.client.get("/api/v1/admin/users", headers=user_headers) + self.assertEqual(denied.status_code, 403, denied.text) + opened = self.client.get("/api/v1/admin/summary", headers=self._admin_headers()) + self.assertEqual(opened.status_code, 200, opened.text) + self.assertGreaterEqual(opened.json()["users"], 1) + + wallet_admin = self.client.post("/api/v1/auth/login", json={"wallet_address": "admin-root"}) + self.assertEqual(wallet_admin.status_code, 403, wallet_admin.text) + + def _auth(self, wallet: str) -> dict[str, str]: + logged = self.client.post("/api/v1/auth/login", json={"wallet_address": wallet}) + self.assertEqual(logged.status_code, 200, logged.text) + return {"Authorization": f"Bearer {logged.json()['access_token']}"} + + def _admin_headers(self) -> dict[str, str]: + logged = self.client.post( + "/api/v1/auth/login", + json={"username": "admin", "password": "test-admin-pass"}, + ) + self.assertEqual(logged.status_code, 200, logged.text) + self.assertEqual(logged.json()["role"], "admin") + return {"Authorization": f"Bearer {logged.json()['access_token']}"} + def _register(self, wallet: str, sn: str, amount: str) -> None: user = self.client.post("/api/v1/users", json={"wallet_address": wallet}) self.assertEqual(user.status_code, 200, user.text) node = self.client.post( "/api/v1/nodes", + headers=self._auth(wallet), json={"wallet_address": wallet, "sn": sn, "purchase_amount": amount}, ) self.assertEqual(node.status_code, 200, node.text) diff --git a/web/app.js b/web/app.js deleted file mode 100644 index 7ff2760..0000000 --- a/web/app.js +++ /dev/null @@ -1,498 +0,0 @@ -const viewRoot = document.querySelector("#view"); -const titleEl = document.querySelector("#title"); -const toastEl = document.querySelector("#toast"); -const state = { - view: "overview", - summary: null, - nodes: [], - users: [], - withdrawals: [], - yields: [], - selected: "", -}; - -const copy = { - overview: "总览", - nodes: "节点", - ledger: "流水", - cash: "提现", -}; - -document.querySelectorAll("#nav button").forEach((button) => { - button.addEventListener("click", () => go(button.dataset.view)); -}); -document.querySelector("#refresh").addEventListener("click", () => load().catch(showError)); -window.addEventListener("hashchange", syncFromHash); - -tick(); -setInterval(tick, 1000); -syncFromHash(); -load().catch(showError); -probeHealth(); - -function tick() { - document.querySelector("#clock").textContent = new Intl.DateTimeFormat("zh-CN", { - timeZone: "Asia/Shanghai", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }).format(new Date()); -} - -function go(view) { - location.hash = view; -} - -async function syncFromHash() { - const next = (location.hash || "#overview").slice(1); - state.view = copy[next] ? next : "overview"; - document.querySelectorAll("#nav button").forEach((button) => { - button.classList.toggle("active", button.dataset.view === state.view); - }); - titleEl.textContent = copy[state.view]; - if (state.view === "ledger" && state.selected) { - try { - state.yields = await api(`/nodes/${encodeURIComponent(state.selected)}/yields`); - } catch (error) { - showError(error); - } - } - render(); -} - -async function load() { - const [summary, nodes, users, withdrawals] = await Promise.all([ - api("/summary"), - api("/nodes"), - api("/users"), - api("/withdrawals"), - ]); - state.summary = summary; - state.nodes = nodes; - state.users = users; - state.withdrawals = withdrawals; - if (!state.selected && nodes[0]) state.selected = nodes[0].sn; - if (state.view === "ledger" && state.selected) { - state.yields = await api(`/nodes/${encodeURIComponent(state.selected)}/yields`); - } - render(); -} - -async function probeHealth() { - const dot = document.querySelector("#health-dot"); - const text = document.querySelector("#health-text"); - try { - const res = await fetch("/health"); - if (!res.ok) throw new Error("offline"); - dot.classList.add("on"); - text.textContent = "服务在线"; - } catch { - text.textContent = "服务未响应"; - } -} - -function render() { - const painters = { overview: paintOverview, nodes: paintNodes, ledger: paintLedger, cash: paintCash }; - viewRoot.innerHTML = painters[state.view](); - bind(); -} - -function paintOverview() { - const s = state.summary || {}; - return ` -
- ${metric("授权金额", money(s.authorized_amount), `${s.nodes || 0} 台节点`)} - ${metric("在线", `${s.online || 0}`, `离线 ${s.offline || 0}`)} - ${metric("已入账积分", money(s.settled_points), `${s.users || 0} 个钱包`)} - ${metric("待处理提现", money(s.pending_withdrawals), "占用可用余额")} -
-
- - - - ${state.nodes.length ? state.nodes.slice(0, 8).map(nodeRow).join("") : ``} - -
节点钱包购买金额状态到期
还没有节点。下面可以登记第一台。
-
-
- ${userForm()} - ${nodeForm()} -
- `; -} - -function paintNodes() { - return ` -
-
- - - - ${state.nodes.length ? state.nodes.map(nodeRow).join("") : ``} - -
序列号钱包购买金额状态到期
节点列表是空的。
-
- ${nodeForm()} -
- `; -} - -function paintLedger() { - const options = state.nodes.map((node) => ``).join(""); - return ` -
- - - -
-
- - - - ${state.yields.length ? state.yields.map(yieldRow).join("") : ``} - -
日期积分算力产出在线率响应率表现状态
这一天还没有入账记录。
-
- `; -} - -function paintCash() { - return ` -
-
- - - - ${state.withdrawals.length ? state.withdrawals.map(cashRow).join("") : ``} - -
单号钱包金额状态
还没有提现申请。
-
-
-
-

可用积分

-

已入账积分减去待处理和已完成的提现。

- -
- -
-

- ${walletList()} -
-
-

申请提现

-

提交后金额会被占用,确认时再写入链上哈希。

-
- - - - -
-
-
-
- `; -} - -function metric(label, value, note) { - return `
${esc(label)}${esc(value ?? "0")}${esc(note || "")}
`; -} - -function userForm() { - return ` -
-

登记钱包

-

一个钱包可以绑定多台节点。

- -
-
- `; -} - -function nodeForm() { - return ` -
-

登记节点

-

授权周期 12 个月,购买金额按天释放。

- ${walletList()} -
- - - - - -
-
- `; -} - -function walletList() { - const options = state.users.map((user) => ``).join(""); - return `${options}`; -} - -function nodeRow(node) { - return ` - ${esc(node.sn)} - ${esc(short(node.wallet_address))} - ${esc(money(node.purchase_amount))} - ${pill(node.status)} - ${esc(day(node.license_expires_at))} - `; -} - -function yieldRow(row) { - return ` - ${esc(row.yield_date)} - ${esc(money(row.points_delta))} - ${esc(money(row.compute_output))} - ${esc(pct(row.online_rate))} - ${esc(pct(row.task_response_rate))} - ${esc(pct(row.performance))} - ${pill(row.status)} - `; -} - -function cashRow(row) { - const actions = row.status === "pending" - ? `
- - - -
` - : `${esc(row.tx_hash || "—")}`; - return ` - #${row.id} - ${esc(short(row.wallet_address))} - ${esc(money(row.amount))} ${esc(row.asset_symbol)} - ${pill(row.status)} - ${actions} - `; -} - -function pill(status) { - const label = { - online: "在线", - offline: "离线", - pending: "待处理", - completed: "已完成", - rejected: "已拒绝", - failed: "失败", - settled: "已入账", - void: "作废", - }[status] || status; - return `${esc(label)}`; -} - -function bind() { - const userFormEl = document.querySelector("#user-form"); - const nodeFormEl = document.querySelector("#node-form"); - const balanceForm = document.querySelector("#balance-form"); - const withdrawForm = document.querySelector("#withdraw-form"); - if (userFormEl) userFormEl.addEventListener("submit", onCreateUser); - if (nodeFormEl) nodeFormEl.addEventListener("submit", onCreateNode); - if (balanceForm) balanceForm.addEventListener("submit", onBalance); - if (withdrawForm) withdrawForm.addEventListener("submit", onWithdraw); - document.querySelectorAll("tr.clickable").forEach((row) => row.addEventListener("click", () => openNode(row.dataset.sn))); - const ledgerNode = document.querySelector("#ledger-node"); - if (ledgerNode) { - ledgerNode.addEventListener("change", async () => { - state.selected = ledgerNode.value; - state.yields = state.selected ? await api(`/nodes/${encodeURIComponent(state.selected)}/yields`) : []; - render(); - }); - } - const settle = document.querySelector("#settle"); - if (settle) settle.addEventListener("click", onSettle); - document.querySelectorAll("[data-confirm]").forEach((button) => button.addEventListener("click", () => onConfirm(button.dataset.confirm))); - document.querySelectorAll("[data-reject]").forEach((button) => button.addEventListener("click", () => onReject(button.dataset.reject))); -} - -async function onCreateUser(event) { - event.preventDefault(); - const body = Object.fromEntries(new FormData(event.target)); - await run(() => api("/users", { method: "POST", body: JSON.stringify(body) }), "钱包已登记"); -} - -async function onCreateNode(event) { - event.preventDefault(); - const body = Object.fromEntries(new FormData(event.target)); - if (!body.callback_url) delete body.callback_url; - await run(() => api("/nodes", { method: "POST", body: JSON.stringify(body) }), "节点已登记"); -} - -async function onBalance(event) { - event.preventDefault(); - const wallet = new FormData(event.target).get("wallet_address"); - const data = await api(`/users/${encodeURIComponent(wallet)}/balance`); - document.querySelector("#balance-result").textContent = `${data.wallet_address} 可用 ${money(data.available_points)}`; -} - -async function onWithdraw(event) { - event.preventDefault(); - const body = Object.fromEntries(new FormData(event.target)); - await run(() => api("/withdrawals", { method: "POST", body: JSON.stringify(body) }), "提现申请已提交"); -} - -async function onSettle() { - try { - const date = document.querySelector("#settle-date").value; - const query = date ? `?yield_date=${date}` : ""; - const result = await api(`/yield/settle${query}`, { method: "POST" }); - showToast(`已清算 ${result.yield_date},新增 ${result.created} 条,积分 ${money(result.total_points)}`); - await load(); - } catch (error) { - showError(error); - } -} - -async function onConfirm(id) { - const hash = document.querySelector(`[data-hash="${id}"]`).value.trim(); - if (!hash) return showToast("请先填写交易哈希"); - await run(() => api(`/withdrawals/${id}/confirm`, { method: "POST", body: JSON.stringify({ tx_hash: hash }) }), "提现已确认"); -} - -async function onReject(id) { - await run(() => api(`/withdrawals/${id}/reject`, { method: "POST", body: JSON.stringify({ note: "控制台拒绝" }) }), "提现已拒绝"); -} - -async function openNode(sn) { - const node = state.nodes.find((item) => item.sn === sn); - if (!node) return; - let yields = []; - try { - yields = await api(`/nodes/${encodeURIComponent(sn)}/yields`); - } catch (error) { - showError(error); - } - const mask = document.createElement("div"); - mask.className = "mask"; - mask.innerHTML = ` - - `; - mask.addEventListener("click", (event) => { - if (event.target === mask || event.target.closest("[data-close]")) mask.remove(); - }); - mask.querySelector("#beat").addEventListener("click", async () => { - await run(() => api("/node/heartbeat", { method: "POST", body: JSON.stringify({ sn }) }), "心跳已记录"); - mask.remove(); - }); - document.body.appendChild(mask); -} - -async function run(action, message) { - try { - await action(); - showToast(message); - await load(); - } catch (error) { - showError(error); - } -} - -async function api(path, options = {}) { - const res = await fetch(`/api/v1${path}`, { - ...options, - headers: { "Content-Type": "application/json", ...(options.headers || {}) }, - }); - const text = await res.text(); - const data = text ? JSON.parse(text) : null; - if (!res.ok) throw new Error(errorText(data)); - return data; -} - -function errorText(data) { - const detail = data && data.detail; - if (typeof detail === "string") return detail; - if (Array.isArray(detail)) return detail.map((item) => item.msg).join(";"); - return "请求失败"; -} - -function showToast(message) { - toastEl.textContent = message; - toastEl.classList.add("show"); - clearTimeout(showToast.timer); - showToast.timer = setTimeout(() => toastEl.classList.remove("show"), 2600); -} - -function showError(error) { - showToast(error.message || "操作失败"); -} - -function money(value) { - if (value == null || value === "") return "0"; - const raw = String(value); - const neg = raw.startsWith("-"); - const [whole, frac = ""] = raw.replace("-", "").split("."); - const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ","); - const trimmed = frac.replace(/0+$/, ""); - return `${neg ? "-" : ""}${trimmed ? `${grouped}.${trimmed}` : grouped}`; -} - -function pct(value) { - const number = Number(value); - if (!Number.isFinite(number)) return "—"; - return `${(number * 100).toFixed(1)}%`; -} - -function when(value) { - if (!value) return "—"; - return new Intl.DateTimeFormat("zh-CN", { - timeZone: "Asia/Shanghai", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }).format(new Date(value)); -} - -function day(value) { - if (!value) return "—"; - return new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date(value)); -} - -function short(value) { - const text = String(value || ""); - return text.length > 16 ? `${text.slice(0, 8)}…${text.slice(-4)}` : text; -} - -function esc(value) { - return String(value ?? "").replace(/[&<>"']/g, (char) => ({ - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - }[char])); -} diff --git a/web/assets/index-CZ0L8T-X.js b/web/assets/index-CZ0L8T-X.js new file mode 100644 index 0000000..d4f6cce --- /dev/null +++ b/web/assets/index-CZ0L8T-X.js @@ -0,0 +1,26 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const l of i.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&s(l)}).observe(document,{childList:!0,subtree:!0});function n(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(r){if(r.ep)return;r.ep=!0;const i=n(r);fetch(r.href,i)}})();/** +* @vue/shared v3.5.43 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Bs(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const se={},Nt=[],st=()=>{},ri=()=>!1,Wn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),qn=e=>e.startsWith("onUpdate:"),ve=Object.assign,Us=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Tl=Object.prototype.hasOwnProperty,Q=(e,t)=>Tl.call(e,t),j=Array.isArray,St=e=>wn(e)==="[object Map]",Vn=e=>wn(e)==="[object Set]",cr=e=>wn(e)==="[object Date]",U=e=>typeof e=="function",oe=e=>typeof e=="string",je=e=>typeof e=="symbol",X=e=>e!==null&&typeof e=="object",ii=e=>(X(e)||U(e))&&U(e.then)&&U(e.catch),li=Object.prototype.toString,wn=e=>li.call(e),Pl=e=>wn(e).slice(8,-1),oi=e=>wn(e)==="[object Object]",$s=e=>oe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,rn=Bs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Jn=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Il=/-\w/g,Oe=Jn(e=>e.replace(Il,t=>t.slice(1).toUpperCase())),Nl=/\B([A-Z])/g,Vt=Jn(e=>e.replace(Nl,"-$1").toLowerCase()),zn=Jn(e=>e.charAt(0).toUpperCase()+e.slice(1)),cs=Jn(e=>e?`on${zn(e)}`:""),et=(e,t)=>!Object.is(e,t),Nn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},Ks=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let ar;const Yn=()=>ar||(ar=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function En(e){if(j(e)){const t={};for(let n=0;nn.startsWith("/*")?"":n).split(Ml).forEach(n=>{if(n){const s=n.split(Dl);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function ke(e){let t="";if(oe(e))t=e;else if(j(e))for(let n=0;n!!(e&&e.__v_isRef===!0),V=e=>oe(e)?e:e==null?"":j(e)||X(e)&&(e.toString===li||!U(e.toString))?ai(e)?V(e.value):JSON.stringify(e,fi,2):String(e),fi=(e,t)=>ai(t)?fi(e,t.value):St(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[as(s,i)+" =>"]=r,n),{})}:Vn(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>as(n))}:je(t)?as(t):X(t)&&!j(t)&&!oi(t)?String(t):t,as=(e,t="")=>{var n;return je(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.43 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let pe;class Bl{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&pe&&(pe.active?(this.parent=pe,this.index=(pe.scopes||(pe.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes){const s=this.scopes.slice();for(t=0,n=s.length;t0&&--this._on===0){if(pe===this)pe=this.prevScope;else{let t=pe;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(on){let t=on;for(on=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;ln;){let t=ln;for(ln=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function gi(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function mi(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),qs(s),$l(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function As(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(vi(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function vi(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===hn)||(e.globalVersion=hn,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!As(e))))return;e.flags|=2;const t=e.dep,n=re,s=He;re=e,He=!0;try{gi(e);const r=e.fn(e._value);(t.version===0||et(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{re=n,He=s,mi(e),e.flags&=-3}}function qs(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)qs(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function $l(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let He=!0;const _i=[];function gt(){_i.push(He),He=!1}function mt(){const e=_i.pop();He=e===void 0?!0:e}function hr(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=re;re=void 0;try{t()}finally{re=n}}}let hn=0;class Kl{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Js{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!re||!He||re===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==re)n=this.activeLink=new Kl(re,this),re.deps?(n.prevDep=re.depsTail,re.depsTail.nextDep=n,re.depsTail=n):re.deps=re.depsTail=n,yi(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=re.depsTail,n.nextDep=void 0,re.depsTail.nextDep=n,re.depsTail=n,re.deps===n&&(re.deps=s)}return n}trigger(t){this.version++,hn++,this.notify(t)}notify(t){Gs();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Ws()}}}function yi(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)yi(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Cs=new WeakMap,Dt=Symbol(""),Rs=Symbol(""),pn=Symbol("");function ye(e,t,n){if(He&&re){let s=Cs.get(e);s||Cs.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new Js),r.map=s,r.key=n),r.track()}}function dt(e,t,n,s,r,i){const l=Cs.get(e);if(!l){hn++;return}const o=u=>{u&&u.trigger()};if(Gs(),t==="clear")l.forEach(o);else{const u=j(e),d=u&&$s(n);if(u&&n==="length"){const a=Number(s);l.forEach((f,p)=>{(p==="length"||p===pn||!je(p)&&p>=a)&&o(f)})}else switch((n!==void 0||l.has(void 0))&&o(l.get(n)),d&&o(l.get(pn)),t){case"add":u?d&&o(l.get("length")):(o(l.get(Dt)),St(e)&&o(l.get(Rs)));break;case"delete":u||(o(l.get(Dt)),St(e)&&o(l.get(Rs)));break;case"set":St(e)&&o(l.get(Dt));break}}Ws()}function jt(e){const t=Y(e);return t===e||(ye(t,"iterate",pn),Le(e))?t:it(e)?At(e)?t.map(n=>Ct(Ve(n))):t.map(Ct):t.map(Ve)}function Xn(e){return ye(e=Y(e),"iterate",pn),e}function Xe(e,t){return it(e)?Ct(At(e)?Ve(t):t):Ve(t)}const Gl={__proto__:null,[Symbol.iterator](){return ds(this,Symbol.iterator,e=>Xe(this,e))},concat(...e){return jt(this).concat(...e.map(t=>j(t)?jt(t):t))},entries(){return ds(this,"entries",e=>(e[1]=Xe(this,e[1]),e))},every(e,t){return ut(this,"every",e,t,void 0,arguments)},filter(e,t){return ut(this,"filter",e,t,n=>n.map(s=>Xe(this,s)),arguments)},find(e,t){return ut(this,"find",e,t,n=>Xe(this,n),arguments)},findIndex(e,t){return ut(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return ut(this,"findLast",e,t,n=>Xe(this,n),arguments)},findLastIndex(e,t){return ut(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return ut(this,"forEach",e,t,void 0,arguments)},includes(...e){return hs(this,"includes",e)},indexOf(...e){return hs(this,"indexOf",e)},join(e){return jt(this).join(e)},lastIndexOf(...e){return hs(this,"lastIndexOf",e)},map(e,t){return ut(this,"map",e,t,void 0,arguments)},pop(){return Zt(this,"pop")},push(...e){return Zt(this,"push",e)},reduce(e,...t){return pr(this,"reduce",e,t)},reduceRight(e,...t){return pr(this,"reduceRight",e,t)},shift(){return Zt(this,"shift")},some(e,t){return ut(this,"some",e,t,void 0,arguments)},splice(...e){return Zt(this,"splice",e)},toReversed(){return jt(this).toReversed()},toSorted(e){return jt(this).toSorted(e)},toSpliced(...e){return jt(this).toSpliced(...e)},unshift(...e){return Zt(this,"unshift",e)},values(){return ds(this,"values",e=>Xe(this,e))}};function ds(e,t,n){const s=Xn(e),r=s[t]();return s!==e&&!Le(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.done||(i.value=n(i.value)),i}),r}const Wl=Array.prototype;function ut(e,t,n,s,r,i){const l=Xn(e),o=l!==e&&!Le(e),u=l[t];if(u!==Wl[t]){const f=u.apply(e,i);return o?Ve(f):f}let d=n;l!==e&&(o?d=function(f,p){return n.call(this,Xe(e,f),p,e)}:n.length>2&&(d=function(f,p){return n.call(this,f,p,e)}));const a=u.call(l,d,s);return o&&r?r(a):a}function pr(e,t,n,s){const r=Xn(e),i=r!==e&&!Le(e);let l=n,o=!1;r!==e&&(i?(o=s.length===0,l=function(d,a,f){return o&&(o=!1,d=Xe(e,d)),n.call(this,d,Xe(e,a),f,e)}):n.length>3&&(l=function(d,a,f){return n.call(this,d,a,f,e)}));const u=r[t](l,...s);return o?Xe(e,u):u}function hs(e,t,n){const s=Y(e);ye(s,"iterate",pn);const r=s[t](...n);return(r===-1||r===!1)&&Qs(n[0])?(n[0]=Y(n[0]),s[t](...n)):r}function Zt(e,t,n=[]){gt(),Gs();const s=Y(e)[t].apply(e,n);return Ws(),mt(),s}const ql=Bs("__proto__,__v_isRef,__isVue"),bi=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(je));function Jl(e){je(e)||(e=String(e));const t=Y(this);return ye(t,"has",e),t.hasOwnProperty(e)}class wi{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?ro:Ai:i?Si:xi).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const l=j(t);if(!r){let u;if(l&&(u=Gl[n]))return u;if(n==="hasOwnProperty")return Jl}const o=Reflect.get(t,n,xe(t)?t:s);if((je(n)?bi.has(n):ql(n))||(r||ye(t,"get",n),i))return o;if(xe(o)){const u=l&&$s(n)?o:o.value;return r&&X(u)?Ts(u):u}return X(o)?r?Ts(o):Zn(o):o}}class Ei extends wi{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];const l=j(t)&&$s(n);if(!this._isShallow){const d=it(i);if(!Le(s)&&!it(s)&&(i=Y(i),s=Y(s)),!l&&xe(i)&&!xe(s))return d||(i.value=s),!0}const o=l?Number(n)e,Cn=e=>Reflect.getPrototypeOf(e);function Zl(e,t,n){return function(...s){const r=this.__v_raw,i=Y(r),l=St(i),o=e==="entries"||e===Symbol.iterator&&l,u=e==="keys"&&l,d=r[e](...s),a=n?Os:t?Ct:Ve;return!t&&ye(i,"iterate",u?Rs:Dt),ve(Object.create(d),{next(){const{value:f,done:p}=d.next();return p?{value:f,done:p}:{value:o?[a(f[0]),a(f[1])]:a(f),done:p}}})}}function Rn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function eo(e,t){const n={get(r){const i=this.__v_raw,l=Y(i),o=Y(r);e||(et(r,o)&&ye(l,"get",r),ye(l,"get",o));const{has:u}=Cn(l),d=t?Os:e?Ct:Ve;if(u.call(l,r))return d(i.get(r));if(u.call(l,o))return d(i.get(o));i!==l&&i.get(r)},get size(){const r=this.__v_raw;return!e&&ye(Y(r),"iterate",Dt),r.size},has(r){const i=this.__v_raw,l=Y(i),o=Y(r);return e||(et(r,o)&&ye(l,"has",r),ye(l,"has",o)),r===o?i.has(r):i.has(r)||i.has(o)},forEach(r,i){const l=this,o=l.__v_raw,u=Y(o),d=t?Os:e?Ct:Ve;return!e&&ye(u,"iterate",Dt),o.forEach((a,f)=>r.call(i,d(a),d(f),l))}};return ve(n,e?{add:Rn("add"),set:Rn("set"),delete:Rn("delete"),clear:Rn("clear")}:{add(r){const i=Y(this),l=Cn(i),o=Y(r),u=!t&&!Le(r)&&!it(r)?o:r;return l.has.call(i,u)||et(r,u)&&l.has.call(i,r)||et(o,u)&&l.has.call(i,o)||(i.add(u),dt(i,"add",u,u)),this},set(r,i){!t&&!Le(i)&&!it(i)&&(i=Y(i));const l=Y(this),{has:o,get:u}=Cn(l);let d=o.call(l,r);d||(r=Y(r),d=o.call(l,r));const a=u.call(l,r);return l.set(r,i),d?et(i,a)&&dt(l,"set",r,i):dt(l,"add",r,i),this},delete(r){const i=Y(this),{has:l,get:o}=Cn(i);let u=l.call(i,r);u||(r=Y(r),u=l.call(i,r)),o&&o.call(i,r);const d=i.delete(r);return u&&dt(i,"delete",r,void 0),d},clear(){const r=Y(this),i=r.size!==0,l=r.clear();return i&&dt(r,"clear",void 0,void 0),l}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=Zl(r,e,t)}),n}function zs(e,t){const n=eo(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(Q(n,r)&&r in s?n:s,r,i)}const to={get:zs(!1,!1)},no={get:zs(!1,!0)},so={get:zs(!0,!1)};const xi=new WeakMap,Si=new WeakMap,Ai=new WeakMap,ro=new WeakMap;function io(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Zn(e){return it(e)?e:Ys(e,!1,Yl,to,xi)}function Ci(e){return Ys(e,!1,Xl,no,Si)}function Ts(e){return Ys(e,!0,Ql,so,Ai)}function Ys(e,t,n,s,r){if(!X(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const i=r.get(e);if(i)return i;const l=io(Pl(e));if(l===0)return e;const o=new Proxy(e,l===2?s:n);return r.set(e,o),o}function At(e){return it(e)?At(e.__v_raw):!!(e&&e.__v_isReactive)}function it(e){return!!(e&&e.__v_isReadonly)}function Le(e){return!!(e&&e.__v_isShallow)}function Qs(e){return e?!!e.__v_raw:!1}function Y(e){const t=e&&e.__v_raw;return t?Y(t):e}function lo(e){return!Q(e,"__v_skip")&&Object.isExtensible(e)&&ui(e,"__v_skip",!0),e}const Ve=e=>X(e)?Zn(e):e,Ct=e=>X(e)?Ts(e):e;function xe(e){return e?e.__v_isRef===!0:!1}function fe(e){return Ri(e,!1)}function oo(e){return Ri(e,!0)}function Ri(e,t){return xe(e)?e:new uo(e,t)}class uo{constructor(t,n){this.dep=new Js,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:Y(t),this._value=n?t:Ve(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Le(t)||it(t);t=s?t:Y(t),et(t,n)&&(this._rawValue=t,this._value=s?t:Ve(t),this.dep.trigger())}}function W(e){return xe(e)?e.value:e}const co={get:(e,t,n)=>t==="__v_raw"?e:W(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return xe(r)&&!xe(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Oi(e){return At(e)?e:new Proxy(e,co)}class ao{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new Js(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=hn-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&re!==this)return pi(this,!0),!0}get value(){const t=this.dep.track();return vi(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function fo(e,t,n=!1){let s,r;return U(e)?s=e:(s=e.get,r=e.set),new ao(s,r,n)}const On={},Fn=new WeakMap;let It;function ho(e,t=!1,n=It){if(n){let s=Fn.get(n);s||Fn.set(n,s=[]),s.push(e)}}function po(e,t,n=se){const{immediate:s,deep:r,once:i,scheduler:l,augmentJob:o,call:u}=n,d=N=>r?N:Le(N)||r===!1||r===0?ht(N,1):ht(N);let a,f,p,g,S=!1,E=!1;if(xe(e)?(f=()=>e.value,S=Le(e)):At(e)?(f=()=>d(e),S=!0):j(e)?(E=!0,S=e.some(N=>At(N)||Le(N)),f=()=>e.map(N=>{if(xe(N))return N.value;if(At(N))return d(N);if(U(N))return u?u(N,2):N()})):U(e)?t?f=u?()=>u(e,2):e:f=()=>{if(p){gt();try{p()}finally{mt()}}const N=It;It=a;try{return u?u(e,3,[g]):e(g)}finally{It=N}}:f=st,t&&r){const N=f,te=r===!0?1/0:r;f=()=>ht(N(),te)}const B=Ul(),k=()=>{a.stop(),B&&B.active&&Us(B.effects,a)};if(i&&t){const N=t;t=(...te)=>{const ce=N(...te);return k(),ce}}let P=E?new Array(e.length).fill(On):On;const M=N=>{if(!(!(a.flags&1)||!a.dirty&&!N))if(t){const te=a.run();if(N||r||S||(E?te.some((ce,ie)=>et(ce,P[ie])):et(te,P))){p&&p();const ce=It;It=a;try{const ie=[te,P===On?void 0:E&&P[0]===On?[]:P,g];P=te,u?u(t,3,ie):t(...ie)}finally{It=ce}}}else a.run()};return o&&o(M),a=new di(f),a.scheduler=l?()=>l(M,!1):M,g=N=>ho(N,!1,a),p=a.onStop=()=>{const N=Fn.get(a);if(N){if(u)u(N,4);else for(const te of N)te();Fn.delete(a)}},t?s?M(!0):P=a.run():l?l(M.bind(null,!0),!0):a.run(),k.pause=a.pause.bind(a),k.resume=a.resume.bind(a),k.stop=k,k}function ht(e,t=1/0,n){if(t<=0||!X(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,xe(e))ht(e.value,t,n);else if(j(e))for(let s=0;s{ht(s,t,n)});else if(oi(e)){for(const s in e)ht(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&ht(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.43 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function xn(e,t,n,s){try{return s?e(...s):e()}catch(r){es(r,t,n)}}function Be(e,t,n,s){if(U(e)){const r=xn(e,t,n,s);return r&&ii(r)&&r.catch(i=>{es(i,t,n)}),r}if(j(e)){const r=[];for(let i=0;i>>1,r=Re[s],i=gn(r);i=gn(n)?Re.push(e):Re.splice(mo(t),0,e),e.flags|=1,Ii()}}function Ii(){kn||(kn=Ti.then(Mi))}function vo(e){if(!j(e))Et&&e.id===-1?Et.splice(Bt+1,0,e):e.flags&1||(Kt.push(e),e.flags|=1);else for(let t=0;tgn(n)-gn(s));if(Kt.length=0,Et){for(let n=0;ne.id==null?e.flags&2?-1:1/0:e.id;function Mi(e){try{for(Qe=0;Qe{s._d&&Un(-1);const i=Hn(t),l=pt.length;let o;try{o=e(...r)}finally{for(let u=pt.length;u>l;u--)sr();Hn(i),s._d&&Un(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function tt(e,t){if(me===null)return e;const n=ls(me),s=e.dirs||(e.dirs=[]);for(let r=0;r1)return n&&U(t)?t.call(s&&s.proxy):t}}const _o=Symbol.for("v-scx"),yo=()=>rt(_o);function Dn(e,t,n){return Li(e,t,n)}function Li(e,t,n=se){const{immediate:s,deep:r,flush:i,once:l}=n,o=ve({},n),u=t&&s||!t&&i!=="post";let d;if(yn){if(i==="sync"){const g=yo();d=g.__watcherHandles||(g.__watcherHandles=[])}else if(!u){const g=()=>{};return g.stop=st,g.resume=st,g.pause=st,g}}const a=we;o.call=(g,S,E)=>Be(g,a,S,E);let f=!1;i==="post"?o.scheduler=g=>{Te(g,a&&a.suspense)}:i!=="sync"&&(f=!0,o.scheduler=(g,S)=>{S?g():Xs(g)}),o.augmentJob=g=>{t&&(g.flags|=4),f&&(g.flags|=2,a&&(g.id=a.uid,g.i=a))};const p=po(e,t,o);return yn&&(d?d.push(p):u&&p()),p}function bo(e,t,n){const s=this.proxy,r=oe(e)?e.includes(".")?Vi(s,e):()=>s[e]:e.bind(s,s);let i;U(t)?i=t:(i=t.handler,n=t);const l=An(this),o=Li(r,i.bind(s),n);return l(),o}function Vi(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;re.__isTeleport,ps=Symbol("_leaveCb");function Eo(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==lt){t=n;break}}return t}function Fi(e){if(!er(e))return ts(e.type)&&e.children?Eo(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&U(n.default))return n.default()}}function Zs(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;const n=e.component.subTree;Zs(ts(n.type)&&Fi(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function ki(e,t){return U(e)?ve({name:e.name},t,{setup:e}):e}function Hi(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function mr(e,t){let n;return!!((n=Object.getOwnPropertyDescriptor(e,t))&&!n.configurable)}const jn=new WeakMap;function un(e,t,n,s,r=!1){if(j(e)){e.forEach((E,B)=>un(E,t&&(j(t)?t[B]:t),n,s,r));return}if(Wt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&un(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?ls(s.component):s.el,l=r?null:i,{i:o,r:u}=e,d=t&&t.r,a=o.refs===se?o.refs={}:o.refs,f=o.setupState,p=Y(f),g=f===se?ri:E=>mr(a,E)?!1:Q(p,E),S=(E,B)=>!(B&&mr(a,B));if(d!=null&&d!==u){if(vr(t),oe(d))a[d]=null,g(d)&&(f[d]=null);else if(xe(d)){const E=t;S(d,E.k)&&(d.value=null),E.k&&(a[E.k]=null)}}if(U(u))xn(u,o,12,[l,a]);else{const E=oe(u),B=xe(u);if(E||B){const k=()=>{if(e.f){const P=E?g(u)?f[u]:a[u]:S()||!e.k?u.value:a[e.k];if(r)j(P)&&Us(P,i);else if(j(P))P.includes(i)||P.push(i);else if(E)a[u]=[i],g(u)&&(f[u]=a[u]);else{const M=[i];S(u,e.k)&&(u.value=M),e.k&&(a[e.k]=M)}}else E?(a[u]=l,g(u)&&(f[u]=l)):B&&(S(u,e.k)&&(u.value=l),e.k&&(a[e.k]=l))};if(l){const P=()=>{k(),jn.delete(e)};P.id=-1,jn.set(e,P),Te(P,n)}else vr(e),k()}}}function vr(e){const t=jn.get(e);t&&(t.flags|=8,jn.delete(e))}Yn().requestIdleCallback;Yn().cancelIdleCallback;const Wt=e=>!!e.type.__asyncLoader,er=e=>e.type.__isKeepAlive;function xo(e,t){ji(e,"a",t)}function So(e,t){ji(e,"da",t)}function ji(e,t,n=we){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(ns(t,s,n),n){let r=n.parent;for(;r&&r.parent;)er(r.parent.vnode)&&Ao(s,t,n,r),r=r.parent}}function Ao(e,t,n,s){const r=ns(t,e,s,!0);Sn(()=>{Us(s[t],r)},n)}function ns(e,t,n=we,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...l)=>{gt();const o=An(n),u=Be(t,n,e,l);return o(),mt(),u});return s?r.unshift(i):r.push(i),i}}const vt=e=>(t,n=we)=>{(!yn||e==="sp")&&ns(e,(...s)=>t(...s),n)},Co=vt("bm"),ss=vt("m"),Ro=vt("bu"),Oo=vt("u"),To=vt("bum"),Sn=vt("um"),Po=vt("sp"),Io=vt("rtg"),No=vt("rtc");function Mo(e,t=we){ns("ec",e,t)}const Do="components";function Bi(e,t){return Vo(Do,e,!0,t)||e}const Lo=Symbol.for("v-ndc");function Vo(e,t,n=!0,s=!1){const r=me||we;if(r){const i=r.type;{const o=wu(i,!1);if(o&&(o===t||o===Oe(t)||o===zn(Oe(t))))return i}const l=_r(r[e]||i[e],t)||_r(r.appContext[e],t);return!l&&s?i:l}}function _r(e,t){return e&&(e[t]||e[Oe(t)]||e[zn(Oe(t))])}function cn(e,t,n,s){let r;const i=n,l=j(e);if(l||oe(e)){const o=l&&At(e);let u=!1,d=!1;o&&(u=!Le(e),d=it(e),e=Xn(e)),r=new Array(e.length);for(let a=0,f=e.length;at(o,u,void 0,i));else{const o=Object.keys(e);r=new Array(o.length);for(let u=0,d=o.length;u0;return $(),Lt(ge,null,[Ee("slot",d,s)],a?-2:64)}let l=e[t];l&&l._c&&(l._d=!1);const o=pt.length;$();let u;try{const d=l&&Ui(l(n)),a=n.key||i||d&&d.key;u=Lt(ge,{key:(a&&!je(a)?a:`_${t}`)+(!d&&s?"_fb":"")},d||(s?s():[]),d&&e._===1?64:-2)}catch(d){for(let a=pt.length;a>o;a--)sr();throw d}finally{l&&l._c&&(l._d=!0)}return u.scopeId&&(u.slotScopeIds=[u.scopeId+"-s"]),u}function Ui(e){return e.some(t=>vn(t)?!(t.type===lt||t.type===ge&&!Ui(t.children)):!0)?e:null}const Ps=e=>e?ul(e)?ls(e):Ps(e.parent):null,an=ve(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Ps(e.parent),$root:e=>Ps(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Ki(e),$forceUpdate:e=>e.f||(e.f=()=>{Xs(e.update)}),$nextTick:e=>e.n||(e.n=Pi.bind(e.proxy)),$watch:e=>bo.bind(e)}),gs=(e,t)=>e!==se&&!e.__isScriptSetup&&Q(e,t),ko={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:l,type:o,appContext:u}=e;if(t[0]!=="$"){const p=l[t];if(p!==void 0)switch(p){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(gs(s,t))return l[t]=1,s[t];if(r!==se&&Q(r,t))return l[t]=2,r[t];if(Q(i,t))return l[t]=3,i[t];if(n!==se&&Q(n,t))return l[t]=4,n[t];Is&&(l[t]=0)}}const d=an[t];let a,f;if(d)return t==="$attrs"&&ye(e.attrs,"get",""),d(e);if((a=o.__cssModules)&&(a=a[t]))return a;if(n!==se&&Q(n,t))return l[t]=4,n[t];if(f=u.config.globalProperties,Q(f,t))return f[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return gs(r,t)?(r[t]=n,!0):s!==se&&Q(s,t)?(s[t]=n,!0):Q(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,props:i,type:l}},o){let u;return!!(n[o]||e!==se&&o[0]!=="$"&&Q(e,o)||gs(t,o)||Q(i,o)||Q(s,o)||Q(an,o)||Q(r.config.globalProperties,o)||(u=l.__cssModules)&&u[o])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:Q(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function yr(e){return j(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Is=!0;function Ho(e){const t=Ki(e),n=e.proxy,s=e.ctx;Is=!1,t.beforeCreate&&br(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:l,watch:o,provide:u,inject:d,created:a,beforeMount:f,mounted:p,beforeUpdate:g,updated:S,activated:E,deactivated:B,beforeDestroy:k,beforeUnmount:P,destroyed:M,unmounted:N,render:te,renderTracked:ce,renderTriggered:ie,errorCaptured:$e,serverPrefetch:_t,expose:Ke,inheritAttrs:yt,components:Rt,directives:Ge,filters:Qt}=t;if(d&&jo(d,s,null),l)for(const Z in l){const J=l[Z];U(J)&&(s[Z]=J.bind(n))}if(r){const Z=r.call(n,n);X(Z)&&(e.data=Zn(Z))}if(Is=!0,i)for(const Z in i){const J=i[Z],ot=U(J)?J.bind(n,n):U(J.get)?J.get.bind(n,n):st,bt=!U(J)&&U(J.set)?J.set.bind(n):st,We=Fe({get:ot,set:bt});Object.defineProperty(s,Z,{enumerable:!0,configurable:!0,get:()=>We.value,set:Se=>We.value=Se})}if(o)for(const Z in o)$i(o[Z],s,n,Z);if(u){const Z=U(u)?u.call(n):u;Reflect.ownKeys(Z).forEach(J=>{Mn(J,Z[J])})}a&&br(a,e,"c");function de(Z,J){j(J)?J.forEach(ot=>Z(ot.bind(n))):J&&Z(J.bind(n))}if(de(Co,f),de(ss,p),de(Ro,g),de(Oo,S),de(xo,E),de(So,B),de(Mo,$e),de(No,ce),de(Io,ie),de(To,P),de(Sn,N),de(Po,_t),j(Ke))if(Ke.length){const Z=e.exposed||(e.exposed={});Ke.forEach(J=>{Object.defineProperty(Z,J,{get:()=>n[J],set:ot=>n[J]=ot,enumerable:!0})})}else e.exposed||(e.exposed={});te&&e.render===st&&(e.render=te),yt!=null&&(e.inheritAttrs=yt),Rt&&(e.components=Rt),Ge&&(e.directives=Ge),_t&&Hi(e)}function jo(e,t,n=st){j(e)&&(e=Ns(e));for(const s in e){const r=e[s];let i;X(r)?"default"in r?i=rt(r.from||s,r.default,!0):i=rt(r.from||s):i=rt(r),xe(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:l=>i.value=l}):t[s]=i}}function br(e,t,n){Be(j(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function $i(e,t,n,s){let r=s.includes(".")?Vi(n,s):()=>n[s];if(oe(e)){const i=t[e];U(i)&&Dn(r,i)}else if(U(e))Dn(r,e.bind(n));else if(X(e))if(j(e))e.forEach(i=>$i(i,t,n,s));else{const i=U(e.handler)?e.handler.bind(n):t[e.handler];U(i)&&Dn(r,i,e)}}function Ki(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:l}}=e.appContext,o=i.get(t);let u;return o?u=o:!r.length&&!n&&!s?u=t:(u={},r.length&&r.forEach(d=>Bn(u,d,l,!0)),Bn(u,t,l)),X(t)&&i.set(t,u),u}function Bn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Bn(e,i,n,!0),r&&r.forEach(l=>Bn(e,l,n,!0));for(const l in t)if(!(s&&l==="expose")){const o=Bo[l]||n&&n[l];e[l]=o?o(e[l],t[l]):t[l]}return e}const Bo={data:wr,props:Er,emits:Er,methods:nn,computed:nn,beforeCreate:Ae,created:Ae,beforeMount:Ae,mounted:Ae,beforeUpdate:Ae,updated:Ae,beforeDestroy:Ae,beforeUnmount:Ae,destroyed:Ae,unmounted:Ae,activated:Ae,deactivated:Ae,errorCaptured:Ae,serverPrefetch:Ae,components:nn,directives:nn,watch:$o,provide:wr,inject:Uo};function wr(e,t){return t?e?function(){return ve(U(e)?e.call(this,this):e,U(t)?t.call(this,this):t)}:t:e}function Uo(e,t){return nn(Ns(e),Ns(t))}function Ns(e){if(j(e)){const t={};for(let n=0;nt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Oe(t)}Modifiers`]||e[`${Vt(t)}Modifiers`];function qo(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||se;let r=n;const i=t.startsWith("update:"),l=i&&Wo(s,t.slice(7));l&&(l.trim&&(r=n.map(a=>oe(a)?a.trim():a)),l.number&&(r=r.map(Ks)));let o,u=s[o=cs(t)]||s[o=cs(Oe(t))];!u&&i&&(u=s[o=cs(Vt(t))]),u&&Be(u,e,6,r);const d=s[o+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[o])return;e.emitted[o]=!0,Be(d,e,6,r)}}const Jo=new WeakMap;function Wi(e,t,n=!1){const s=n?Jo:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let l={},o=!1;if(!U(e)){const u=d=>{const a=Wi(d,t,!0);a&&(o=!0,ve(l,a))};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}return!i&&!o?(X(e)&&s.set(e,null),null):(j(i)?i.forEach(u=>l[u]=null):ve(l,i),X(e)&&s.set(e,l),l)}function rs(e,t){return!e||!Wn(t)?!1:(t=t.slice(2),t=t==="Once"?t:t.replace(/Once$/,""),Q(e,t[0].toLowerCase()+t.slice(1))||Q(e,Vt(t))||Q(e,t))}function xr(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:l,attrs:o,emit:u,render:d,renderCache:a,props:f,data:p,setupState:g,ctx:S,inheritAttrs:E}=e,B=Hn(e);let k,P;try{if(n.shapeFlag&4){const N=r||s,te=N;k=Ze(d.call(te,N,a,f,g,p,S)),P=o}else{const N=t;k=Ze(N.length>1?N(f,{attrs:o,slots:l,emit:u}):N(f,null)),P=t.props?o:zo(o)}}catch(N){pt.length=0,es(N,e,1),k=Ee(lt)}let M=k;if(P&&E!==!1){const N=Object.keys(P),{shapeFlag:te}=M;N.length&&te&7&&(i&&N.some(qn)&&(P=Yo(P,i)),M=Jt(M,P,!1,!0))}if(n.dirs&&(M=Jt(M,null,!1,!0),M.dirs=M.dirs?M.dirs.concat(n.dirs):n.dirs),n.transition){const N=ts(M.type)&&Fi(M)||M;Zs(N,n.transition)}return k=M,Hn(B),k}const zo=e=>{let t;for(const n in e)(n==="class"||n==="style"||Wn(n))&&((t||(t={}))[n]=e[n]);return t},Yo=(e,t)=>{const n={};for(const s in e)(!qn(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Qo(e,t,n){const{props:s,children:r,component:i}=e,{props:l,children:o,patchFlag:u}=t,d=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&u>=0){if(u&1024)return!0;if(u&16)return s?Sr(s,l,d):!!l;if(u&8){const a=t.dynamicProps;for(let f=0;fObject.create(Ji),Yi=e=>Object.getPrototypeOf(e)===Ji;function Zo(e,t,n,s=!1){const r={},i=zi();e.propsDefaults=Object.create(null),Qi(e,t,r,i);for(const l in e.propsOptions[0])l in r||(r[l]=void 0);n?e.props=s?r:Ci(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function eu(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:l}}=e,o=Y(r),[u]=e.propsOptions;let d=!1;if((s||l>0)&&!(l&16)){if(l&8){const a=e.vnode.dynamicProps;for(let f=0;f{u=!0;const[p,g]=Xi(f,t,!0);ve(l,p),g&&o.push(...g)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!u)return X(e)&&s.set(e,Nt),Nt;if(j(i))for(let a=0;ae==="_"||e==="_ctx"||e==="$stable",nr=e=>j(e)?e.map(Ze):[Ze(e)],nu=(e,t,n)=>{if(t._n)return t;const s=Gt((...r)=>nr(t(...r)),n);return s._c=!1,s},Zi=(e,t,n)=>{const s=e._ctx;for(const r in e){if(tr(r))continue;const i=e[r];if(U(i))t[r]=nu(r,i,s);else if(i!=null){const l=nr(i);t[r]=()=>l}}},el=(e,t)=>{const n=nr(t);e.slots.default=()=>n},tl=(e,t,n)=>{for(const s in t)(n||!tr(s))&&(e[s]=t[s])},su=(e,t,n)=>{const s=e.slots=zi();if(e.vnode.shapeFlag&32){const r=t._;r?(tl(s,t,n),n&&ui(s,"_",r,!0)):Zi(t,s)}else t&&el(e,t)},ru=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,l=se;if(s.shapeFlag&32){const o=t._;o?n&&o===1?i=!1:tl(r,t,n):(i=!t.$stable,Zi(t,r)),l=t}else t&&(el(e,t),l={default:1});if(i)for(const o in r)!tr(o)&&l[o]==null&&delete r[o]},Te=cu;function iu(e){return lu(e)}function lu(e,t){const n=Yn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:l,createText:o,createComment:u,setText:d,setElementText:a,parentNode:f,nextSibling:p,setScopeId:g=st,insertStaticContent:S}=e,E=(c,h,m,y=null,w=null,_=null,R=void 0,C=null,A=!!h.dynamicChildren)=>{if(c===h)return;c&&!en(c,h)&&(y=b(c),Se(c,w,_,!0),c=null),h.patchFlag===-2&&(A=!1,h.dynamicChildren=null),h.dynamicChildren&&c&&c.dynamicChildren&&c.dynamicChildren.hasOnce&&(h.dynamicChildren===Nt&&(h.dynamicChildren=[]),h.dynamicChildren.hasOnce=!0);const{type:x,ref:F,shapeFlag:T}=h;switch(x){case is:B(c,h,m,y);break;case lt:k(c,h,m,y);break;case vs:c==null&&P(h,m,y,R);break;case ge:Rt(c,h,m,y,w,_,R,C,A);break;default:T&1?te(c,h,m,y,w,_,R,C,A):T&6?Ge(c,h,m,y,w,_,R,C,A):(T&64||T&128)&&x.process(c,h,m,y,w,_,R,C,A,D)}F!=null&&w?un(F,c&&c.ref,_,h||c,!h):F==null&&c&&c.ref!=null&&un(c.ref,null,_,c,!0)},B=(c,h,m,y)=>{if(c==null)s(h.el=o(h.children),m,y);else{const w=h.el=c.el;h.children!==c.children&&d(w,h.children)}},k=(c,h,m,y)=>{c==null?s(h.el=u(h.children||""),m,y):h.el=c.el},P=(c,h,m,y)=>{[c.el,c.anchor]=S(c.children,h,m,y,c.el,c.anchor)},M=({el:c,anchor:h},m,y)=>{let w;for(;c&&c!==h;)w=p(c),s(c,m,y),c=w;s(h,m,y)},N=({el:c,anchor:h})=>{let m;for(;c&&c!==h;)m=p(c),r(c),c=m;r(h)},te=(c,h,m,y,w,_,R,C,A)=>{if(h.type==="svg"?R="svg":h.type==="math"&&(R="mathml"),c==null)ce(h,m,y,w,_,R,C,A);else{const x=c.el&&c.el._isVueCE?c.el:null;try{x&&x._beginPatch(),_t(c,h,w,_,R,C,A)}finally{x&&x._endPatch()}}},ce=(c,h,m,y,w,_,R,C)=>{let A,x;const{props:F,shapeFlag:T,transition:L,dirs:H}=c;if(A=c.el=l(c.type,_,F&&F.is,F),T&8?a(A,c.children):T&16&&$e(c.children,A,null,y,w,ms(c,_),R,C),H&&Tt(c,null,y,"created"),ie(A,c,c.scopeId,R,y),F){for(const ne in F)ne!=="value"&&!rn(ne)&&i(A,ne,null,F[ne],_,y);"value"in F&&i(A,"value",null,F.value,_),(x=F.onVnodeBeforeMount)&&Ye(x,y,c)}H&&Tt(c,null,y,"beforeMount");const G=ou(w,L);G&&L.beforeEnter(A),s(A,h,m),((x=F&&F.onVnodeMounted)||G||H)&&Te(()=>{try{x&&Ye(x,y,c),G&&L.enter(A),H&&Tt(c,null,y,"mounted")}finally{}},w)},ie=(c,h,m,y,w)=>{if(m&&g(c,m),y)for(let _=0;_{for(let x=A;x{const C=h.el=c.el;let{patchFlag:A,dynamicChildren:x,dirs:F}=h;A|=c.patchFlag&16;const T=c.props||se,L=h.props||se;let H;if(m&&Pt(m,!1),(H=L.onVnodeBeforeUpdate)&&Ye(H,m,h,c),F&&Tt(h,c,m,"beforeUpdate"),m&&Pt(m,!0),x&&(!c.dynamicChildren||c.dynamicChildren.length!==x.length)&&(A=0,R=!1,x=null),(T.innerHTML&&L.innerHTML==null||T.textContent&&L.textContent==null)&&a(C,""),x?Ke(c.dynamicChildren,x,C,m,y,ms(h,w),_):R||J(c,h,C,null,m,y,ms(h,w),_,!1),A>0){if(A&16)yt(C,T,L,m,w);else if(A&2&&T.class!==L.class&&i(C,"class",null,L.class,w),A&4&&i(C,"style",T.style,L.style,w),A&8){const G=h.dynamicProps;for(let ne=0;ne{H&&Ye(H,m,h,c),F&&Tt(h,c,m,"updated")},y)},Ke=(c,h,m,y,w,_,R)=>{for(let C=0;C{if(h!==m){if(h!==se)for(const _ in h)!rn(_)&&!(_ in m)&&i(c,_,h[_],null,w,y);for(const _ in m){if(rn(_))continue;const R=m[_],C=h[_];R!==C&&_!=="value"&&i(c,_,C,R,w,y)}"value"in m&&i(c,"value",h.value,m.value,w)}},Rt=(c,h,m,y,w,_,R,C,A)=>{const x=h.el=c?c.el:o(""),F=h.anchor=c?c.anchor:o("");let{patchFlag:T,dynamicChildren:L,slotScopeIds:H}=h;H&&(C=C?C.concat(H):H),c==null?(s(x,m,y),s(F,m,y),$e(h.children||[],m,F,w,_,R,C,A)):T>0&&T&64&&L&&c.dynamicChildren&&c.dynamicChildren.length===L.length?(Ke(c.dynamicChildren,L,m,w,_,R,C),(h.key!=null||w&&h===w.subTree)&&nl(c,h,!0)):J(c,h,m,F,w,_,R,C,A)},Ge=(c,h,m,y,w,_,R,C,A)=>{h.slotScopeIds=C,c==null?h.shapeFlag&512?w.ctx.activate(h,m,y,R,A):Qt(h,m,y,w,_,R,A):Ft(c,h,A)},Qt=(c,h,m,y,w,_,R)=>{const C=c.component=gu(c,y,w);if(er(c)&&(C.ctx.renderer=D),vu(C,!1,R),C.asyncDep){if(w&&w.registerDep(C,de,R),!c.el){const A=C.subTree=Ee(lt);k(null,A,h,m),c.placeholder=A.el}}else de(C,c,h,m,w,_,R)},Ft=(c,h,m)=>{const y=h.component=c.component;if(Qo(c,h,m))if(y.asyncDep&&!y.asyncResolved){h.el=c.el,Z(y,h,m);return}else y.next=h,y.update();else h.el=c.el,y.vnode=h},de=(c,h,m,y,w,_,R)=>{const C=()=>{if(c.isMounted){let{next:T,bu:L,u:H,parent:G,vnode:ne}=c;{const Je=sl(c);if(Je){T&&(T.el=ne.el,Z(c,T,R)),Je.asyncDep.then(()=>{Te(()=>{c.isUnmounted||x()},w)});return}}let ee=T,ue;Pt(c,!1),T?(T.el=ne.el,Z(c,T,R)):T=ne,L&&Nn(L),(ue=T.props&&T.props.onVnodeBeforeUpdate)&&Ye(ue,G,T,ne),Pt(c,!0);const he=xr(c),qe=c.subTree;c.subTree=he,E(qe,he,f(qe.el),b(qe),c,w,_),T.el=he.el,ee===null&&Xo(c,he.el),H&&Te(H,w),(ue=T.props&&T.props.onVnodeUpdated)&&Te(()=>Ye(ue,G,T,ne),w)}else{let T;const{el:L,props:H}=h,{bm:G,m:ne,parent:ee,root:ue,type:he}=c,qe=Wt(h);Pt(c,!1),G&&Nn(G),!qe&&(T=H&&H.onVnodeBeforeMount)&&Ye(T,ee,h),Pt(c,!0);{ue.ce&&ue.ce._hasShadowRoot()&&ue.ce._injectChildStyle(he,c.parent?c.parent.type:void 0);const Je=c.subTree=xr(c);E(null,Je,m,y,c,w,_),h.el=Je.el}if(ne&&Te(ne,w),!qe&&(T=H&&H.onVnodeMounted)){const Je=h;Te(()=>Ye(T,ee,Je),w)}(h.shapeFlag&256||ee&&Wt(ee.vnode)&&ee.vnode.shapeFlag&256)&&c.a&&Te(c.a,w),c.isMounted=!0,h=m=y=null}};c.scope.on();const A=c.effect=new di(C);c.scope.off();const x=c.update=A.run.bind(A),F=c.job=A.runIfDirty.bind(A);F.i=c,F.id=c.uid,A.scheduler=()=>Xs(F),Pt(c,!0),x()},Z=(c,h,m)=>{h.component=c;const y=c.vnode.props;c.vnode=h,c.next=null,eu(c,h.props,y,m),ru(c,h.children,m),gt(),gr(c),mt()},J=(c,h,m,y,w,_,R,C,A=!1)=>{const x=c&&c.children,F=c?c.shapeFlag:0,T=h.children,{patchFlag:L,shapeFlag:H}=h;if(L>0){if(L&128){bt(x,T,m,y,w,_,R,C,A);return}else if(L&256){ot(x,T,m,y,w,_,R,C,A);return}}H&8?(F&16&&Ne(x,w,_),T!==x&&a(m,T)):F&16?H&16?bt(x,T,m,y,w,_,R,C,A):Ne(x,w,_,!0):(F&8&&a(m,""),H&16&&$e(T,m,y,w,_,R,C,A))},ot=(c,h,m,y,w,_,R,C,A)=>{c=c||Nt,h=h||Nt;const x=c.length,F=h.length,T=Math.min(x,F);let L;for(L=0;LF?Ne(c,w,_,!0,!1,T):$e(h,m,y,w,_,R,C,A,T)},bt=(c,h,m,y,w,_,R,C,A)=>{let x=0;const F=h.length;let T=c.length-1,L=F-1;for(;x<=T&&x<=L;){const H=c[x],G=h[x]=A?ft(h[x]):Ze(h[x]);if(en(H,G))E(H,G,m,null,w,_,R,C,A);else break;x++}for(;x<=T&&x<=L;){const H=c[T],G=h[L]=A?ft(h[L]):Ze(h[L]);if(en(H,G))E(H,G,m,null,w,_,R,C,A);else break;T--,L--}if(x>T){if(x<=L){const H=L+1,G=HL)for(;x<=T;)Se(c[x],w,_,!0),x++;else{const H=x,G=x,ne=new Map;for(x=G;x<=L;x++){const Pe=h[x]=A?ft(h[x]):Ze(h[x]);Pe.key!=null&&ne.set(Pe.key,x)}let ee,ue=0;const he=L-G+1;let qe=!1,Je=0;const Xt=new Array(he);for(x=0;x=he){Se(Pe,w,_,!0);continue}let ze;if(Pe.key!=null)ze=ne.get(Pe.key);else for(ee=G;ee<=L;ee++)if(Xt[ee-G]===0&&en(Pe,h[ee])){ze=ee;break}ze===void 0?Se(Pe,w,_,!0):(Xt[ze-G]=x+1,ze>=Je?Je=ze:qe=!0,E(Pe,h[ze],m,null,w,_,R,C,A),ue++)}const lr=qe?uu(Xt):Nt;for(ee=lr.length-1,x=he-1;x>=0;x--){const Pe=G+x,ze=h[Pe],or=h[Pe+1],ur=Pe+1{const{el:_,type:R,transition:C,children:A,shapeFlag:x}=c;if(x&6){We(c.component.subTree,h,m,y);return}if(x&128){c.suspense.move(h,m,y);return}if(x&64){R.move(c,h,m,D);return}if(R===ge){s(_,h,m);for(let T=0;TC.enter(_),w));else{const{leave:T,delayLeave:L,afterLeave:H}=C,G=()=>{c.ctx.isUnmounted?r(_):s(_,h,m)},ne=()=>{const ee=_._isLeaving||!!_[ps];_._isLeaving&&_[ps](!0),C.persisted&&!ee?G():T(_,()=>{G(),H&&H()})};L?L(_,G,ne):ne()}else s(_,h,m)},Se=(c,h,m,y=!1,w=!1)=>{const{type:_,props:R,ref:C,children:A,dynamicChildren:x,shapeFlag:F,patchFlag:T,dirs:L,cacheIndex:H,memo:G}=c;if((T===-2||x&&x.hasOnce)&&(w=!1),C!=null&&(gt(),un(C,null,m,c,!0),mt()),H!=null&&(!c.ctx||c.ctx===h)&&(h.renderCache[H]=void 0),F&256){h.ctx.deactivate(c);return}const ne=F&1&&L,ee=!Wt(c);let ue;if(ee&&(ue=R&&R.onVnodeBeforeUnmount)&&Ye(ue,h,c),F&6)Ot(c.component,m,y);else{if(F&128){c.suspense.unmount(m,y);return}ne&&Tt(c,null,h,"beforeUnmount"),F&64?c.type.remove(c,h,m,D,y):x&&!x.hasOnce&&(_!==ge||T>0&&T&64)?Ne(x,h,m,!1,!0):(_===ge&&T&384||!w&&F&16)&&Ne(A,h,m),y&&kt(c)}const he=G!=null&&H==null;(ee&&(ue=R&&R.onVnodeUnmounted)||ne||he)&&Te(()=>{ue&&Ye(ue,h,c),ne&&Tt(c,null,h,"unmounted"),he&&(c.el=null)},m)},kt=c=>{const{type:h,el:m,anchor:y,transition:w}=c;if(h===ge){Ht(m,y);return}if(h===vs){N(c),w&&!w.persisted&&w.afterLeave&&w.afterLeave();return}const _=()=>{r(m),w&&!w.persisted&&w.afterLeave&&w.afterLeave()};if(c.shapeFlag&1&&w&&!w.persisted){const{leave:R,delayLeave:C}=w,A=()=>R(m,_);C?C(c.el,_,A):A()}else _()},Ht=(c,h)=>{let m;for(;c!==h;)m=p(c),r(c),c=m;r(h)},Ot=(c,h,m)=>{const{bum:y,scope:w,job:_,subTree:R,um:C,m:A,a:x}=c;Cr(A),Cr(x),y&&Nn(y),w.stop(),_?(_.flags|=8,Se(R,c,h,m)):c.vnode.el&&R&&(R.transition=c.vnode.transition,Se(R,c,h,m)),C&&Te(C,h),Te(()=>{c.isUnmounted=!0},h)},Ne=(c,h,m,y=!1,w=!1,_=0)=>{for(let R=_;R{if(c.shapeFlag&6)return b(c.component.subTree);if(c.shapeFlag&128)return c.suspense.next();const h=p(c.anchor||c.el),m=h&&h[wo];return m?p(m):h};let I=!1;const O=(c,h,m)=>{let y;c==null?h._vnode&&(Se(h._vnode,null,null,!0),y=h._vnode.component):E(h._vnode||null,c,h,null,null,null,m),h._vnode=c,I||(I=!0,gr(y),Ni(),I=!1)},D={p:E,um:Se,m:We,r:kt,mt:Qt,mc:$e,pc:J,pbc:Ke,n:b,o:e};return{render:O,hydrate:void 0,createApp:Go(O)}}function ms({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function Pt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function ou(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function nl(e,t,n=!1){const s=e.children,r=t.children;if(j(s)&&j(r))for(let i=0;i>1,e[n[o]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,l=n[i-1];i-- >0;)n[i]=l,l=t[l];return n}function sl(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:sl(t)}function Cr(e){if(e)for(let t=0;te.__isSuspense;function cu(e,t){t&&t.pendingBranch?j(e)?t.effects.push(...e):t.effects.push(e):vo(e)}const ge=Symbol.for("v-fgt"),is=Symbol.for("v-txt"),lt=Symbol.for("v-cmt"),vs=Symbol.for("v-stc"),pt=[];let Ie=null;function $(e=!1){pt.push(Ie=e?null:[])}function sr(){pt.pop(),Ie=pt[pt.length-1]||null}let mn=1;function Un(e,t=!1){mn+=e,e<0&&Ie&&t&&(Ie.hasOnce=!0)}function ll(e){return e.dynamicChildren=mn>0?Ie||Nt:null,sr(),mn>0&&Ie&&Ie.push(e),e}function q(e,t,n,s,r,i){return ll(v(e,t,n,s,r,i,!0))}function Lt(e,t,n,s,r){return ll(Ee(e,t,n,s,r,!0))}function vn(e){return e?e.__v_isVNode===!0:!1}function en(e,t){return e.type===t.type&&e.key===t.key}const ol=({key:e})=>e??null,Ln=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?oe(e)||xe(e)||U(e)?{i:me,r:e,k:t,f:!!n}:e:null);function v(e,t=null,n=null,s=0,r=null,i=e===ge?0:1,l=!1,o=!1){const u={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ol(t),ref:t&&Ln(t),scopeId:Di,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:me};return o?($n(u,n),i&128&&e.normalize(u)):n&&(u.shapeFlag|=oe(n)?8:16),mn>0&&!l&&Ie&&(u.patchFlag>0||i&6)&&u.patchFlag!==32&&Ie.push(u),u}const Ee=au;function au(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Lo)&&(e=lt),vn(e)){const o=Jt(e,t,!0);return n&&$n(o,n),mn>0&&!i&&Ie&&(o.shapeFlag&6?Ie[Ie.indexOf(e)]=o:Ie.push(o)),o.patchFlag=-2,o}if(Eu(e)&&(e=e.__vccOpts),t){t=fu(t);let{class:o,style:u}=t;o&&!oe(o)&&(t.class=ke(o)),X(u)&&(Qs(u)&&!j(u)&&(u=ve({},u)),t.style=En(u))}const l=oe(e)?1:il(e)?128:ts(e)?64:X(e)?4:U(e)?2:0;return v(e,t,n,s,r,l,i,!0)}function fu(e){return e?Qs(e)||Yi(e)?ve({},e):e:null}function Jt(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:l,children:o,transition:u}=e,d=t?du(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:d,key:d&&ol(d),ref:t&&t.ref?n&&i?j(i)?i.concat(Ln(t)):[i,Ln(t)]:Ln(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ge?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:u,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Jt(e.ssContent),ssFallback:e.ssFallback&&Jt(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce,cacheIndex:e.cacheIndex};return u&&s&&Zs(a,u.clone(a)),a}function _e(e=" ",t=0){return Ee(is,null,e,t)}function be(e="",t=!1){return t?($(),Lt(lt,null,e)):Ee(lt,null,e)}function Ze(e){return e==null||typeof e=="boolean"?Ee(lt):j(e)?Ee(ge,null,e.slice()):vn(e)?ft(e):Ee(is,null,String(e))}function ft(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Jt(e)}function $n(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(j(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),$n(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!Yi(t)?t._ctx=me:r===3&&me&&(me.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else if(U(t)){if(s&65){$n(e,{default:t});return}t={default:t,_ctx:me},n=32}else t=String(t),s&64?(n=16,t=[_e(t)]):n=8;e.children=t,e.shapeFlag|=n}function du(...e){const t={};for(let n=0;nwe||me;let Kn,_n;{const e=Yn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(l=>l(i)):r[0](i)}};Kn=t("__VUE_INSTANCE_SETTERS__",n=>we=n),_n=t("__VUE_SSR_SETTERS__",n=>yn=n)}const An=e=>{const t=we;return Kn(e),e.scope.on(),()=>{e.scope.off(),Kn(t)}},Rr=()=>{we&&we.scope.off(),Kn(null)};function ul(e){return e.vnode.shapeFlag&4}let yn=!1;function vu(e,t=!1,n=!1){t&&_n(t);const{props:s,children:r}=e.vnode,i=ul(e);Zo(e,s,i,t),su(e,r,n||t);const l=i?_u(e,t):void 0;return t&&_n(!1),l}function _u(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,ko);const{setup:s}=n;if(s){gt();const r=e.setupContext=s.length>1?bu(e):null,i=An(e),l=xn(s,e,0,[e.props,r]),o=ii(l);if(mt(),i(),(o||e.sp)&&!Wt(e)&&Hi(e),o){if(l.then(Rr,Rr),t)return l.then(u=>{_n(!0);try{Or(e,u,t)}finally{_n(!1)}}).catch(u=>{es(u,e,0)});e.asyncDep=l}else Or(e,l)}else cl(e)}function Or(e,t,n){U(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:X(t)&&(e.setupState=Oi(t)),cl(e)}function cl(e,t,n){const s=e.type;e.render||(e.render=s.render||st);{const r=An(e);gt();try{Ho(e)}finally{mt(),r()}}}const yu={get(e,t){return ye(e,"get",""),e[t]}};function bu(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,yu),slots:e.slots,emit:e.emit,expose:t}}function ls(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Oi(lo(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in an)return an[n](e)},has(t,n){return n in t||n in an}})):e.proxy}function wu(e,t=!0){return U(e)?e.displayName||e.name:e.name||t&&e.__name}function Eu(e){return U(e)&&"__vccOpts"in e}const Fe=(e,t)=>fo(e,t,yn);function al(e,t,n){try{Un(-1);const s=arguments.length;return s===2?X(t)&&!j(t)?vn(t)?Ee(e,null,[t]):Ee(e,t):Ee(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&vn(n)&&(n=[n]),Ee(e,t,n))}finally{Un(1)}}const xu="3.5.43";/** +* @vue/runtime-dom v3.5.43 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Ds;const Tr=typeof window<"u"&&window.trustedTypes;if(Tr)try{Ds=Tr.createPolicy("vue",{createHTML:e=>e})}catch{}const fl=Ds?e=>Ds.createHTML(e):e=>e,Su="http://www.w3.org/2000/svg",Au="http://www.w3.org/1998/Math/MathML",at=typeof document<"u"?document:null,Pr=at&&at.createElement("template"),Cu={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?at.createElementNS(Su,e):t==="mathml"?at.createElementNS(Au,e):n?at.createElement(e,{is:n}):at.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>at.createTextNode(e),createComment:e=>at.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>at.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const l=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{Pr.innerHTML=fl(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const o=Pr.content;if(s==="svg"||s==="mathml"){const u=o.firstChild;for(;u.firstChild;)o.appendChild(u.firstChild);o.removeChild(u)}t.insertBefore(o,n)}return[l?l.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Ru=Symbol("_vtc");function Ou(e,t,n){const s=e[Ru];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Ir=Symbol("_vod"),Tu=Symbol("_vsh"),Pu=Symbol(""),Iu=/(?:^|;)\s*display\s*:/;function Nu(e,t,n){const s=e.style,r=oe(n);let i=!1;if(n&&!r){if(t)if(oe(t))for(const l of t.split(";")){const o=l.slice(0,l.indexOf(":")).trim();n[o]==null&&sn(s,o,"")}else for(const l in t)n[l]==null&&sn(s,l,"");for(const l in n){l==="display"&&(i=!0);const o=n[l];o!=null?Du(e,l,!oe(t)&&t?t[l]:void 0,o)||sn(s,l,o):sn(s,l,"")}}else if(r){if(t!==n){const l=s[Pu];l&&(n+=";"+l),s.cssText=n,i=Iu.test(n)}}else t&&e.removeAttribute("style");Ir in e&&(e[Ir]=i?s.display:"",e[Tu]&&(s.display="none"))}const Tn=/\s*!important$/;function sn(e,t,n){if(j(n))n.forEach(s=>sn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))Tn.test(n)?e.setProperty(t,n.replace(Tn,""),"important"):e.setProperty(t,n);else{const s=Mu(e,t);Tn.test(n)?e.setProperty(Vt(s),n.replace(Tn,""),"important"):e[s]=n}}const Nr=["Webkit","Moz","ms"],_s={};function Mu(e,t){const n=_s[t];if(n)return n;let s=Oe(t);if(s!=="filter"&&s in e)return _s[t]=s;s=zn(s);for(let r=0;rys||(ju.then(()=>ys=0),ys=Date.now());function Uu(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;const r=n.value;if(j(r)){const i=s.stopImmediatePropagation;s.stopImmediatePropagation=()=>{i.call(s),s._stopped=!0};const l=r.slice(),o=[s];for(let u=0;ue.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,$u=(e,t,n,s,r,i)=>{const l=r==="svg";t==="class"?Ou(e,s,l):t==="style"?Nu(e,n,s):Wn(t)?qn(t)||Vu(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Ku(e,t,s,l))?(Lr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Dr(e,t,s,l,i,t!=="value")):e._isVueCE&&(Gu(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!oe(s)))?Lr(e,Oe(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Dr(e,t,s,l))};function Ku(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Fr(t)&&U(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Fr(t)&&oe(n)?!1:t in e}function Gu(e,t){const n=e._def.props;if(!n)return!1;const s=Oe(t);return Array.isArray(n)?n.some(r=>Oe(r)===s):Object.keys(n).some(r=>Oe(r)===s)}const kr=e=>{const t=e.props["onUpdate:modelValue"]||!1;return j(t)?n=>Nn(t,n):t};function Wu(e){e.target.composing=!0}function Hr(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Pn=Symbol("_assign"),In=Symbol("_initialValue");function bs(e,t,n){return t&&(e=e.trim()),n&&(e=Ks(e)),e}const nt={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e.parentNode&&(e.type==="text"?e[In]=e.defaultValue.replace(/[\r\n]/g,""):e.type==="textarea"&&(e[In]=e.defaultValue.replace(/\r\n?/g,` +`))),e[Pn]=kr(r);const i=s||r.props&&r.props.type==="number";Ut(e,t?"change":"input",l=>{l.target.composing||e[Pn](bs(e.value,n,i))}),(n||i)&&Ut(e,"change",()=>{e.value=bs(e.value,n,i)}),t||(Ut(e,"compositionstart",Wu),Ut(e,"compositionend",Hr),Ut(e,"change",Hr))},mounted(e,{value:t,modifiers:{trim:n,number:s}}){const r=t??"",i=e[In];delete e[In],i!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==i?e[Pn](bs(e.value,n,s)):e.value=r},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:i}},l){if(e[Pn]=kr(l),e.composing)return;const o=(i||e.type==="number")&&!/^0\d/.test(e.value)?Ks(e.value):e.value,u=t??"";if(o===u)return;const d=e.getRootNode();(d instanceof Document||d instanceof ShadowRoot)&&d.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===u)||(e.value=u)}},qu=["ctrl","shift","alt","meta"],Ju={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>qu.some(n=>e[`${n}Key`]&&!t.includes(n))},Ls=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...i)=>{for(let l=0;l{const t=Yu().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Zu(s);if(!r)return;const i=t._component;!U(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const l=n(r,!1,Xu(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),l},t};function Xu(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Zu(e){return oe(e)?document.querySelector(e):e}const ec=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},tc={};function nc(e,t){const n=Bi("router-view");return $(),Lt(n)}const sc=ec(tc,[["render",nc]]);/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */const $t=typeof document<"u";function dl(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function rc(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&dl(e.default)}const z=Object.assign;function ws(e,t){const n={};for(const s in t){const r=t[s];n[s]=Ue(r)?r.map(e):e(r)}return n}const fn=()=>{},Ue=Array.isArray;function Br(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}const hl=/#/g,ic=/&/g,lc=/\//g,oc=/=/g,uc=/\?/g,pl=/\+/g,cc=/%5B/g,ac=/%5D/g,gl=/%5E/g,fc=/%60/g,ml=/%7B/g,dc=/%7C/g,vl=/%7D/g,hc=/%20/g;function rr(e){return e==null?"":encodeURI(""+e).replace(dc,"|").replace(cc,"[").replace(ac,"]")}function pc(e){return rr(e).replace(ml,"{").replace(vl,"}").replace(gl,"^")}function Vs(e){return rr(e).replace(pl,"%2B").replace(hc,"+").replace(hl,"%23").replace(ic,"%26").replace(fc,"`").replace(ml,"{").replace(vl,"}").replace(gl,"^")}function gc(e){return Vs(e).replace(oc,"%3D")}function mc(e){return rr(e).replace(hl,"%23").replace(uc,"%3F")}function vc(e){return mc(e).replace(lc,"%2F")}function bn(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const _c=/\/$/,yc=e=>e.replace(_c,"");function Es(e,t,n="/"){let s,r={},i="",l="";const o=t.indexOf("#");let u=t.indexOf("?");return u=o>=0&&u>o?-1:u,u>=0&&(s=t.slice(0,u),i=t.slice(u,o>0?o:t.length),r=e(i.slice(1))),o>=0&&(s=s||t.slice(0,o),l=t.slice(o,t.length)),s=xc(s??t,n),{fullPath:s+i+l,path:s,query:r,hash:bn(l)}}function bc(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Ur(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function wc(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&zt(t.matched[s],n.matched[r])&&_l(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function zt(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function _l(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Ec(e[n],t[n]))return!1;return!0}function Ec(e,t){return Ue(e)?$r(e,t):Ue(t)?$r(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function $r(e,t){return Ue(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function xc(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),r=s[s.length-1];(r===".."||r===".")&&s.push("");let i=n.length-1,l,o;for(l=0;l1&&i--;else break;return n.slice(0,i).join("/")+"/"+s.slice(l).join("/")}const wt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let Fs=function(e){return e.pop="pop",e.push="push",e}({}),xs=function(e){return e.back="back",e.forward="forward",e.unknown="",e}({});function Sc(e){if(!e)if($t){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),yc(e)}const Ac=/^[^#]+#/;function Cc(e,t){return e.replace(Ac,"#")+t}function Rc(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const os=()=>({left:window.scrollX,top:window.scrollY});function Oc(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=Rc(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function Kr(e,t){return(history.state?history.state.position-t:-1)+e}const ks=new Map;function Tc(e,t){ks.set(e,t)}function Pc(e){const t=ks.get(e);return ks.delete(e),t}function Ic(e){return typeof e=="string"||e&&typeof e=="object"}function yl(e){return typeof e=="string"||typeof e=="symbol"}let le=function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e}({});const bl=Symbol("");le.MATCHER_NOT_FOUND+"",le.NAVIGATION_GUARD_REDIRECT+"",le.NAVIGATION_ABORTED+"",le.NAVIGATION_CANCELLED+"",le.NAVIGATION_DUPLICATED+"";function Yt(e,t){return z(new Error,{type:e,[bl]:!0},t)}function ct(e,t){return e instanceof Error&&bl in e&&(t==null||!!(e.type&t))}const Nc=["params","query","hash"];function Mc(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const n of Nc)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function Dc(e){const t={};if(e===""||e==="?")return t;const n=(e[0]==="?"?e.slice(1):e).split("&");for(let s=0;sr&&Vs(r)):[s&&Vs(s)]).forEach(r=>{r!==void 0&&(t+=(t.length?"&":"")+n,r!=null&&(t+="="+r))})}return t}function Lc(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=Ue(s)?s.map(r=>r==null?null:""+r):s==null?s:""+s)}return t}const Vc=Symbol(""),Wr=Symbol(""),us=Symbol(""),wl=Symbol(""),Hs=Symbol("");function tn(){let e=[];function t(s){return e.push(s),()=>{const r=e.indexOf(s);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function xt(e,t,n,s,r,i=l=>l()){const l=s&&(s.enterCallbacks[r]=s.enterCallbacks[r]||[]);return()=>new Promise((o,u)=>{const d=p=>{p===!1?u(Yt(le.NAVIGATION_ABORTED,{from:n,to:t})):p instanceof Error?u(p):Ic(p)?u(Yt(le.NAVIGATION_GUARD_REDIRECT,{from:t,to:p})):(l&&s.enterCallbacks[r]===l&&typeof p=="function"&&l.push(p),o())},a=i(()=>e.call(s&&s.instances[r],t,n,d));let f=Promise.resolve(a);e.length<3&&(f=f.then(d)),f.catch(p=>u(p))})}function Ss(e,t,n,s,r=i=>i()){const i=[];for(const l of e)for(const o in l.components){let u=l.components[o];if(!(t!=="beforeRouteEnter"&&!l.instances[o]))if(dl(u)){const d=(u.__vccOpts||u)[t];d&&i.push(xt(d,n,s,l,o,r))}else{let d=u();i.push(()=>d.then(a=>{if(!a)throw new Error(`Couldn't resolve component "${o}" at "${l.path}"`);const f=rc(a)?a.default:a;l.mods[o]=a,l.components[o]=f;const p=(f.__vccOpts||f)[t];return p&&xt(p,n,s,l,o,r)()}))}}return i}function Fc(e,t){const n=[],s=[],r=[],i=Math.max(t.matched.length,e.matched.length);for(let l=0;lzt(d,o))?s.push(o):n.push(o));const u=e.matched[l];u&&(t.matched.find(d=>zt(d,u))||r.push(u))}return[n,s,r]}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */let kc=()=>location.protocol+"//"+location.host;function El(e,t){const{pathname:n,search:s,hash:r}=t,i=e.indexOf("#");if(i>-1){let l=r.includes(e.slice(i))?e.slice(i).length:1,o=r.slice(l);return o[0]!=="/"&&(o="/"+o),Ur(o,"")}return Ur(n,e)+s+r}function Hc(e,t,n,s){let r=[],i=[],l=null;const o=({state:p})=>{const g=El(e,location),S=n.value,E=t.value;let B=0;if(p){if(n.value=g,t.value=p,l&&l===S){l=null;return}B=E?p.position-E.position:0}else s(g);r.forEach(k=>{k(n.value,S,{delta:B,type:Fs.pop,direction:B?B>0?xs.forward:xs.back:xs.unknown})})};function u(){l=n.value}function d(p){r.push(p);const g=()=>{const S=r.indexOf(p);S>-1&&r.splice(S,1)};return i.push(g),g}function a(){if(document.visibilityState==="hidden"){const{history:p}=window;if(!p.state)return;p.replaceState(z({},p.state,{scroll:os()}),"")}}function f(){for(const p of i)p();i=[],window.removeEventListener("popstate",o),window.removeEventListener("pagehide",a),document.removeEventListener("visibilitychange",a)}return window.addEventListener("popstate",o),window.addEventListener("pagehide",a),document.addEventListener("visibilitychange",a),{pauseListeners:u,listen:d,destroy:f}}function qr(e,t,n,s=!1,r=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:r?os():null}}function jc(e){const{history:t,location:n}=window,s={value:El(e,n)},r={value:t.state};r.value||i(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(u,d,a){const f=e.indexOf("#"),p=f>-1?(n.host&&document.querySelector("base")?e:e.slice(f))+u:kc()+e+u;try{t[a?"replaceState":"pushState"](d,"",p),r.value=d}catch(g){console.error(g),n[a?"replace":"assign"](p)}}function l(u,d){i(u,z({},t.state,qr(r.value.back,u,r.value.forward,!0),d,{position:r.value.position}),!0),s.value=u}function o(u,d){const a=z({},r.value,t.state,{forward:u,scroll:os()});i(a.current,a,!0),i(u,z({},qr(s.value,u,null),{position:a.position+1},d),!1),s.value=u}return{location:s,state:r,push:o,replace:l}}function Bc(e){e=Sc(e);const t=jc(e),n=Hc(e,t.state,t.location,t.replace);function s(i,l=!0){l||n.pauseListeners(),history.go(i)}const r=z({location:"",base:e,go:s,createHref:Cc.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}let Mt=function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e}({});var ae=function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e}(ae||{});const Uc={type:Mt.Static,value:""},$c=/[a-zA-Z0-9_]/;function Kc(e){if(!e)return[[]];if(e==="/")return[[Uc]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(g){throw new Error(`ERR (${n})/"${d}": ${g}`)}let n=ae.Static,s=n;const r=[];let i;function l(){i&&r.push(i),i=[]}let o=0,u,d="",a="";function f(){d&&(n===ae.Static?i.push({type:Mt.Static,value:d}):n===ae.Param||n===ae.ParamRegExp||n===ae.ParamRegExpEnd?(i.length>1&&(u==="*"||u==="+")&&t(`A repeatable param (${d}) must be alone in its segment. eg: '/:ids+.`),i.push({type:Mt.Param,value:d,regexp:a,repeatable:u==="*"||u==="+",optional:u==="*"||u==="?"})):t("Invalid state to consume buffer"),d="")}function p(){d+=u}for(;ot.length?t.length===1&&t[0]===Ce.Static+Ce.Segment?1:-1:0}function xl(e,t){let n=0;const s=e.score,r=t.score;for(;n0&&t[t.length-1]<0}const zc={strict:!1,end:!0,sensitive:!1};function Yc(e,t,n){const s=qc(Kc(e.path),n),r=z(s,{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function Qc(e,t){const n=[],s=new Map;t=Br(zc,t);function r(f){return s.get(f)}function i(f,p,g){const S=!g,E=Qr(f);E.aliasOf=g&&g.record;const B=Br(t,f),k=[E];if("alias"in f){const N=typeof f.alias=="string"?[f.alias]:f.alias;for(const te of N)k.push(Qr(z({},E,{components:g?g.record.components:E.components,path:te,aliasOf:g?g.record:E})))}let P,M;for(const N of k){const{path:te}=N;if(p&&te[0]!=="/"){const ce=p.record.path,ie=ce[ce.length-1]==="/"?"":"/";N.path=p.record.path+(te&&ie+te)}if(P=Yc(N,p,B),g?g.alias.push(P):(M=M||P,M!==P&&M.alias.push(P),S&&f.name&&!Xr(P)&&l(f.name)),Sl(P)&&u(P),E.children){const ce=E.children;for(let ie=0;ie{l(M)}:fn}function l(f){if(yl(f)){const p=s.get(f);p&&(s.delete(f),n.splice(n.indexOf(p),1),p.children.forEach(l),p.alias.forEach(l))}else{const p=n.indexOf(f);p>-1&&(n.splice(p,1),f.record.name&&s.delete(f.record.name),f.children.forEach(l),f.alias.forEach(l))}}function o(){return n}function u(f){const p=ea(f,n);n.splice(p,0,f),f.record.name&&!Xr(f)&&s.set(f.record.name,f)}function d(f,p){let g,S={},E,B;if("name"in f&&f.name){if(g=s.get(f.name),!g)throw Yt(le.MATCHER_NOT_FOUND,{location:f});B=g.record.name,S=z(Yr(p.params,g.keys.filter(M=>!M.optional).concat(g.parent?g.parent.keys.filter(M=>M.optional):[]).map(M=>M.name)),f.params&&Yr(f.params,g.keys.map(M=>M.name))),E=g.stringify(S)}else if(f.path!=null)E=f.path,g=n.find(M=>M.re.test(E)),g&&(S=g.parse(E),B=g.record.name);else{if(g=p.name?s.get(p.name):n.find(M=>M.re.test(p.path)),!g)throw Yt(le.MATCHER_NOT_FOUND,{location:f,currentLocation:p});B=g.record.name,S=z({},p.params,f.params),E=g.stringify(S)}const k=[];let P=g;for(;P;)k.unshift(P.record),P=P.parent;return{name:B,path:E,params:S,matched:k,meta:Zc(k)}}e.forEach(f=>i(f));function a(){n.length=0,s.clear()}return{addRoute:i,resolve:d,removeRoute:l,clearRoutes:a,getRoutes:o,getRecordMatcher:r}}function Yr(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function Qr(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Xc(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function Xc(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="object"?n[s]:n;return t}function Xr(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Zc(e){return e.reduce((t,n)=>z(t,n.meta),{})}function ea(e,t){let n=0,s=t.length;for(;n!==s;){const i=n+s>>1;xl(e,t[i])<0?s=i:n=i+1}const r=ta(e);return r&&(s=t.lastIndexOf(r,s-1)),s}function ta(e){let t=e;for(;t=t.parent;)if(Sl(t)&&xl(e,t)===0)return t}function Sl({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Zr(e){const t=rt(us),n=rt(wl),s=Fe(()=>{const u=W(e.to);return t.resolve(u)}),r=Fe(()=>{const{matched:u}=s.value,{length:d}=u,a=u[d-1],f=n.matched;if(!a||!f.length)return-1;const p=f.findIndex(zt.bind(null,a));if(p>-1)return p;const g=ei(u[d-2]);return d>1&&ei(a)===g&&f[f.length-1].path!==g?f.findIndex(zt.bind(null,u[d-2])):p}),i=Fe(()=>r.value>-1&&la(n.params,s.value.params)),l=Fe(()=>r.value>-1&&r.value===n.matched.length-1&&_l(n.params,s.value.params));function o(u={}){if(ia(u)){const d=t[W(e.replace)?"replace":"push"](W(e.to)).catch(fn);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>d),d}return Promise.resolve()}return{route:s,href:Fe(()=>s.value.href),isActive:i,isExactActive:l,navigate:o}}function na(e){return e.length===1?e[0]:e}const sa=ki({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:Zr,setup(e,{slots:t}){const n=Zn(Zr(e)),{options:s}=rt(us),r=Fe(()=>({[ti(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[ti(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const i=t.default&&na(t.default(n));return e.custom?i:al("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},i)}}}),ra=sa;function ia(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function la(e,t){for(const n in t){const s=t[n],r=e[n];if(typeof s=="string"){if(s!==r)return!1}else if(!Ue(r)||r.length!==s.length||s.some((i,l)=>i.valueOf()!==r[l].valueOf()))return!1}return!0}function ei(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const ti=(e,t,n)=>e??t??n,oa=ki({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=rt(Hs),r=Fe(()=>e.route||s.value),i=rt(Wr,0),l=Fe(()=>{let d=W(i);const{matched:a}=r.value;let f;for(;(f=a[d])&&!f.components;)d++;return d}),o=Fe(()=>r.value.matched[l.value]);Mn(Wr,Fe(()=>l.value+1)),Mn(Vc,o),Mn(Hs,r);const u=fe();return Dn(()=>[u.value,o.value,e.name],([d,a,f],[p,g,S])=>{a&&(a.instances[f]=d,g&&g!==a&&d&&d===p&&(a.leaveGuards.size||(a.leaveGuards=g.leaveGuards),a.updateGuards.size||(a.updateGuards=g.updateGuards))),d&&a&&(!g||!zt(a,g)||!p)&&(a.enterCallbacks[f]||[]).forEach(E=>E(d))},{flush:"post"}),()=>{const d=r.value,a=e.name,f=o.value,p=f&&f.components[a];if(!p)return ni(n.default,{Component:p,route:d});const g=f.props[a],S=g?g===!0?d.params:typeof g=="function"?g(d):g:null,B=al(p,z({},S,t,{onVnodeUnmounted:k=>{k.component.isUnmounted&&(f.instances[a]=null)},ref:u}));return ni(n.default,{Component:B,route:d})||B}}});function ni(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const ua=oa;function ca(e){const t=Qc(e.routes,e),n=e.parseQuery||Dc,s=e.stringifyQuery||Gr,r=e.history,i=tn(),l=tn(),o=tn(),u=oo(wt);let d=wt;$t&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const a=ws.bind(null,b=>""+b),f=ws.bind(null,vc),p=ws.bind(null,bn);function g(b,I){let O,D;return yl(b)?(O=t.getRecordMatcher(b),D=I):D=b,t.addRoute(D,O)}function S(b){const I=t.getRecordMatcher(b);I&&t.removeRoute(I)}function E(){return t.getRoutes().map(b=>b.record)}function B(b){return!!t.getRecordMatcher(b)}function k(b,I){if(I=z({},I||u.value),typeof b=="string"){const m=Es(n,b,I.path),y=t.resolve({path:m.path},I),w=r.createHref(m.fullPath);return z(m,y,{params:p(y.params),hash:bn(m.hash),redirectedFrom:void 0,href:w})}let O;if(b.path!=null)O=z({},b,{path:Es(n,b.path,I.path).path});else{const m=z({},b.params);for(const y in m)m[y]==null&&delete m[y];O=z({},b,{params:f(m)}),I.params=f(I.params)}const D=t.resolve(O,I),K=b.hash||"";D.params=a(p(D.params));const c=bc(s,z({},b,{hash:pc(K),path:D.path})),h=r.createHref(c);return z({fullPath:c,hash:K,query:s===Gr?Lc(b.query):b.query||{}},D,{redirectedFrom:void 0,href:h})}function P(b){return typeof b=="string"?Es(n,b,u.value.path):z({},b)}function M(b,I){if(d!==b)return Yt(le.NAVIGATION_CANCELLED,{from:I,to:b})}function N(b){return ie(b)}function te(b){return N(z(P(b),{replace:!0}))}function ce(b,I){const O=b.matched[b.matched.length-1];if(O&&O.redirect){const{redirect:D}=O;let K=typeof D=="function"?D(b,I):D;return typeof K=="string"&&(K=K.includes("?")||K.includes("#")?K=P(K):{path:K},K.params={}),z({query:b.query,hash:b.hash,params:K.path!=null?{}:b.params},K)}}function ie(b,I){const O=d=k(b),D=u.value,K=b.state,c=b.force,h=b.replace===!0,m=ce(O,D);if(m)return ie(z(P(m),{state:typeof m=="object"?z({},K,m.state):K,force:c,replace:h}),I||O);const y=O;y.redirectedFrom=I;let w;return!c&&wc(s,D,O)&&(w=Yt(le.NAVIGATION_DUPLICATED,{to:y,from:D}),We(D,D,!0,!1)),(w?Promise.resolve(w):Ke(y,D)).catch(_=>ct(_)?ct(_,le.NAVIGATION_GUARD_REDIRECT)?_:bt(_):J(_,y,D)).then(_=>{if(_){if(ct(_,le.NAVIGATION_GUARD_REDIRECT))return ie(z({replace:h},P(_.to),{state:typeof _.to=="object"?z({},K,_.to.state):K,force:c}),I||y)}else _=Rt(y,D,!0,h,K);return yt(y,D,_),_})}function $e(b,I){const O=M(b,I);return O?Promise.reject(O):Promise.resolve()}function _t(b){const I=Ht.values().next().value;return I&&typeof I.runWithContext=="function"?I.runWithContext(b):b()}function Ke(b,I){let O;const[D,K,c]=Fc(b,I);O=Ss(D.reverse(),"beforeRouteLeave",b,I);for(const m of D)m.leaveGuards.forEach(y=>{O.push(xt(y,b,I))});const h=$e.bind(null,b,I);return O.push(h),Ne(O).then(()=>{O=[];for(const m of i.list())O.push(xt(m,b,I));return O.push(h),Ne(O)}).then(()=>{O=Ss(K,"beforeRouteUpdate",b,I);for(const m of K)m.updateGuards.forEach(y=>{O.push(xt(y,b,I))});return O.push(h),Ne(O)}).then(()=>{O=[];for(const m of c)if(m.beforeEnter)if(Ue(m.beforeEnter))for(const y of m.beforeEnter)O.push(xt(y,b,I));else O.push(xt(m.beforeEnter,b,I));return O.push(h),Ne(O)}).then(()=>(b.matched.forEach(m=>m.enterCallbacks={}),O=Ss(c,"beforeRouteEnter",b,I,_t),O.push(h),Ne(O))).then(()=>{O=[];for(const m of l.list())O.push(xt(m,b,I));return O.push(h),Ne(O)}).catch(m=>ct(m,le.NAVIGATION_CANCELLED)?m:Promise.reject(m))}function yt(b,I,O){o.list().forEach(D=>_t(()=>D(b,I,O)))}function Rt(b,I,O,D,K){const c=M(b,I);if(c)return c;const h=I===wt,m=$t?history.state:{};O&&(D||h?r.replace(b.fullPath,z({scroll:h&&m&&m.scroll},K)):r.push(b.fullPath,K)),u.value=b,We(b,I,O,h),bt()}let Ge;function Qt(){Ge||(Ge=r.listen((b,I,O)=>{if(!Ot.listening)return;const D=k(b),K=ce(D,Ot.currentRoute.value);if(K){ie(z(K,{replace:!0,force:!0}),D).catch(fn);return}d=D;const c=u.value;$t&&Tc(Kr(c.fullPath,O.delta),os()),Ke(D,c).catch(h=>ct(h,le.NAVIGATION_ABORTED|le.NAVIGATION_CANCELLED)?h:ct(h,le.NAVIGATION_GUARD_REDIRECT)?(ie(z(P(h.to),{force:!0}),D).then(m=>{ct(m,le.NAVIGATION_ABORTED|le.NAVIGATION_DUPLICATED)&&!O.delta&&O.type===Fs.pop&&r.go(-1,!1)}).catch(fn),Promise.reject()):(O.delta&&r.go(-O.delta,!1),J(h,D,c))).then(h=>{h=h||Rt(D,c,!1),h&&(O.delta&&!ct(h,le.NAVIGATION_CANCELLED)?r.go(-O.delta,!1):O.type===Fs.pop&&ct(h,le.NAVIGATION_ABORTED|le.NAVIGATION_DUPLICATED)&&r.go(-1,!1)),yt(D,c,h)}).catch(fn)}))}let Ft=tn(),de=tn(),Z;function J(b,I,O){bt(b);const D=de.list();return D.length?D.forEach(K=>K(b,I,O)):console.error(b),Promise.reject(b)}function ot(){return Z&&u.value!==wt?Promise.resolve():new Promise((b,I)=>{Ft.add([b,I])})}function bt(b){return Z||(Z=!b,Qt(),Ft.list().forEach(([I,O])=>b?O(b):I()),Ft.reset()),b}function We(b,I,O,D){const{scrollBehavior:K}=e;if(!$t||!K)return Promise.resolve();const c=!O&&Pc(Kr(b.fullPath,0))||(D||!O)&&history.state&&history.state.scroll||null;return Pi().then(()=>K(b,I,c)).then(h=>h&&Oc(h)).catch(h=>J(h,b,I))}const Se=b=>r.go(b);let kt;const Ht=new Set,Ot={currentRoute:u,listening:!0,addRoute:g,removeRoute:S,clearRoutes:t.clearRoutes,hasRoute:B,getRoutes:E,resolve:k,options:e,push:N,replace:te,go:Se,back:()=>Se(-1),forward:()=>Se(1),beforeEach:i.add,beforeResolve:l.add,afterEach:o.add,onError:de.add,isReady:ot,install(b){b.component("RouterLink",ra),b.component("RouterView",ua),b.config.globalProperties.$router=Ot,Object.defineProperty(b.config.globalProperties,"$route",{enumerable:!0,get:()=>W(u)}),$t&&!kt&&u.value===wt&&(kt=!0,N(r.location).catch(D=>{}));const I={};for(const D in wt)Object.defineProperty(I,D,{get:()=>u.value[D],enumerable:!0});b.provide(us,Ot),b.provide(wl,Ci(I)),b.provide(Hs,u);const O=b.unmount;Ht.add(b),b.unmount=function(){Ht.delete(b),Ht.size<1&&(d=wt,Ge&&Ge(),Ge=null,u.value=wt,kt=!1,Z=!1),O()}}};function Ne(b){return b.reduce((I,O)=>I.then(()=>_t(O)),Promise.resolve())}return Ot}function Al(){return rt(us)}const aa=["token","role","wallet","username"];function ir(){return{token:localStorage.getItem("token")||"",role:localStorage.getItem("role")||"",wallet:localStorage.getItem("wallet")||"",username:localStorage.getItem("username")||""}}function fa(e){localStorage.setItem("token",e.access_token),localStorage.setItem("role",e.role),localStorage.setItem("wallet",e.wallet_address),localStorage.setItem("username",e.username||"")}function Cl(){aa.forEach(e=>localStorage.removeItem(e))}async function Me(e,t={}){const n={...t.headers||{}};t.body&&!n["Content-Type"]&&(n["Content-Type"]="application/json");const s=localStorage.getItem("token");s&&(n.Authorization=`Bearer ${s}`);const r=await fetch(`/api/v1${e}`,{...t,headers:n}),i=await r.text(),l=i?JSON.parse(i):null;if(r.status===401)throw Cl(),location.pathname!=="/login"&&location.assign("/login"),new Error(si(l)||"登录已失效");if(!r.ok)throw new Error(si(l)||"请求失败");return l}function si(e){return e?typeof e.detail=="string"?e.detail:Array.isArray(e.detail)?e.detail.map(t=>t.msg).filter(Boolean).join(";"):"":""}const da={class:"gate"},ha={class:"switch"},pa={key:0,class:"switch slim"},ga={key:1},ma={key:2},va={key:3},_a={key:4},ya={key:5,class:"form-error"},ba=["disabled"],wa={__name:"Login",setup(e){const t=Al(),n=fe("login"),s=fe("wallet"),r=fe(""),i=fe(!1),l=fe({wallet_address:"",username:"",password:"",confirm:""});async function o(){if(r.value="",n.value==="register"&&l.value.password!==l.value.confirm){r.value="两次密码不一致";return}i.value=!0;try{const a=n.value==="register"?await d():await u();fa(a),t.push(a.role==="admin"?"/admin":"/dashboard")}catch(a){r.value=a.message}finally{i.value=!1}}function u(){return s.value==="wallet"?Me("/auth/login",{method:"POST",body:JSON.stringify({wallet_address:l.value.wallet_address.trim()})}):Me("/auth/login",{method:"POST",body:JSON.stringify({username:l.value.username.trim(),password:l.value.password})})}function d(){return Me("/auth/register",{method:"POST",body:JSON.stringify({wallet_address:l.value.wallet_address.trim(),username:l.value.username.trim(),password:l.value.password})})}return(a,f)=>($(),q("div",da,[f[13]||(f[13]=v("section",{class:"gate-copy"},[v("p",{class:"eyebrow"},"HyperNode"),v("h1",null,"算力在这里入账。"),v("p",null,"钱包或账号进入自己的节点。管理员从这里审核提现、看全网在线。")],-1)),v("form",{class:"gate-card",onSubmit:Ls(o,["prevent"])},[v("div",ha,[v("button",{type:"button",class:ke({on:n.value==="login"}),onClick:f[0]||(f[0]=p=>n.value="login")},"登录",2),v("button",{type:"button",class:ke({on:n.value==="register"}),onClick:f[1]||(f[1]=p=>n.value="register")},"注册",2)]),n.value==="login"?($(),q("div",pa,[v("button",{type:"button",class:ke({on:s.value==="wallet"}),onClick:f[2]||(f[2]=p=>s.value="wallet")},"钱包地址",2),v("button",{type:"button",class:ke({on:s.value==="account"}),onClick:f[3]||(f[3]=p=>s.value="account")},"账号密码",2)])):be("",!0),n.value==="register"||s.value==="wallet"?($(),q("label",ga,[f[8]||(f[8]=_e(" 钱包地址 ",-1)),tt(v("input",{"onUpdate:modelValue":f[4]||(f[4]=p=>l.value.wallet_address=p),required:"",minlength:"4",placeholder:"0xabc123",autocomplete:"username"},null,512),[[nt,l.value.wallet_address]])])):be("",!0),n.value==="register"||s.value==="account"?($(),q("label",ma,[f[9]||(f[9]=_e(" 用户名 ",-1)),tt(v("input",{"onUpdate:modelValue":f[5]||(f[5]=p=>l.value.username=p),required:"",minlength:"3",maxlength:"32",placeholder:"miner",autocomplete:"username"},null,512),[[nt,l.value.username]])])):be("",!0),n.value==="register"||s.value==="account"?($(),q("label",va,[f[10]||(f[10]=_e(" 密码 ",-1)),tt(v("input",{"onUpdate:modelValue":f[6]||(f[6]=p=>l.value.password=p),required:"",minlength:"6",type:"password",autocomplete:"current-password"},null,512),[[nt,l.value.password]])])):be("",!0),n.value==="register"?($(),q("label",_a,[f[11]||(f[11]=_e(" 确认密码 ",-1)),tt(v("input",{"onUpdate:modelValue":f[7]||(f[7]=p=>l.value.confirm=p),required:"",minlength:"6",type:"password",autocomplete:"new-password"},null,512),[[nt,l.value.confirm]])])):be("",!0),r.value?($(),q("p",ya,V(r.value),1)):be("",!0),v("button",{class:"gold wide",type:"submit",disabled:i.value},V(i.value?"请稍候":n.value==="login"?"进入":"创建账号"),9,ba),f[12]||(f[12]=v("p",{class:"hint"},"管理员不能只用钱包进入,需要账号密码。",-1))],32)]))}},Ea={class:"shell"},xa={class:"rail"},Sa={class:"nav"},Aa={class:"rail-foot"},Ca={class:"who"},Ra={class:"stage"},Oa={class:"top"},Ta={class:"eyebrow"},Pa={class:"clock"},Ia={class:"view"},Rl={__name:"Shell",props:{eyebrow:{type:String,default:"控制面"},title:{type:String,required:!0}},setup(e){const t=Al(),n=ir(),s=fe("--:--:--");let r=0;function i(){s.value=new Intl.DateTimeFormat("zh-CN",{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1,timeZone:"Asia/Shanghai"}).format(new Date)}function l(){Cl(),t.push("/login")}return ss(()=>{i(),r=window.setInterval(i,1e3)}),Sn(()=>window.clearInterval(r)),(o,u)=>{const d=Bi("router-link");return $(),q("div",Ea,[v("aside",xa,[Ee(d,{class:"brand",to:"/"},{default:Gt(()=>[...u[0]||(u[0]=[v("span",{class:"mark","aria-hidden":"true"},[v("svg",{viewBox:"0 0 32 32",width:"28",height:"28"},[v("path",{d:"M16 3 L27 9.5 V22.5 L16 29 L5 22.5 V9.5 Z",fill:"none",stroke:"currentColor","stroke-width":"1.4"}),v("circle",{cx:"16",cy:"16",r:"2.2",fill:"currentColor"})])],-1),v("span",null,[v("strong",null,"HyperNode"),v("em",null,"Compute Atelier")],-1)])]),_:1}),v("nav",Sa,[Ee(d,{to:"/dashboard"},{default:Gt(()=>[...u[1]||(u[1]=[_e("我的节点",-1)])]),_:1}),W(n).role==="admin"?($(),Lt(d,{key:0,to:"/admin"},{default:Gt(()=>[...u[2]||(u[2]=[_e("管理后台",-1)])]),_:1})):be("",!0)]),v("div",Aa,[v("span",Ca,V(W(n).username||W(n).wallet),1),v("button",{type:"button",class:"texty",onClick:l},"退出")])]),v("div",Ra,[v("header",Oa,[v("div",null,[v("p",Ta,V(e.eyebrow),1),v("h1",null,V(e.title),1)]),v("div",Pa,[u[3]||(u[3]=v("span",null,"北京时间",-1)),v("strong",null,V(s.value),1)])]),v("main",Ia,[Fo(o.$slots,"default")])])])}}};function De(e){const[t,n=""]=String(e??"0").split("."),s=t.replace(/\B(?=(\d{3})+(?!\d))/g,","),r=n.replace(/0+$/,"");return r?`${s}.${r}`:s}function Gn(e){const t=Number(e)*100;return Number.isFinite(t)?`${t.toFixed(t>=10?0:1)}%`:"0%"}function js(e){if(!e)return"—";const t=new Date(e);return Number.isNaN(t.getTime())?"—":new Intl.DateTimeFormat("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}).format(t)}const dn={online:"在线",offline:"离线",pending:"待审核",completed:"已打款",rejected:"已拒绝",failed:"失败",settled:"已入账",user:"用户",admin:"管理员"},Na={key:0,class:"form-error banner"},Ma={key:1,class:"banner ok"},Da={key:2,class:"metrics"},La={class:"metric"},Va={class:"metric"},Fa={class:"metric"},ka={class:"board"},Ha={class:"rate"},ja={key:0,class:"empty"},Ba={class:"forms"},Ua={class:"mini"},$a={__name:"Dashboard",setup(e){const t=fe(null),n=fe(""),s=fe(""),r=fe({sn:"",purchase_amount:"",callback_url:""}),i=fe({amount:"",asset_symbol:"POINT"});let l=0;async function o(){t.value=await Me("/me/dashboard")}async function u(f,p){n.value="",s.value="";try{await f(),s.value=p,await o()}catch(g){n.value=g.message}}function d(){const f={sn:r.value.sn.trim(),purchase_amount:r.value.purchase_amount};return r.value.callback_url.trim()&&(f.callback_url=r.value.callback_url.trim()),u(async()=>{await Me("/me/nodes",{method:"POST",body:JSON.stringify(f)}),r.value={sn:"",purchase_amount:"",callback_url:""}},"节点已登记")}function a(){return u(async()=>{await Me("/me/withdrawals",{method:"POST",body:JSON.stringify({amount:i.value.amount,asset_symbol:i.value.asset_symbol||"POINT"})}),i.value.amount=""},"提现申请已提交")}return ss(async()=>{try{await o()}catch(f){n.value=f.message}l=window.setInterval(()=>{o().catch(f=>{n.value=f.message})},1e4)}),Sn(()=>window.clearInterval(l)),(f,p)=>($(),Lt(Rl,{eyebrow:"用户控制台",title:"我的算力"},{default:Gt(()=>{var g,S;return[n.value?($(),q("p",Na,V(n.value),1)):be("",!0),s.value?($(),q("p",Ma,V(s.value),1)):be("",!0),t.value?($(),q("section",Da,[v("article",La,[p[5]||(p[5]=v("span",null,"可用积分",-1)),v("strong",null,V(W(De)(t.value.available_points)),1),v("em",null,"已入账 "+V(W(De)(t.value.settled_points)),1)]),v("article",Va,[p[6]||(p[6]=v("span",null,"今日预计",-1)),v("strong",null,V(W(De)(t.value.today_points)),1),p[7]||(p[7]=v("em",null,"按当前在线实时估算",-1))]),v("article",Fa,[p[8]||(p[8]=v("span",null,"节点",-1)),v("strong",null,V(t.value.nodes.length),1),v("em",null,V(t.value.nodes.filter(E=>E.status==="online").length)+" 台在线",1)])])):be("",!0),v("section",ka,[($(!0),q(ge,null,cn(((g=t.value)==null?void 0:g.nodes)||[],E=>($(),q("article",{key:E.sn,class:"plate"},[v("header",null,[v("strong",null,V(E.sn),1),v("span",{class:ke(["pill",E.status])},[p[9]||(p[9]=v("i",null,null,-1)),_e(V(W(dn)[E.status]),1)],2)]),v("p",Ha,[v("b",{style:En({width:W(Gn)(E.online_rate)})},null,4),_e(V(W(Gn)(E.online_rate))+" 今日在线",1)]),v("dl",null,[v("div",null,[p[10]||(p[10]=v("dt",null,"购买金额",-1)),v("dd",null,V(W(De)(E.purchase_amount)),1)]),v("div",null,[p[11]||(p[11]=v("dt",null,"今日积分",-1)),v("dd",null,V(W(De)(E.today_points)),1)]),v("div",null,[p[12]||(p[12]=v("dt",null,"最近心跳",-1)),v("dd",null,V(W(js)(E.last_heartbeat_at)),1)])])]))),128)),t.value&&!t.value.nodes.length?($(),q("p",ja,"还没有节点。下面可以登记第一台。")):be("",!0)]),v("section",Ba,[v("form",{class:"panel",onSubmit:Ls(d,["prevent"])},[p[16]||(p[16]=v("h2",null,"登记节点",-1)),p[17]||(p[17]=v("p",{class:"sub"},"绑到当前钱包,授权周期 12 个月。",-1)),v("label",null,[p[13]||(p[13]=_e("序列号",-1)),tt(v("input",{"onUpdate:modelValue":p[0]||(p[0]=E=>r.value.sn=E),required:"",minlength:"4",placeholder:"NODE-001"},null,512),[[nt,r.value.sn]])]),v("label",null,[p[14]||(p[14]=_e("购买金额",-1)),tt(v("input",{"onUpdate:modelValue":p[1]||(p[1]=E=>r.value.purchase_amount=E),required:"",placeholder:"365"},null,512),[[nt,r.value.purchase_amount]])]),v("label",null,[p[15]||(p[15]=_e("回调地址",-1)),tt(v("input",{"onUpdate:modelValue":p[2]||(p[2]=E=>r.value.callback_url=E),placeholder:"https:// 可选"},null,512),[[nt,r.value.callback_url]])]),p[18]||(p[18]=v("button",{class:"gold",type:"submit"},"登记",-1))],32),v("form",{class:"panel",onSubmit:Ls(a,["prevent"])},[p[21]||(p[21]=v("h2",null,"申请提现",-1)),p[22]||(p[22]=v("p",{class:"sub"},"提交后金额会被占用,等管理员打款。",-1)),v("label",null,[p[19]||(p[19]=_e("金额",-1)),tt(v("input",{"onUpdate:modelValue":p[3]||(p[3]=E=>i.value.amount=E),required:"",placeholder:"1.25"},null,512),[[nt,i.value.amount]])]),v("label",null,[p[20]||(p[20]=_e("资产",-1)),tt(v("input",{"onUpdate:modelValue":p[4]||(p[4]=E=>i.value.asset_symbol=E)},null,512),[[nt,i.value.asset_symbol]])]),p[23]||(p[23]=v("button",{class:"gold",type:"submit"},"提交申请",-1)),v("ul",Ua,[($(!0),q(ge,null,cn(((S=t.value)==null?void 0:S.withdrawals)||[],E=>($(),q("li",{key:E.id},[v("span",null,"#"+V(E.id)+" · "+V(W(De)(E.amount))+" "+V(E.asset_symbol),1),v("span",{class:ke(["pill",E.status])},V(W(dn)[E.status]),3)]))),128))])],32)])]}),_:1}))}},Ka={key:0,class:"form-error banner"},Ga={key:1,class:"banner ok"},Wa={key:2,class:"metrics"},qa={class:"metric"},Ja={class:"metric"},za={class:"metric"},Ya={class:"metric"},Qa={class:"board"},Xa={class:"wallet"},Za={class:"rate"},ef={key:0,class:"empty"},tf={class:"table-wrap"},nf={class:"mono"},sf=["onUpdate:modelValue"],rf={key:1,class:"mono"},lf={key:0,class:"actions"},of=["onClick"],uf=["onClick"],cf={key:1},af={key:0},ff={class:"table-wrap"},df={class:"mono"},hf={__name:"AdminOverview",setup(e){const t=fe(null),n=fe([]),s=fe([]),r=fe([]),i=fe({}),l=fe(""),o=fe("");let u=0;async function d(){const[p,g,S,E]=await Promise.all([Me("/admin/summary"),Me("/admin/users"),Me("/admin/nodes"),Me("/admin/withdrawals")]);t.value=p,n.value=g,s.value=S,r.value=E}async function a(p){const g=(i.value[p.id]||"").trim();if(!g){l.value="请先填写链上哈希";return}l.value="",o.value="";try{await Me(`/admin/withdrawals/${p.id}/confirm`,{method:"POST",body:JSON.stringify({tx_hash:g})}),o.value=`提现 #${p.id} 已打款`,await d()}catch(S){l.value=S.message}}async function f(p){l.value="",o.value="";try{await Me(`/admin/withdrawals/${p.id}/reject`,{method:"POST",body:JSON.stringify({note:"管理后台拒绝"})}),o.value=`提现 #${p.id} 已拒绝`,await d()}catch(g){l.value=g.message}}return ss(async()=>{try{await d()}catch(p){l.value=p.message}u=window.setInterval(()=>{d().catch(p=>{l.value=p.message})},1e4)}),Sn(()=>window.clearInterval(u)),(p,g)=>($(),Lt(Rl,{eyebrow:"管理后台",title:"全网概览"},{default:Gt(()=>[l.value?($(),q("p",Ka,V(l.value),1)):be("",!0),o.value?($(),q("p",Ga,V(o.value),1)):be("",!0),t.value?($(),q("section",Wa,[v("article",qa,[g[0]||(g[0]=v("span",null,"用户",-1)),v("strong",null,V(t.value.users),1),g[1]||(g[1]=v("em",null,"已注册钱包",-1))]),v("article",Ja,[g[2]||(g[2]=v("span",null,"节点",-1)),v("strong",null,V(t.value.nodes),1),v("em",null,"在线 "+V(t.value.online)+" · 离线 "+V(t.value.offline),1)]),v("article",za,[g[3]||(g[3]=v("span",null,"总产出",-1)),v("strong",null,V(W(De)(t.value.settled_points)),1),v("em",null,"授权 "+V(W(De)(t.value.authorized_amount)),1)]),v("article",Ya,[g[4]||(g[4]=v("span",null,"待审核",-1)),v("strong",null,V(W(De)(t.value.pending_withdrawals)),1),g[5]||(g[5]=v("em",null,"占用中的提现",-1))])])):be("",!0),g[12]||(g[12]=v("h2",{class:"section-title"},"节点监控",-1)),v("section",Qa,[($(!0),q(ge,null,cn(s.value,S=>($(),q("article",{key:S.sn,class:"plate"},[v("header",null,[v("strong",null,V(S.sn),1),v("span",{class:ke(["pill",S.status])},[g[6]||(g[6]=v("i",null,null,-1)),_e(V(W(dn)[S.status]),1)],2)]),v("p",Xa,V(S.wallet_address),1),v("p",Za,[v("b",{style:En({width:W(Gn)(S.online_rate)})},null,4),_e(V(W(Gn)(S.online_rate))+" 今日在线率",1)]),v("dl",null,[v("div",null,[g[7]||(g[7]=v("dt",null,"今日积分",-1)),v("dd",null,V(W(De)(S.today_points)),1)]),v("div",null,[g[8]||(g[8]=v("dt",null,"心跳",-1)),v("dd",null,V(W(js)(S.last_heartbeat_at)),1)])])]))),128)),s.value.length?be("",!0):($(),q("p",ef,"还没有节点。"))]),g[13]||(g[13]=v("h2",{class:"section-title"},"提现审核",-1)),v("div",tf,[v("table",null,[g[10]||(g[10]=v("thead",null,[v("tr",null,[v("th",null,"单号"),v("th",null,"钱包"),v("th",null,"金额"),v("th",null,"状态"),v("th",null,"链上哈希"),v("th")])],-1)),v("tbody",null,[($(!0),q(ge,null,cn(r.value,S=>($(),q("tr",{key:S.id},[v("td",null,"#"+V(S.id),1),v("td",nf,V(S.wallet_address),1),v("td",null,V(W(De)(S.amount))+" "+V(S.asset_symbol),1),v("td",null,[v("span",{class:ke(["pill",S.status])},V(W(dn)[S.status]),3)]),v("td",null,[S.status==="pending"?tt(($(),q("input",{key:0,"onUpdate:modelValue":E=>i.value[S.id]=E,placeholder:"交易哈希"},null,8,sf)),[[nt,i.value[S.id]]]):($(),q("span",rf,V(S.tx_hash||"—"),1))]),S.status==="pending"?($(),q("td",lf,[v("button",{class:"gold",type:"button",onClick:E=>a(S)},"打款",8,of),v("button",{class:"texty",type:"button",onClick:E=>f(S)},"拒绝",8,uf)])):($(),q("td",cf))]))),128)),r.value.length?be("",!0):($(),q("tr",af,[...g[9]||(g[9]=[v("td",{colspan:"6",class:"empty"},"还没有提现申请。",-1)])]))])])]),g[14]||(g[14]=v("h2",{class:"section-title"},"用户",-1)),v("div",ff,[v("table",null,[g[11]||(g[11]=v("thead",null,[v("tr",null,[v("th",null,"钱包"),v("th",null,"用户名"),v("th",null,"角色"),v("th",null,"节点"),v("th",null,"可用积分"),v("th",null,"注册")])],-1)),v("tbody",null,[($(!0),q(ge,null,cn(n.value,S=>($(),q("tr",{key:S.id},[v("td",df,V(S.wallet_address),1),v("td",null,V(S.username||"—"),1),v("td",null,V(W(dn)[S.role]),1),v("td",null,V(S.node_count),1),v("td",null,V(W(De)(S.available_points)),1),v("td",null,V(W(js)(S.created_at)),1)]))),128))])])])]),_:1}))}},Ol=ca({history:Bc(),routes:[{path:"/",redirect:()=>ir().role==="admin"?"/admin":"/dashboard"},{path:"/login",component:wa},{path:"/dashboard",component:$a,meta:{auth:!0}},{path:"/admin",component:hf,meta:{auth:!0,admin:!0}}]});Ol.beforeEach(e=>{const t=ir();return e.meta.auth&&!t.token?"/login":e.meta.admin&&t.role!=="admin"?"/dashboard":e.path==="/login"&&t.token?t.role==="admin"?"/admin":"/dashboard":!0});Qu(sc).use(Ol).mount("#app"); diff --git a/web/assets/index-mWM9aOGS.css b/web/assets/index-mWM9aOGS.css new file mode 100644 index 0000000..96787cc --- /dev/null +++ b/web/assets/index-mWM9aOGS.css @@ -0,0 +1 @@ +:root{--bg: #08090d;--ink: #f4efe6;--muted: #a3988a;--gold: #e0c07a;--line: rgba(224, 192, 122, .18);--panel: rgba(16, 18, 24, .82);--online: #8ed0b5;--offline: #d08a78;--serif: "Fraunces", Palatino, serif;--sans: "Manrope", "Segoe UI", sans-serif;--mono: "IBM Plex Mono", ui-monospace, monospace}*{box-sizing:border-box}html,body,#app{min-height:100%}body{margin:0;color:var(--ink);font-family:var(--sans);background:radial-gradient(980px 520px at 0% -10%,rgba(224,192,122,.16),transparent 58%),radial-gradient(760px 480px at 100% 0%,rgba(126,164,196,.12),transparent 52%),var(--bg)}body:before{content:"";position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;background-image:linear-gradient(rgba(244,239,230,.035) 1px,transparent 1px),linear-gradient(90deg,rgba(244,239,230,.035) 1px,transparent 1px);background-size:84px 84px;-webkit-mask-image:radial-gradient(circle at 20% 10%,#000 0%,transparent 72%);mask-image:radial-gradient(circle at 20% 10%,#000 0%,transparent 72%)}button,input{font:inherit;color:inherit}button{cursor:pointer}h1,h2,p{margin:0}a{color:inherit}.gate{min-height:100vh;display:grid;grid-template-columns:1.1fr .9fr;gap:48px;align-items:center;padding:64px 8vw}.gate-copy h1{margin:12px 0 18px;font-family:var(--serif);font-size:clamp(52px,6vw,88px);font-weight:560;letter-spacing:-.04em;line-height:.95}.gate-copy p:last-child{max-width:420px;color:var(--muted);font-size:18px;line-height:1.6}.eyebrow{color:var(--gold);letter-spacing:.18em;text-transform:uppercase;font-size:12px}.gate-card,.panel,.metric,.plate,.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:24px;box-shadow:0 24px 80px #00000059}.gate-card{display:grid;gap:14px;padding:28px}.switch{display:grid;grid-template-columns:1fr 1fr;gap:8px}.switch button,.texty,.ghost{border:1px solid transparent;background:transparent;color:var(--muted);border-radius:999px;padding:10px 14px}.switch button.on{color:var(--ink);background:#e0c07a1a;border-color:var(--line)}.gold{border:0;border-radius:999px;padding:12px 18px;background:linear-gradient(180deg,#f0d7a2,#c4964e);color:#24180b;font-weight:700}.gold.wide{width:100%;margin-top:4px}.gold:disabled{opacity:.6}label{display:grid;gap:6px;color:var(--muted);font-size:13px}input{width:100%;border:1px solid var(--line);background:#08090d8c;border-radius:12px;padding:12px 14px;outline:none}input:focus{border-color:var(--gold)}.hint,.sub{color:var(--muted);font-size:13px;line-height:1.5}.form-error{color:#e7b2a6;font-size:14px}.banner{margin-bottom:14px}.banner.ok{color:var(--online)}.shell{min-height:100vh;display:grid;grid-template-columns:248px 1fr}.rail{position:sticky;top:0;height:100vh;padding:28px 20px;display:flex;flex-direction:column;border-right:1px solid var(--line);background:#08090db8}.brand{display:flex;gap:12px;align-items:center;text-decoration:none}.mark{width:44px;height:44px;display:grid;place-items:center;color:var(--gold);border:1px solid var(--line);border-radius:14px}.brand strong{display:block;font-family:var(--serif);font-size:22px;font-weight:560}.brand em{display:block;margin-top:2px;color:var(--muted);font-style:normal;font-size:11px;letter-spacing:.16em;text-transform:uppercase}.nav{display:grid;gap:6px;margin-top:36px}.nav a{text-decoration:none;color:var(--muted);border-radius:12px;padding:12px 14px}.nav a.router-link-active{color:var(--ink);background:#e0c07a14;box-shadow:inset 2px 0 0 var(--gold)}.rail-foot{margin-top:auto;display:flex;justify-content:space-between;align-items:center;gap:8px}.who{color:var(--muted);font-size:13px;overflow:hidden;text-overflow:ellipsis}.stage{padding:28px 32px 64px}.top{display:flex;justify-content:space-between;align-items:end;gap:16px}.top h1{margin-top:8px;font-family:var(--serif);font-size:56px;font-weight:560;letter-spacing:-.04em}.clock span{display:block;color:var(--muted);font-size:11px;letter-spacing:.14em}.clock strong{font-family:var(--mono);font-size:20px;font-weight:500}.view{margin-top:28px}.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px}.metric{padding:22px}.metric span{color:var(--muted);font-size:12px;letter-spacing:.14em;text-transform:uppercase}.metric strong{display:block;margin-top:12px;font-family:var(--serif);font-size:40px;font-weight:560;letter-spacing:-.04em}.metric em{display:block;margin-top:8px;color:var(--muted);font-style:normal;font-size:13px}.section-title{margin:28px 0 14px;font-family:var(--serif);font-weight:560;font-size:28px}.board{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px;margin-top:16px}.plate{padding:18px}.plate header{display:flex;justify-content:space-between;align-items:center;gap:8px}.plate header strong{font-family:var(--mono);font-size:14px}.wallet{margin-top:8px;color:var(--muted);font-family:var(--mono);font-size:12px}.rate{position:relative;overflow:hidden;margin:14px 0;padding:8px 10px;border-radius:999px;background:#ffffff08;color:var(--muted);font-size:12px}.rate b{position:absolute;inset:0 auto 0 0;background:#e0c07a2e}.plate dl{display:grid;gap:8px;margin:0}.plate dl div{display:flex;justify-content:space-between;gap:12px;font-size:14px}.plate dt{color:var(--muted)}.plate dd{margin:0}.pill{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:4px 10px;border:1px solid var(--line);font-size:12px}.pill i{width:6px;height:6px;border-radius:50%;background:currentColor}.pill.online,.pill.completed{color:var(--online)}.pill.offline,.pill.rejected,.pill.failed{color:var(--offline)}.pill.pending{color:var(--gold)}.forms{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px;align-items:start}.panel{display:grid;gap:12px;padding:22px}.panel h2{font-family:var(--serif);font-size:28px;font-weight:560}.table-wrap{overflow:auto;margin-top:8px}table{width:100%;border-collapse:collapse}th,td{padding:14px 16px;text-align:left;border-bottom:1px solid rgba(224,192,122,.08);font-size:14px}th{color:var(--muted);font-size:11px;letter-spacing:.14em;text-transform:uppercase}td input{width:180px;padding:8px 10px}.actions{display:flex;gap:8px}.mono{font-family:var(--mono);font-size:12px}.empty{padding:28px 16px;color:var(--muted)}.mini{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:8px}.mini li{display:flex;justify-content:space-between;gap:8px;font-size:13px}@media (max-width: 1100px){.metrics,.forms,.gate,.shell{grid-template-columns:1fr}.rail{position:relative;height:auto}.top h1{font-size:40px}} diff --git a/web/index.html b/web/index.html index f47876b..87896fd 100644 --- a/web/index.html +++ b/web/index.html @@ -1,60 +1,16 @@ - - - - - - HyperNode · 算力控制面 - - - - - - -
- - -
-
-
-

控制面

-

总览

-
-
-
- 北京时间 - --:--:-- -
- -
-
-
-
-
-
- - - + + + + + + HyperNode · 算力控制面 + + + + + + + +
+ + diff --git a/web/styles.css b/web/styles.css deleted file mode 100644 index 32ffbf2..0000000 --- a/web/styles.css +++ /dev/null @@ -1,359 +0,0 @@ -:root { - --bg: #08090d; - --ink: #f4efe6; - --muted: #a3988a; - --gold: #e0c07a; - --gold-deep: #a88345; - --line: rgba(224, 192, 122, 0.18); - --panel: rgba(16, 18, 24, 0.82); - --panel-solid: #12141b; - --online: #8ed0b5; - --offline: #d08a78; - --danger: #e09888; - --shadow: 0 24px 80px rgba(0, 0, 0, 0.35); - --serif: "Fraunces", "Palatino Linotype", Palatino, serif; - --sans: "Manrope", "Segoe UI", sans-serif; - --mono: "IBM Plex Mono", ui-monospace, monospace; -} - -* { box-sizing: border-box; } -html, body { height: 100%; } -body { - margin: 0; - color: var(--ink); - font-family: var(--sans); - background: - radial-gradient(980px 520px at 0% -10%, rgba(224, 192, 122, 0.16), transparent 58%), - radial-gradient(760px 480px at 100% 0%, rgba(126, 164, 196, 0.12), transparent 52%), - var(--bg); -} -body::before { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - background-image: - linear-gradient(rgba(244, 239, 230, 0.035) 1px, transparent 1px), - linear-gradient(90deg, rgba(244, 239, 230, 0.035) 1px, transparent 1px); - background-size: 84px 84px; - mask-image: radial-gradient(circle at 20% 10%, #000 0%, transparent 72%); -} - -button, input, select { font: inherit; color: inherit; } -button { cursor: pointer; } -h1, h2, p { margin: 0; } - -.shell { - min-height: 100%; - display: grid; - grid-template-columns: 248px 1fr; -} - -.rail { - position: sticky; - top: 0; - height: 100vh; - padding: 28px 20px; - display: flex; - flex-direction: column; - border-right: 1px solid var(--line); - background: rgba(8, 9, 13, 0.72); - backdrop-filter: blur(16px); -} -.brand { - display: flex; - gap: 12px; - align-items: center; - color: inherit; - text-decoration: none; -} -.mark { - width: 44px; - height: 44px; - display: grid; - place-items: center; - color: var(--gold); - border: 1px solid var(--line); - border-radius: 14px; - background: linear-gradient(180deg, rgba(224, 192, 122, 0.16), transparent); -} -.brand strong { - display: block; - font-family: var(--serif); - font-size: 22px; - font-weight: 560; - letter-spacing: -0.03em; -} -.brand em { - display: block; - margin-top: 2px; - color: var(--muted); - font-style: normal; - font-size: 11px; - letter-spacing: 0.16em; - text-transform: uppercase; -} -.nav { - display: grid; - gap: 6px; - margin-top: 36px; -} -.nav button { - text-align: left; - border: 0; - background: transparent; - color: var(--muted); - border-radius: 12px; - padding: 12px 14px; - letter-spacing: 0.04em; -} -.nav button.active, -.nav button:hover { - color: var(--ink); - background: rgba(224, 192, 122, 0.08); -} -.nav button.active { box-shadow: inset 2px 0 0 var(--gold); } -.rail-foot { - margin-top: auto; - display: flex; - align-items: center; - gap: 8px; - color: var(--muted); - font-size: 13px; -} -.dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--muted); -} -.dot.on { - background: var(--online); - box-shadow: 0 0 0 4px rgba(142, 208, 181, 0.15); -} - -.stage { min-width: 0; padding: 32px 40px 56px; } -.top { - display: flex; - justify-content: space-between; - gap: 24px; - align-items: flex-end; - margin-bottom: 28px; -} -.eyebrow { - color: var(--gold); - font-size: 12px; - letter-spacing: 0.22em; - text-transform: uppercase; -} -h1 { - margin-top: 6px; - font-family: var(--serif); - font-size: 48px; - font-weight: 560; - letter-spacing: -0.04em; -} -.top-meta { display: flex; align-items: center; gap: 16px; } -.clock { - text-align: right; -} -.clock span { - display: block; - color: var(--muted); - font-size: 11px; - letter-spacing: 0.14em; -} -.clock strong { - font-family: var(--mono); - font-size: 20px; - font-weight: 500; -} - -.ghost, .gold, .texty { - border-radius: 999px; - border: 1px solid var(--line); - background: transparent; - padding: 10px 16px; -} -.ghost:hover, .texty:hover { border-color: var(--gold); } -.gold { - border-color: transparent; - background: linear-gradient(180deg, #f0d7a2, #c4964e); - color: #24180b; - font-weight: 700; -} -.gold:hover { filter: brightness(1.05); } -.texty { padding: 6px 10px; color: var(--gold); } - -.metrics { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 16px; -} -.metric, .panel, .table-wrap, .drawer { - background: var(--panel); - border: 1px solid var(--line); - border-radius: 22px; - box-shadow: var(--shadow); -} -.metric { padding: 22px 22px 18px; } -.metric span { - color: var(--muted); - font-size: 12px; - letter-spacing: 0.14em; - text-transform: uppercase; -} -.metric strong { - display: block; - margin-top: 14px; - font-family: var(--serif); - font-size: 40px; - font-weight: 560; - letter-spacing: -0.04em; - font-variant-numeric: tabular-nums; -} -.metric em { - display: block; - margin-top: 8px; - color: var(--muted); - font-style: normal; - font-size: 13px; -} - -.layout, .forms { - display: grid; - gap: 16px; - margin-top: 16px; - align-items: start; -} -.layout { grid-template-columns: 1.4fr 0.8fr; } -.forms { grid-template-columns: 1fr 1fr; } -.panel { padding: 22px; } -.panel h2 { - font-family: var(--serif); - font-size: 28px; - font-weight: 560; - letter-spacing: -0.03em; -} -.sub { - margin: 6px 0 18px; - color: var(--muted); - font-size: 14px; -} -.stack { display: grid; gap: 14px; } -label { display: grid; gap: 6px; font-size: 13px; color: var(--muted); } -input, select { - width: 100%; - border: 1px solid var(--line); - background: rgba(8, 9, 13, 0.55); - border-radius: 12px; - padding: 12px 14px; - outline: none; -} -input:focus, select:focus { border-color: var(--gold); } -.row-actions { display: flex; gap: 8px; flex-wrap: wrap; } - -.table-wrap { overflow: auto; } -table { width: 100%; border-collapse: collapse; } -th, td { padding: 14px 16px; text-align: left; border-bottom: 1px solid rgba(224, 192, 122, 0.08); font-size: 14px; } -th { - color: var(--muted); - font-size: 11px; - font-weight: 600; - letter-spacing: 0.14em; - text-transform: uppercase; -} -td input { width: 148px; padding: 8px 10px; } -tr.clickable { cursor: pointer; } -tr.clickable:hover td { background: rgba(224, 192, 122, 0.04); } -.mono { font-family: var(--mono); font-size: 13px; } -.pill { - display: inline-flex; - align-items: center; - gap: 6px; - border-radius: 999px; - padding: 4px 10px; - border: 1px solid var(--line); - font-size: 12px; -} -.pill i { - width: 6px; - height: 6px; - border-radius: 50%; - background: currentColor; -} -.pill.online { color: var(--online); } -.pill.offline, .pill.rejected, .pill.failed { color: var(--offline); } -.pill.pending { color: var(--gold); } -.pill.completed, .pill.settled { color: var(--online); } - -.empty { - padding: 48px 20px; - text-align: center; - color: var(--muted); -} -.toolbar { - display: flex; - justify-content: space-between; - gap: 12px; - align-items: end; - margin-bottom: 16px; -} -.toolbar .grow { min-width: 240px; flex: 1; } - -.mask { - position: fixed; - inset: 0; - background: rgba(5, 6, 8, 0.62); - display: grid; - justify-items: end; - z-index: 5; -} -.drawer { - width: min(460px, 100%); - height: 100%; - border-radius: 0; - border-right: 0; - padding: 28px; - overflow: auto; - background: #101218; -} -.drawer header { display: flex; justify-content: space-between; align-items: start; gap: 12px; } -.kv { display: grid; grid-template-columns: 88px 1fr; gap: 8px 12px; margin: 22px 0; font-size: 14px; } -.kv span { color: var(--muted); } - -.toast { - position: fixed; - left: 50%; - bottom: 24px; - transform: translateX(-50%) translateY(12px); - opacity: 0; - pointer-events: none; - background: #1b1e27; - border: 1px solid var(--line); - color: var(--ink); - padding: 12px 16px; - border-radius: 999px; - transition: 0.2s ease; -} -.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } - -@media (max-width: 1100px) { - .metrics, .forms { grid-template-columns: 1fr 1fr; } - .layout { grid-template-columns: 1fr; } -} -@media (max-width: 860px) { - .shell { grid-template-columns: 1fr; } - .rail { - position: relative; - height: auto; - border-right: 0; - border-bottom: 1px solid var(--line); - } - .nav { grid-template-columns: repeat(4, 1fr); margin-top: 18px; } - .rail-foot { display: none; } - .stage { padding: 22px 16px 40px; } - h1 { font-size: 36px; } - .metrics, .layout, .forms { grid-template-columns: 1fr; } - .top { align-items: start; } -} diff --git a/说明.md b/说明.md index 32978c9..d46a13f 100644 --- a/说明.md +++ b/说明.md @@ -23,14 +23,15 @@ python -m app | `app/jobs/daily_yield.py` | 凌晨清算入口 | | `app/scheduler.py` | 进程内定时任务 | | `app/settle.py` | 可单独执行的清算命令 | -| `web/` | 控制台页面,服务启动后打开 `/` | +| `frontend/` | Vue 3 控制台源码。改完后在该目录执行 `npm run build` | +| `web/` | 控制台构建结果,服务启动后打开 `/` | | `deploy/manage.sh` | Ubuntu 22.04 交互式部署,支持 `curl \| bash` | ## 数据表 业务表: -- `users`:钱包地址、注册时间。 +- `users`:钱包地址、可选用户名和密码、角色 `user` / `admin`、注册时间。 - `compute_nodes`:节点 SN、购买金额、12 个月授权、`online` / `offline`、最后心跳。可选 `callback_url` 用来推送任务。 - `node_yields`:每日算力产出和积分。同一节点同一天只有一条。 - `withdrawal_logs`:提现金额、资产符号、链上哈希、状态。 @@ -66,14 +67,43 @@ python -m app 回传必须带上任务里的 `nonce`。超时、重复回传、nonce 不匹配都不算成功。 +## 登录 + +普通用户可以注册钱包、用户名和密码。之后用钱包地址,或用户名加密码登录。管理员只能用账号密码。 + +新部署会在安装结束时打印管理员账号 `admin` 和随机密码,并写入 `/opt/hypernode/.env` 的 `ADMIN_PASSWORD`。已经部署过的机器如果 `.env` 里没有这两项,服务第一次启动会在日志里写一次初始密码: + +```bash +journalctl -u hypernode.service | grep 初始密码 +``` + +页面路由: + +- `/login` 登录和注册 +- `/dashboard` 用户自己的节点、今日预计积分、提现申请 +- `/admin` 管理员看总用户、总节点、总产出、节点在线率和提现审核 + +节点心跳、拉任务、回传任务不需要登录。查看全网数据、清算、确认提现需要管理员令牌。 + ## 接口 -前缀都是 `/api/v1`。金额在 JSON 里是字符串。 +前缀都是 `/api/v1`。金额在 JSON 里是字符串。浏览器登录后带 `Authorization: Bearer `。 | 方法 | 路径 | 作用 | |---|---|---| -| POST | `/users` | 注册钱包 | -| POST | `/nodes` | 登记节点 | +| POST | `/auth/register` | 注册钱包,可带用户名和密码,返回令牌 | +| POST | `/auth/login` | 钱包地址,或用户名加密码 | +| GET | `/me/dashboard` | 当前用户的节点、积分和提现 | +| POST | `/me/nodes` | 给当前钱包登记节点 | +| POST | `/me/withdrawals` | 当前用户申请提现 | +| GET | `/admin/summary` | 管理员:用户数、节点数、总产出 | +| GET | `/admin/users` | 管理员:全网用户 | +| GET | `/admin/nodes` | 管理员:节点状态与今日在线率 | +| GET | `/admin/withdrawals` | 管理员:提现审核列表 | +| POST | `/admin/withdrawals/{id}/confirm` | 管理员:写入链上哈希并完成 | +| POST | `/admin/withdrawals/{id}/reject` | 管理员:拒绝并释放占用的积分 | +| POST | `/users` | 只登记钱包,不设置密码 | +| POST | `/nodes` | 登记节点,必须是该钱包本人或管理员 | | GET | `/nodes/{sn}` | 查看节点 | | POST | `/node/heartbeat` | 心跳,请求体 `{"sn":"..."}` | | GET | `/node/{sn}/tasks` | 拉取未完成任务 | @@ -107,5 +137,10 @@ python -m app | `YIELD_HOUR` / `YIELD_MINUTE` | `0` / `5` | 清算时刻 | | `LICENSE_MONTHS` | `12` | 新节点授权月数 | | `DISABLE_SCHEDULER` | `false` | 设为 `true` 时不启动定时任务 | +| `JWT_SECRET` | 空则自动生成并写入数据库 | 登录令牌签名 | +| `JWT_EXPIRE_MINUTES` | `1440` | 令牌有效分钟数 | +| `ADMIN_USERNAME` | `admin` | 初始管理员用户名 | +| `ADMIN_PASSWORD` | 空则首次启动随机生成并写入日志 | 初始管理员密码 | +| `ADMIN_WALLET` | `admin-root` | 初始管理员钱包地址 | 修改 `.env` 后执行 `systemctl restart hypernode`。 diff --git a/部署.md b/部署.md index 5ba96ae..e879830 100644 --- a/部署.md +++ b/部署.md @@ -60,7 +60,7 @@ journalctl -u hypernode.service -f curl http://127.0.0.1:33333/health ``` -控制台:`http://<服务器 IP>:33333/` +控制台:`http://<服务器 IP>:33333/`。首次部署结束时会打印管理员账号和密码;已经部署过的机器,密码在第一次更新后的服务日志里,搜索「初始密码」。 接口文档:`http://<服务器 IP>:33333/docs`