From 0c25287c190bcfd9c48c0806092013b268231836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 13:39:23 +0800 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20=E5=AF=86=E9=92=A5=E6=8E=A9?= =?UTF-8?q?=E7=A0=81=E5=93=A8=E5=85=B5(=5F=5FSET=5F=5F)=E5=9B=9B=E5=A4=84?= =?UTF-8?q?=E5=93=8D=E5=BA=94=20+=20=E7=AB=99=E7=82=B9=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=94=BE=E5=BC=80=E7=A7=9F=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config_routes: push.api_key/ai.api_key/email.smtp.password 掩码助手 (MASKED="__SET__";非空才掩,空串/None 原样)。作用于 GET config、 GET versions/{n}、POST rollback 响应、GET settings(计划四处),并盖 PUT/reset/PUT settings 的写回显——哨兵还原后的库值不经任何响应外泄。 - PUT 哨兵还原: 密钥字段收到 __SET__ → 当前快照原值替换后再写版本 (前端"不改就原样传回");传新值照常覆盖;空串传回仍=清空(既有语义 不变)。settings 同款(密钥名含 key/password/secret,递归嵌套 dict; 设置 key 本身命中也算)。库行/版本历史永远存原值。 - tenant_routes: 站点/域名四端点(POST sites/PATCH sites/POST domains/ DELETE domains)require_super → _tenant_guard;租户生命周期三端点 (POST /tenants/GET 列表/PATCH /tenants/{slug})保持超管专属。 - 测试: 掩码四处响应+落库分离断言(_current_snapshot 直查库)、哨兵 往返/新值/清空/嵌套路径/未设还原 None、settings 标量+嵌套+非密钥 字面量;租户管理员四端点自助(201/200)+他租户 403+生命周期仍 403。 Co-Authored-By: Claude Code --- app/platform/routes/config_routes.py | 163 +++++++++++++++++++++++--- app/platform/routes/tenant_routes.py | 24 ++-- tests/test_config_api.py | 167 +++++++++++++++++++++++++++ tests/test_tenant_admin_api.py | 63 ++++++++++ 4 files changed, 392 insertions(+), 25 deletions(-) diff --git a/app/platform/routes/config_routes.py b/app/platform/routes/config_routes.py index 8f27eb0..cac7ccd 100644 --- a/app/platform/routes/config_routes.py +++ b/app/platform/routes/config_routes.py @@ -26,6 +26,12 @@ 鉴权矩阵(spec §3):config 系 = require_tenant(slug)(租户全开——本租户 管理员可自助改配置);settings = require_super。409 门经 platform_user 连锁。 +密钥掩码哨兵(Phase 3 Task 1):push.api_key / ai.api_key / +email.smtp.password 及平台设置的密钥名值,所有携带 config/value 的响应 +一律掩码为 "__SET__"(原值非空;空串/None 原样——前端能区分"配了"与 +"没配");PUT 收到 "__SET__" 还原为库值再写版本(前端"不改就原样传回" +的往返契约)。库行/版本历史永远存原值——掩码只作用响应层。 + 版本写入一律走 db.write_config_version(与 Task 5 创建向导同一 MAX+1 路径)。 """ import copy @@ -110,6 +116,111 @@ def _updated_by(user: UserContext) -> str: return f"{user.role}#{user.user_id}" +# ---- 密钥掩码 / __SET__ 哨兵(Phase 3 Task 1) -------------------------------------- + +MASKED = "__SET__" + +# 租户配置密钥字段清单(组 → 组内路径元组;多元素元组 = 嵌套键)。响应一律 +# 掩码;PUT 收到 MASKED 值还原为当前快照原值——前端"不改就原样传回"的往返 +# 契约。语义:原值非空 → MASKED;空串/None/缺键原样(前端能区分"配了"与 +# "没配")。空串传回仍 = 清空(既有语义不因掩码改变)。 +_SECRET_PATHS: dict[str, tuple[tuple[str, ...], ...]] = { + "push": (("api_key",),), + "ai": (("api_key",),), + "email": (("smtp", "password"),), +} + + +def _dig(container, path: tuple[str, ...]): + """按路径取嵌套 dict 值;任一层不是 dict → None。""" + node = container + for key in path: + node = node.get(key) if isinstance(node, dict) else None + return node + + +def _mask_group(group: str, value): + """单组掩码视图(深拷贝):密钥路径上非空字符串 → MASKED,空串/None/ + 缺键原样。非 dict 组值原样深拷贝返回。""" + out = copy.deepcopy(value) + if not isinstance(out, dict): + return out + for path in _SECRET_PATHS.get(group, ()): + leaf = _dig(out, path) + if isinstance(leaf, str) and leaf: + node = _dig(out, path[:-1]) + node[path[-1]] = MASKED + return out + + +def _mask_snapshot(snapshot: dict) -> dict: + """整份快照的掩码视图(逐组 _mask_group)。""" + return {group: _mask_group(group, value) + for group, value in snapshot.items()} + + +def _restore_sentinels(group: str, body: dict, current: dict) -> dict: + """PUT 哨兵还原:body 密钥路径上值恰为 MASKED → 替换为当前快照同路径 + 原值(无库存 → None,保持"未设置"——哨兵字面量绝不落库)。返回处理后 + 的深拷贝,不改入参;非密钥路径与传新值/空串的路径一律原样。""" + out = copy.deepcopy(body) + for path in _SECRET_PATHS.get(group, ()): + if _dig(out, path) == MASKED: + node = _dig(out, path[:-1]) + node[path[-1]] = _dig(current.get(group), path) + return out + + +# 平台设置密钥判定:key 名(不区分大小写)含以下片段即视为密钥。适用设置 +# 项自身的 key(标量密钥,如 glm_api_key = "sk-...")与 value 的嵌套 dict +# 递归两层。 +_SETTING_SECRET_MARKERS = ("key", "password", "secret") + + +def _is_secret_setting_key(name: str) -> bool: + lowered = name.lower() if isinstance(name, str) else "" + return any(marker in lowered for marker in _SETTING_SECRET_MARKERS) + + +def _mask_setting_value(value): + """递归掩码:密钥名下的非空字符串值 → MASKED;dict/list 递归,其余 + 标量原样。""" + if isinstance(value, dict): + return { + k: (MASKED if _is_secret_setting_key(k) + and isinstance(v, str) and v else _mask_setting_value(v)) + for k, v in value.items()} + if isinstance(value, list): + return [_mask_setting_value(v) for v in value] + return value + + +def _mask_setting_item(key: str, value): + """单个设置项的掩码视图:设置 key 本身命中密钥名且值为非空字符串 → + 掩码;否则递归掩码 value 内部。""" + if _is_secret_setting_key(key) and isinstance(value, str) and value: + return MASKED + return _mask_setting_value(value) + + +def _restore_setting_sentinels(value, stored): + """value 中密钥位置的 MASKED → stored 同路径值(递归结构与 + _mask_setting_value 对齐;stored 无该路径 → None)。不改入参。""" + if isinstance(value, dict): + src = stored if isinstance(stored, dict) else {} + return { + k: (copy.deepcopy(src.get(k)) + if _is_secret_setting_key(k) and v == MASKED + else _restore_setting_sentinels(v, src.get(k))) + for k, v in value.items()} + if isinstance(value, list): + src = stored if isinstance(stored, list) else [] + return [_restore_setting_sentinels( + v, src[i] if i < len(src) else None) + for i, v in enumerate(value)] + return value + + # ---- 配置快照 / 分组编辑 ---------------------------------------------------------- @@ -117,12 +228,14 @@ def _updated_by(user: UserContext) -> str: async def get_config(slug: str, request: Request, user=Depends(_tenant_guard)): """当前生效快照(MAX 版本)+ version。原样快照,非合并视图——缺省键 - 由消费侧(registry)深合并 DEFAULT_CONFIG。""" + 由消费侧(registry)深合并 DEFAULT_CONFIG。密钥字段掩码(Phase 3 + Task 1):非空 → MASKED,空串/None 原样。""" engine = request.app.state.platform_engine async with AsyncSession(engine, expire_on_commit=False) as session: tenant = await _get_tenant_or_404(session, slug) row, snapshot = await _load_current(session, tenant.id, slug) - return {"slug": slug, "version": row.version, "config": snapshot} + return {"slug": slug, "version": row.version, + "config": _mask_snapshot(snapshot)} @router.put("/{slug}/config/{group}") @@ -131,7 +244,12 @@ async def put_config_group( user=Depends(_tenant_guard)): """分组编辑:校验(config_validation 管线)→ 整组替换当前快照该组 → MAX+1 写新版本 → registry_reload 热生效。快照存用户配置**原样组** - (组内缺省键消费侧深合并出厂默认补)。""" + (组内缺省键消费侧深合并出厂默认补)。 + + 密钥哨兵(Phase 3 Task 1):密钥字段传 MASKED = 保持库值(校验按 + 字符串过,还原发生在校验后写入前);传新值 → 覆盖;空串传回 = 清空 + (既有语义不因掩码改变)。响应的密钥字段同样掩码——哨兵还原后的原值 + 不经响应回显。""" body = await _json_body(request) engine = request.app.state.platform_engine async with AsyncSession(engine, expire_on_commit=False) as session: @@ -142,6 +260,7 @@ async def put_config_group( if errors: raise HTTPException(status_code=400, detail="; ".join(errors)) _, current = await _load_current(session, tenant.id, slug) + body = _restore_sentinels(group, body, current) new_snapshot = dict(current) new_snapshot[group] = copy.deepcopy(body) version = await write_config_version( @@ -151,7 +270,7 @@ async def put_config_group( await request.app.state.registry.registry_reload(slug) return {"slug": slug, "group": group, "version": version, - "config": new_snapshot[group]} + "config": _mask_group(group, new_snapshot[group])} @router.post("/{slug}/config/{group}/reset") @@ -197,7 +316,8 @@ async def reset_config_group( await session.commit() await request.app.state.registry.registry_reload(slug) - return {"slug": slug, "group": group, "version": version, "config": value} + return {"slug": slug, "group": group, "version": version, + "config": _mask_group(group, value)} # ---- 版本历史 / 回滚 --------------------------------------------------------------- @@ -226,7 +346,8 @@ async def list_config_versions( @router.get("/{slug}/config/versions/{version}") async def get_config_version( slug: str, version: int, request: Request, user=Depends(_tenant_guard)): - """单版本详情(含整份 config)。不存在 → 404。""" + """单版本详情(含整份 config)。不存在 → 404。config 掩码视图 + (Phase 3 Task 1)——历史行本身永远存原值。""" engine = request.app.state.platform_engine async with AsyncSession(engine, expire_on_commit=False) as session: tenant = await _get_tenant_or_404(session, slug) @@ -242,7 +363,7 @@ async def get_config_version( "slug": slug, "version": row.version, "created_at": row.created_at.isoformat(), "updated_by": row.updated_by, "comment": row.comment, - "config": json.loads(row.config_json), + "config": _mask_snapshot(json.loads(row.config_json)), } @@ -250,7 +371,7 @@ async def get_config_version( async def rollback_config( slug: str, request: Request, user=Depends(_tenant_guard)): """回滚:目标版本整份复制为新版本(历史不可变——旧版本行永不改写), - reload 生效。""" + reload 生效。落库原值;响应为掩码视图(Phase 3 Task 1)。""" body = await _json_body(request) target = body.get("version") if not isinstance(target, int) or isinstance(target, bool) or target < 1: @@ -277,7 +398,7 @@ async def rollback_config( await request.app.state.registry.registry_reload(slug) return {"slug": slug, "rollback_to": target, "version": new_version, - "config": snapshot} + "config": _mask_snapshot(snapshot)} # ---- 试金石(wall/test) ----------------------------------------------------------- @@ -380,21 +501,28 @@ def _decode_setting(raw: str): @settings_router.get("") async def list_settings(request: Request, user=Depends(require_super)): - """平台级默认(集团 AI 凭据/默认 SMTP 等,spec §5.2)——本阶段只落 - 存储与接口;消费(resolver 平台默认回退链)归 Phase 3。""" + """平台级默认(集团 AI 凭据/默认 SMTP 等,spec §5.2)。值掩码视图 + (Phase 3 Task 1):key 名含 key/password/secret 的非空字符串值 → + MASKED(设置 key 本身与嵌套 dict 递归都算);库行永远存原值。""" engine = request.app.state.platform_engine async with AsyncSession(engine, expire_on_commit=False) as session: rows = (await session.execute( select(PlatformSetting).order_by(PlatformSetting.key)) ).scalars().all() - return {"items": [{"key": row.key, "value": _decode_setting(row.value)} + return {"items": [{"key": row.key, + "value": _mask_setting_item( + row.key, _decode_setting(row.value))} for row in rows]} @settings_router.put("") async def put_setting(request: Request, user=Depends(require_super)): """upsert 单个设置项:key 1-64 字符;value 任意 JSON(json.dumps 入 - Text,读回还原)。同 key 再写 = 整值替换。""" + Text,读回还原)。同 key 再写 = 整值替换。 + + 密钥哨兵(Phase 3 Task 1):key 命中密钥名且值恰为 MASKED,或嵌套 + dict 内密钥位置为 MASKED → 还原为现存同路径值(无库存 → None)。 + 非密钥位置的 MASKED 字面量 = 普通字符串原样落库。响应掩码。""" body = await _json_body(request) key = body.get("key") if not isinstance(key, str) or not 1 <= len(key) <= 64: @@ -402,16 +530,21 @@ async def put_setting(request: Request, user=Depends(require_super)): status_code=400, detail="key: 须为 1-64 字符字符串") if "value" not in body: raise HTTPException(status_code=400, detail="value: 缺少(必填)") - encoded = json.dumps(body["value"], ensure_ascii=False) engine = request.app.state.platform_engine async with AsyncSession(engine, expire_on_commit=False) as session: row = (await session.execute( select(PlatformSetting).where(PlatformSetting.key == key) )).scalar_one_or_none() + stored = _decode_setting(row.value) if row is not None else None + if _is_secret_setting_key(key) and body["value"] == MASKED: + value = copy.deepcopy(stored) + else: + value = _restore_setting_sentinels(body["value"], stored) + encoded = json.dumps(value, ensure_ascii=False) if row is None: session.add(PlatformSetting(key=key, value=encoded)) else: row.value = encoded await session.commit() - return {"key": key, "value": body["value"]} + return {"key": key, "value": _mask_setting_item(key, value)} diff --git a/app/platform/routes/tenant_routes.py b/app/platform/routes/tenant_routes.py index 5a9bef3..d6e0e07 100644 --- a/app/platform/routes/tenant_routes.py +++ b/app/platform/routes/tenant_routes.py @@ -13,8 +13,10 @@ DEFAULT_CONFIG 满足不了。 也直接插入,创建即可用);停用走 reload 路径顺带 dispose 旧 engine (引擎释放由 registry 负责,本层不碰)。 -鉴权:写操作与列表 require_super;GET /{slug} 详情放宽 require_tenant -(slug)(租户管理员看自己概要,403 边界有测试钉住)。 +鉴权:租户生命周期(POST 创建 / GET 列表 / PATCH 停用改名)require_super; +GET /{slug} 详情与站点·域名四端点(POST sites / PATCH sites / +POST domains / DELETE domains)放宽 require_tenant(slug)(Phase 3 Task 1: +本租户管理员自助管理站点与域名,403 边界有测试钉住)。 安全纪律: - 密码哈希/salt 绝不进响应——管理员序列化显式挑 {username, status} @@ -386,9 +388,10 @@ async def patch_tenant( @router.post("/{slug}/sites", status_code=201) async def create_site( slug: str, body: CreateSiteRequest, request: Request, - user=Depends(require_super)): + user=Depends(_tenant_guard)): """建站:slug/name/brand_overlay/domains 可选。brand_overlay 是 ORM - JSON 列——整字典赋值(M-2a)。""" + JSON 列——整字典赋值(M-2a)。鉴权 _tenant_guard(Phase 3 Task 1 + 放宽):本租户管理员可自助建站。""" _validate_slug(body.slug) domains = list(dict.fromkeys( _normalize_domain(d) for d in body.domains)) @@ -427,9 +430,9 @@ async def create_site( @router.patch("/{slug}/sites/{site_slug}") async def patch_site( slug: str, site_slug: str, body: dict, request: Request, - user=Depends(require_super)): + user=Depends(_tenant_guard)): """改站点:停用/改名/改 overlay(整字典赋值,旧键不残留)。站点 slug - 同样不可变(400)。""" + 同样不可变(400)。鉴权 _tenant_guard(Phase 3 Task 1 放宽)。""" fields = _patch_fields(body, allow=("name", "status", "brand_overlay")) if "name" in fields: _check_name(fields["name"]) @@ -462,9 +465,9 @@ async def patch_site( @router.post("/{slug}/sites/{site_slug}/domains", status_code=201) async def add_domain( slug: str, site_slug: str, body: AddDomainRequest, request: Request, - user=Depends(require_super)): + user=Depends(_tenant_guard)): """域名绑定:全局唯一(撞车 400 带现有归属);绑定后 registry reload - 生效。""" + 生效。鉴权 _tenant_guard(Phase 3 Task 1 放宽)。""" domain = _normalize_domain(body.domain) engine = request.app.state.platform_engine async with AsyncSession(engine, expire_on_commit=False) as session: @@ -486,8 +489,9 @@ async def add_domain( @router.delete("/{slug}/sites/{site_slug}/domains/{domain}") async def delete_domain( slug: str, site_slug: str, domain: str, request: Request, - user=Depends(require_super)): - """域名解绑:只解本站点的该域名(不存在 404);解绑后 reload 生效。""" + user=Depends(_tenant_guard)): + """域名解绑:只解本站点的该域名(不存在 404);解绑后 reload 生效。 + 鉴权 _tenant_guard(Phase 3 Task 1 放宽)。""" domain = _normalize_domain(domain) engine = request.app.state.platform_engine async with AsyncSession(engine, expire_on_commit=False) as session: diff --git a/tests/test_config_api.py b/tests/test_config_api.py index 43dc30c..09a9e89 100644 --- a/tests/test_config_api.py +++ b/tests/test_config_api.py @@ -5,13 +5,24 @@ 鉴权矩阵(spec §3):config GET/PUT/reset/versions/rollback/wall-test = require_tenant(slug)(租户全开);settings = require_super。 + +Phase 3 Task 1:密钥掩码哨兵——push.api_key / ai.api_key / +email.smtp.password 及平台设置的密钥名值,响应一律 "__SET__"(原值非空), +PUT 收到 "__SET__" 还原为库值(前端"不改就原样传回"的往返契约)。 """ +import json + +from sqlalchemy import select + +from app.platform.db import PlatformSetting from app.platform.tenant_source import FileTenantSource from tests.conftest import SEED_DIR from tests.test_tenant_admin_api import ( _auth, _bearer, _create, + _current_snapshot, + _db_fetch, _tenant_admin_token, ) @@ -458,3 +469,159 @@ def test_wall_group_round_trip_and_body_hygiene(super_client): ).json()["token"] r = super_client.get(_cfg("t2"), headers=_bearer(token)) assert r.status_code == 409 + + +# ---- 密钥掩码 / __SET__ 哨兵(Phase 3 Task 1) --------------------------------------- + + +def test_get_config_masks_secrets(super_client): + """GET config:密钥字段非空 → "__SET__";空串/None 原样(区分"配了"与 + "没配");原值绝不出现在响应。""" + _create(super_client, slug="demo") + _put_group(super_client, "demo", "push", + {"prefix": "demo-p", "api_key": "push-s3cret"}) + _put_group(super_client, "demo", "ai", {"api_key": "ai-s3cret"}) + _put_group(super_client, "demo", "email", + {"smtp": {"host": "smtp.example.com", "port": 465, + "password": "smtp-s3cret"}}) + r = super_client.get(_cfg("demo"), headers=_auth(super_client)) + assert r.status_code == 200 + cfg = r.json()["config"] + assert cfg["push"]["api_key"] == "__SET__" + assert cfg["ai"]["api_key"] == "__SET__" + assert cfg["email"]["smtp"]["password"] == "__SET__" + for secret in ("push-s3cret", "ai-s3cret", "smtp-s3cret"): + assert secret not in r.text, f"响应泄露密钥: {secret}" + + # 未设语义不受掩码遮蔽: 空串 → "";None → null + _put_group(super_client, "demo", "push", + {"prefix": "demo-p", "api_key": ""}) + _put_group(super_client, "demo", "ai", {"api_key": None}) + _put_group(super_client, "demo", "email", {"smtp": None}) + cfg = _get_config(super_client, "demo")["config"] + assert cfg["push"]["api_key"] == "" + assert cfg["ai"]["api_key"] is None + assert cfg["email"]["smtp"] is None + + +def test_put_set_sentinel_keeps_stored_value(super_client, tmp_path): + """PUT 哨兵还原:密钥字段传 "__SET__" = 保持库值(版本+1 照常);传新值 + → 覆盖;空串传回仍 = 清空(既有语义不因掩码改变)。""" + _create(super_client, slug="demo") + _put_group(super_client, "demo", "push", + {"prefix": "demo-p", "api_key": "orig-s3cret"}) # v2 + r = _put_group(super_client, "demo", "push", + {"prefix": "demo-p", "api_key": "__SET__"}) # v3 + assert r.status_code == 200, r.text + assert r.json()["version"] == 3 + assert r.json()["config"]["api_key"] == "__SET__" # PUT 响应同样掩码 + snapshot = _current_snapshot(tmp_path, "demo") + assert snapshot["push"]["api_key"] == "orig-s3cret" # 库值原样 + assert snapshot["push"]["prefix"] == "demo-p" # 非密钥字段照常写 + + # 传新值 → 覆盖;空串 → 清空 + _put_group(super_client, "demo", "push", + {"prefix": "demo-p", "api_key": "new-s3cret"}) # v4 + assert _current_snapshot(tmp_path, "demo")["push"]["api_key"] == "new-s3cret" + _put_group(super_client, "demo", "push", + {"prefix": "demo-p", "api_key": ""}) # v5 + assert _current_snapshot(tmp_path, "demo")["push"]["api_key"] == "" + + # 嵌套路径(email.smtp.password)哨兵同款 + _put_group(super_client, "demo", "email", + {"smtp": {"host": "smtp.example.com", "port": 465, + "password": "smtp-orig"}}) # v6 + r = _put_group(super_client, "demo", "email", + {"smtp": {"host": "smtp2.example.com", "port": 587, + "password": "__SET__"}}) # v7 + assert r.status_code == 200 + assert r.json()["config"]["smtp"]["password"] == "__SET__" + assert _current_snapshot(tmp_path, "demo")["email"]["smtp"] == { + "host": "smtp2.example.com", "port": 587, "password": "smtp-orig"} + + # 库值未设时哨兵还原为 None(保持"未设置"),而非落哨兵字面量 + r = _put_group(super_client, "demo", "ai", {"api_key": "__SET__"}) + assert r.status_code == 200 + assert _current_snapshot(tmp_path, "demo")["ai"]["api_key"] is None + + +def test_version_detail_and_rollback_response_masked(super_client, tmp_path): + """GET versions/{n} 与 rollback 响应的密钥字段也是 "__SET__";写入不受 + 影响——rollback 落库仍是原值(掩码只作用响应,不改历史与回滚写入)。""" + _create(super_client, slug="demo") + _put_group(super_client, "demo", "ai", {"api_key": "v2-s3cret"}) # v2 + r = super_client.get(f"{_cfg('demo')}/versions/2", + headers=_auth(super_client)) + assert r.status_code == 200 + assert r.json()["config"]["ai"]["api_key"] == "__SET__" + assert "v2-s3cret" not in r.text + + _put_group(super_client, "demo", "ai", {"api_key": "v3-s3cret"}) # v3 + r = super_client.post(f"{_cfg('demo')}/rollback", headers=_auth(super_client), + json={"version": 2}) + assert r.status_code == 200 + assert r.json()["version"] == 4 + assert r.json()["config"]["ai"]["api_key"] == "__SET__" # 响应掩码 + assert "v2-s3cret" not in r.text + # 落库仍是原值(分离断言:响应掩码 ≠ 写入掩码) + assert _current_snapshot(tmp_path, "demo")["ai"]["api_key"] == "v2-s3cret" + # 当前生效视图(GET config)也掩码 + assert _get_config(super_client, "demo")["config"]["ai"]["api_key"] == "__SET__" + + +def test_platform_settings_masked(super_client, tmp_path): + """平台设置同款:key 名含 key/password/secret 的字符串值掩码(设置 key + 本身与嵌套 dict 递归都算);PUT 哨兵还原;非密钥值原样。""" + assert super_client.put(SETTINGS, headers=_auth(super_client), + json={"key": "glm_api_key", + "value": "sk-plat-1"}).status_code == 200 + assert super_client.put(SETTINGS, headers=_auth(super_client), + json={"key": "default_smtp", "value": { + "host": "smtp.example.com", "port": 465, + "password": "smtp-pw-1", "user": "bot"}} + ).status_code == 200 + assert super_client.put(SETTINGS, headers=_auth(super_client), + json={"key": "hmac_secret", + "value": "sec-plat-1"}).status_code == 200 + assert super_client.put(SETTINGS, headers=_auth(super_client), + json={"key": "glm_timeout", + "value": 30}).status_code == 200 + r = super_client.get(SETTINGS, headers=_auth(super_client)) + assert r.status_code == 200 + items = {i["key"]: i["value"] for i in r.json()["items"]} + assert items["glm_api_key"] == "__SET__" # 设置 key 本身命中 + assert items["default_smtp"] == { # 嵌套 dict 递归 + "host": "smtp.example.com", "port": 465, + "password": "__SET__", "user": "bot"} + assert items["hmac_secret"] == "__SET__" + assert items["glm_timeout"] == 30 # 非密钥原样 + for secret in ("sk-plat-1", "smtp-pw-1", "sec-plat-1"): + assert secret not in r.text, f"响应泄露密钥: {secret}" + + # PUT 哨兵还原: 嵌套密钥位置传 "__SET__" → 库值保持,其余字段更新 + r = super_client.put(SETTINGS, headers=_auth(super_client), + json={"key": "default_smtp", "value": { + "host": "smtp2.example.com", "port": 587, + "password": "__SET__", "user": "bot2"}}) + assert r.status_code == 200 + assert r.json()["value"]["password"] == "__SET__" # 响应掩码 + rows = _db_fetch(tmp_path, select(PlatformSetting) + .where(PlatformSetting.key == "default_smtp")) + assert json.loads(rows[0][0].value) == { + "host": "smtp2.example.com", "port": 587, + "password": "smtp-pw-1", "user": "bot2"} + # 标量密钥设置哨兵还原 + r = super_client.put(SETTINGS, headers=_auth(super_client), + json={"key": "glm_api_key", "value": "__SET__"}) + assert r.status_code == 200 + rows = _db_fetch(tmp_path, select(PlatformSetting) + .where(PlatformSetting.key == "glm_api_key")) + assert json.loads(rows[0][0].value) == "sk-plat-1" + # 非密钥 key 的 "__SET__" 字面量 = 普通字符串(不还原也不掩码) + r = super_client.put(SETTINGS, headers=_auth(super_client), + json={"key": "plain_note", "value": "__SET__"}) + assert r.status_code == 200 + assert r.json()["value"] == "__SET__" + items = {i["key"]: i["value"] for i in super_client.get( + SETTINGS, headers=_auth(super_client)).json()["items"]} + assert items["plain_note"] == "__SET__" diff --git a/tests/test_tenant_admin_api.py b/tests/test_tenant_admin_api.py index f084ac8..032312e 100644 --- a/tests/test_tenant_admin_api.py +++ b/tests/test_tenant_admin_api.py @@ -508,3 +508,66 @@ def test_no_credentials_leak_in_responses(super_client): assert r.status_code == 200 for needle in ("password_hash", "salt", "scrypt", "demo-pass-1"): assert needle not in r.text, f"响应泄露凭据字段: {needle}" + + +# ---- 站点/域名管理放开本租户管理员(Phase 3 Task 1) -------------------------------- + + +def test_tenant_admin_can_manage_own_sites(super_client): + """四端点(POST sites / PATCH sites / POST domains / DELETE domains) + 放宽到 _tenant_guard:本租户管理员可自助管理站点与域名;打别的租户 + 仍 403(slug 收口)。""" + _create(super_client, slug="demo", admin="demo-admin", + password="demo-pass-1", domains=["demo.example.com"]) + _create(super_client, slug="other", profile="blank") + token = _tenant_admin_token(super_client, "demo-admin", "demo-pass-1") + h = _bearer(token) + + # POST sites → 201 + r = super_client.post(f"{TENANTS}/demo/sites", headers=h, + json={"slug": "es", "name": "ES 站", + "domains": ["es.demo.example.com"]}) + assert r.status_code == 201, r.text + # PATCH sites/{site} → 200 + r = super_client.patch(f"{TENANTS}/demo/sites/es", headers=h, + json={"name": "ES 站 v2"}) + assert r.status_code == 200 + assert r.json()["name"] == "ES 站 v2" + # POST domains → 201 + r = super_client.post(f"{TENANTS}/demo/sites/es/domains", headers=h, + json={"domain": "es2.demo.example.com"}) + assert r.status_code == 201 + # DELETE domains/{domain} → 200 + r = super_client.delete( + f"{TENANTS}/demo/sites/es/domains/es2.demo.example.com", headers=h) + assert r.status_code == 200 + + # 对照: 同 token 打别的租户四端点 → 403 + assert super_client.post(f"{TENANTS}/other/sites", headers=h, + json={"slug": "x"}).status_code == 403 + assert super_client.patch(f"{TENANTS}/other/sites/main", headers=h, + json={"name": "x"}).status_code == 403 + assert super_client.post(f"{TENANTS}/other/sites/main/domains", + headers=h, + json={"domain": "x.example.com"}).status_code == 403 + assert super_client.delete( + f"{TENANTS}/other/sites/main/domains/x.example.com", + headers=h).status_code == 403 + + +def test_tenant_lifecycle_still_super_only(super_client): + """不动清单:租户生命周期端点保持超管专属——创建 / 全平台列表 / + PATCH /tenants/{slug}(停用/改名,租户不能自停自改)。""" + _create(super_client, slug="demo", admin="demo-admin", + password="demo-pass-1") + token = _tenant_admin_token(super_client, "demo-admin", "demo-pass-1") + h = _bearer(token) + r = super_client.post(TENANTS, headers=h, json={ + "name": "越权租户", "slug": "evil", "profile": "blank", + "admin_username": "evil-admin", "admin_password": "evil-pass-1"}) + assert r.status_code == 403 + assert super_client.get(TENANTS, headers=h).status_code == 403 + assert super_client.patch(f"{TENANTS}/demo", headers=h, + json={"status": "disabled"}).status_code == 403 + assert super_client.patch(f"{TENANTS}/demo", headers=h, + json={"name": "自改名"}).status_code == 403 From 78ff0604205978835aa63f5cab83daf5b272c3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 14:51:24 +0800 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20art-design-pro=20=E8=84=9A?= =?UTF-8?q?=E6=89=8B=E6=9E=B6=E8=90=BD=E5=9C=B0=E4=B8=8E=E5=93=81=E7=89=8C?= =?UTF-8?q?=E5=8C=96=EF=BC=88=E5=85=AB=E9=A1=B5=E9=AA=A8=E6=9E=B6/?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E9=85=8D=E7=BD=AE/=E6=9D=83=E9=99=90?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 模板 art-design-pro(MIT)GitHub 直连克隆后裁剪落地 admin/:删除演示页面/示例数据/mock/未用组件,保留构建与依赖清单 - 品牌化:应用名/标题/侧栏与登录页 logo/浏览器 favicon 全部替换为 all-kefu 自有资源(static/kefu-small.png),中文界面 - 八页骨架:login / force-change-password / dashboard / tenants / conversations / config / stats / settings 占位页 + 6 组路由模块 - 权限菜单:meta.roles 前端过滤,super 全量,tenant 隐藏 租户管理/平台设置(角色值与后端一致) - 开发代理:/api 直转 + /api/public 与 /static 携带租户 Host 改写(VITE_TENANT_PREVIEW_HOST),适配后端 TenantHostMiddleware - 认证 API 为开发桩(角色调试开关),Task 3 对接真实登录 Co-Authored-By: Claude Code --- admin/.env.development | 19 + admin/.env.production | 10 + admin/.gitattributes | 2 + admin/.gitignore | 11 + admin/.prettierignore | 3 + admin/.prettierrc | 20 + admin/.stylelintignore | 9 + admin/.stylelintrc.cjs | 82 + admin/.vscode/extensions.json | 10 + admin/.vscode/settings.json | 12 + admin/CHANGELOG.md | 650 ++ admin/CHANGELOG.zh-CN.md | 654 ++ admin/LICENSE | 21 + admin/README.md | 104 + admin/README.zh-CN.md | 104 + admin/eslint.config.mjs | 83 + admin/index.html | 44 + admin/package.json | 84 + admin/pnpm-lock.yaml | 5500 +++++++++++++++++ admin/pnpm-workspace.yaml | 7 + admin/public/favicon.png | Bin 0 -> 20288 bytes admin/scripts/clean-dev.ts | 838 +++ admin/src/App.vue | 39 + admin/src/api/auth.ts | 60 + admin/src/assets/images/3d/icon1.webp | Bin 0 -> 10076 bytes admin/src/assets/images/3d/icon2.webp | Bin 0 -> 6712 bytes admin/src/assets/images/3d/icon3.webp | Bin 0 -> 5872 bytes admin/src/assets/images/3d/icon4.webp | Bin 0 -> 5762 bytes admin/src/assets/images/3d/icon5.webp | Bin 0 -> 7932 bytes admin/src/assets/images/3d/icon6.webp | Bin 0 -> 6308 bytes admin/src/assets/images/3d/icon7.webp | Bin 0 -> 8180 bytes admin/src/assets/images/3d/icon8.webp | Bin 0 -> 7678 bytes admin/src/assets/images/avatar/avatar.webp | Bin 0 -> 954 bytes admin/src/assets/images/avatar/avatar1.webp | Bin 0 -> 2296 bytes admin/src/assets/images/avatar/avatar10.webp | Bin 0 -> 1410 bytes admin/src/assets/images/avatar/avatar2.webp | Bin 0 -> 1214 bytes admin/src/assets/images/avatar/avatar3.webp | Bin 0 -> 726 bytes admin/src/assets/images/avatar/avatar4.webp | Bin 0 -> 944 bytes admin/src/assets/images/avatar/avatar5.webp | Bin 0 -> 2272 bytes admin/src/assets/images/avatar/avatar6.webp | Bin 0 -> 810 bytes admin/src/assets/images/avatar/avatar7.webp | Bin 0 -> 2712 bytes admin/src/assets/images/avatar/avatar8.webp | Bin 0 -> 3946 bytes admin/src/assets/images/avatar/avatar9.webp | Bin 0 -> 1680 bytes admin/src/assets/images/ceremony/hb.png | Bin 0 -> 2275 bytes admin/src/assets/images/ceremony/sd.png | Bin 0 -> 4752 bytes admin/src/assets/images/ceremony/xc.png | Bin 0 -> 4910 bytes admin/src/assets/images/ceremony/yd.png | Bin 0 -> 4629 bytes admin/src/assets/images/common/logo-large.png | Bin 0 -> 240149 bytes admin/src/assets/images/common/logo.png | Bin 0 -> 20288 bytes admin/src/assets/images/cover/img1.webp | Bin 0 -> 7522 bytes admin/src/assets/images/cover/img10.webp | Bin 0 -> 3526 bytes admin/src/assets/images/cover/img2.webp | Bin 0 -> 14024 bytes admin/src/assets/images/cover/img3.webp | Bin 0 -> 12128 bytes admin/src/assets/images/cover/img4.webp | Bin 0 -> 9832 bytes admin/src/assets/images/cover/img5.webp | Bin 0 -> 9478 bytes admin/src/assets/images/cover/img6.webp | Bin 0 -> 10898 bytes admin/src/assets/images/cover/img7.webp | Bin 0 -> 8694 bytes admin/src/assets/images/cover/img8.webp | Bin 0 -> 9580 bytes admin/src/assets/images/cover/img9.webp | Bin 0 -> 9212 bytes admin/src/assets/images/draw/draw1.png | Bin 0 -> 11315 bytes admin/src/assets/images/lock/bg_dark.webp | Bin 0 -> 70592 bytes admin/src/assets/images/lock/bg_light.webp | Bin 0 -> 67246 bytes admin/src/assets/images/login/lf_icon2.webp | Bin 0 -> 25016 bytes admin/src/assets/images/safeguard/server.png | Bin 0 -> 2178 bytes .../settings/menu_layouts/dual_column.png | Bin 0 -> 514 bytes .../settings/menu_layouts/horizontal.png | Bin 0 -> 409 bytes .../images/settings/menu_layouts/mixed.png | Bin 0 -> 431 bytes .../images/settings/menu_layouts/vertical.png | Bin 0 -> 439 bytes .../images/settings/menu_styles/dark.png | Bin 0 -> 292 bytes .../images/settings/menu_styles/design.png | Bin 0 -> 286 bytes .../images/settings/menu_styles/light.png | Bin 0 -> 293 bytes .../images/settings/theme_styles/dark.png | Bin 0 -> 448 bytes .../images/settings/theme_styles/light.png | Bin 0 -> 416 bytes .../images/settings/theme_styles/system.png | Bin 0 -> 509 bytes admin/src/assets/images/svg/403.svg | 1 + admin/src/assets/images/svg/404.svg | 1 + admin/src/assets/images/svg/500.svg | 5 + admin/src/assets/images/svg/login_icon.svg | 1 + admin/src/assets/images/user/avatar.webp | Bin 0 -> 2130 bytes admin/src/assets/images/user/bg.webp | Bin 0 -> 12352 bytes admin/src/assets/styles/core/app.scss | 292 + admin/src/assets/styles/core/dark.scss | 93 + admin/src/assets/styles/core/el-dark.scss | 2 + admin/src/assets/styles/core/el-light.scss | 34 + admin/src/assets/styles/core/el-ui.scss | 519 ++ admin/src/assets/styles/core/md.scss | 1036 ++++ admin/src/assets/styles/core/mixin.scss | 157 + admin/src/assets/styles/core/reset.scss | 41 + .../assets/styles/core/router-transition.scss | 104 + admin/src/assets/styles/core/tailwind.css | 208 + .../assets/styles/core/theme-animation.scss | 63 + .../src/assets/styles/core/theme-change.scss | 11 + .../assets/styles/custom/one-dark-pro.scss | 98 + admin/src/assets/styles/index.scss | 23 + admin/src/assets/svg/loading.ts | 32 + .../core/banners/art-basic-banner/index.vue | 343 + .../core/banners/art-card-banner/index.vue | 114 + .../core/base/art-back-to-top/index.vue | 40 + .../components/core/base/art-logo/index.vue | 52 + .../core/base/art-svg-icon/index.vue | 24 + .../core/cards/art-bar-chart-card/index.vue | 103 + .../core/cards/art-data-list-card/index.vue | 74 + .../core/cards/art-donut-chart-card/index.vue | 124 + .../core/cards/art-image-card/index.vue | 89 + .../core/cards/art-line-chart-card/index.vue | 126 + .../core/cards/art-progress-card/index.vue | 86 + .../core/cards/art-stats-card/index.vue | 67 + .../cards/art-timeline-list-card/index.vue | 69 + .../core/charts/art-bar-chart/index.vue | 203 + .../art-dual-bar-compare-chart/index.vue | 195 + .../core/charts/art-h-bar-chart/index.vue | 208 + .../core/charts/art-k-line-chart/index.vue | 152 + .../core/charts/art-line-chart/index.vue | 371 ++ .../core/charts/art-radar-chart/index.vue | 105 + .../core/charts/art-ring-chart/index.vue | 133 + .../core/charts/art-scatter-chart/index.vue | 115 + .../core/forms/art-button-more/index.vue | 71 + .../core/forms/art-button-table/index.vue | 59 + .../core/forms/art-drag-verify/index.vue | 430 ++ .../components/core/forms/art-form/index.vue | 507 ++ .../core/forms/art-search-bar/index.vue | 581 ++ .../core/layouts/art-breadcrumb/index.vue | 142 + .../core/layouts/art-chat-window/index.vue | 262 + .../core/layouts/art-fast-enter/index.vue | 113 + .../layouts/art-fireworks-effect/index.vue | 633 ++ .../layouts/art-global-component/index.vue | 14 + .../core/layouts/art-global-search/index.vue | 431 ++ .../core/layouts/art-header-bar/index.vue | 485 ++ .../art-header-bar/widget/ArtUserMenu.vue | 144 + .../art-menus/art-horizontal-menu/index.vue | 110 + .../widget/HorizontalSubmenu.vue | 106 + .../art-menus/art-mixed-menu/index.vue | 279 + .../art-menus/art-sidebar-menu/index.vue | 369 ++ .../art-menus/art-sidebar-menu/style.scss | 251 + .../art-menus/art-sidebar-menu/theme.scss | 258 + .../widget/SidebarSubmenu.vue | 199 + .../core/layouts/art-notification/index.vue | 456 ++ .../core/layouts/art-page-content/index.vue | 136 + .../core/layouts/art-screen-lock/index.vue | 519 ++ .../composables/useSettingsConfig.ts | 248 + .../composables/useSettingsHandlers.ts | 167 + .../composables/useSettingsPanel.ts | 233 + .../composables/useSettingsState.ts | 37 + .../core/layouts/art-settings-panel/index.vue | 72 + .../layouts/art-settings-panel/style.scss | 92 + .../widget/BasicSettings.vue | 77 + .../widget/BoxStyleSettings.vue | 38 + .../widget/ColorSettings.vue | 35 + .../widget/ContainerSettings.vue | 33 + .../widget/MenuLayoutSettings.vue | 31 + .../widget/MenuStyleSettings.vue | 44 + .../widget/SectionTitle.vue | 17 + .../widget/SettingActions.vue | 235 + .../widget/SettingDrawer.vue | 51 + .../widget/SettingHeader.vue | 18 + .../art-settings-panel/widget/SettingItem.vue | 101 + .../widget/ThemeSettings.vue | 28 + .../core/layouts/art-work-tab/index.vue | 584 ++ .../core/others/art-menu-right/index.vue | 415 ++ .../core/others/art-watermark/index.vue | 64 + .../core/tables/art-table-header/index.vue | 339 + .../core/tables/art-table/index.vue | 368 ++ .../core/tables/art-table/style.scss | 99 + .../core/text-effect/art-count-to/index.vue | 310 + .../art-festival-text-scroll/index.vue | 32 + .../text-effect/art-text-scroll/index.vue | 285 + .../components/core/theme/theme-svg/index.vue | 100 + .../core/views/exception/ArtException.vue | 55 + .../core/views/login/AuthTopBar.vue | 149 + .../core/views/login/LoginLeftView.vue | 602 ++ .../core/views/result/ArtResultPage.vue | 43 + .../core/widget/art-icon-button/index.vue | 23 + admin/src/config/assets/images.ts | 61 + admin/src/config/index.ts | 135 + admin/src/config/modules/component.ts | 105 + admin/src/config/modules/fastEnter.ts | 66 + admin/src/config/modules/festival.ts | 51 + admin/src/config/modules/headerBar.ts | 63 + admin/src/config/setting.ts | 109 + admin/src/directives/business/ripple.ts | 116 + admin/src/directives/core/auth.ts | 66 + admin/src/directives/core/roles.ts | 87 + admin/src/directives/index.ts | 12 + admin/src/enums/appEnum.ts | 81 + admin/src/enums/formEnum.ts | 24 + admin/src/env.d.ts | 8 + admin/src/hooks/core/useAppMode.ts | 45 + admin/src/hooks/core/useAuth.ts | 74 + admin/src/hooks/core/useCeremony.ts | 184 + admin/src/hooks/core/useChart.ts | 745 +++ admin/src/hooks/core/useCommon.ts | 87 + admin/src/hooks/core/useFastEnter.ts | 55 + admin/src/hooks/core/useHeaderBar.ts | 201 + admin/src/hooks/core/useLayoutHeight.ts | 148 + admin/src/hooks/core/useTable.ts | 762 +++ admin/src/hooks/core/useTableColumns.ts | 312 + admin/src/hooks/core/useTableHeight.ts | 105 + admin/src/hooks/core/useTheme.ts | 174 + admin/src/hooks/index.ts | 32 + admin/src/locales/index.ts | 123 + admin/src/locales/langs/en.json | 257 + admin/src/locales/langs/zh.json | 257 + admin/src/main.ts | 25 + admin/src/plugins/echarts.ts | 76 + admin/src/plugins/index.ts | 6 + admin/src/router/core/ComponentLoader.ts | 82 + admin/src/router/core/IframeRouteManager.ts | 78 + admin/src/router/core/MenuProcessor.ts | 264 + .../router/core/RoutePermissionValidator.ts | 161 + admin/src/router/core/RouteRegistry.ts | 90 + admin/src/router/core/RouteTransformer.ts | 132 + admin/src/router/core/RouteValidator.ts | 187 + admin/src/router/core/index.ts | 14 + admin/src/router/guards/afterEach.ts | 34 + admin/src/router/guards/beforeEach.ts | 418 ++ admin/src/router/index.ts | 23 + admin/src/router/modules/config.ts | 24 + admin/src/router/modules/conversations.ts | 24 + admin/src/router/modules/dashboard.ts | 24 + admin/src/router/modules/index.ts | 20 + admin/src/router/modules/settings.ts | 25 + admin/src/router/modules/stats.ts | 24 + admin/src/router/modules/tenants.ts | 25 + admin/src/router/routes/asyncRoutes.ts | 9 + admin/src/router/routes/staticRoutes.ts | 66 + admin/src/router/routesAlias.ts | 9 + admin/src/store/index.ts | 52 + admin/src/store/modules/menu.ts | 109 + admin/src/store/modules/setting.ts | 450 ++ admin/src/store/modules/table.ts | 97 + admin/src/store/modules/user.ts | 235 + admin/src/store/modules/worktab.ts | 568 ++ admin/src/types/api/api.d.ts | 138 + admin/src/types/common/index.ts | 95 + admin/src/types/common/response.ts | 30 + admin/src/types/component/chart.ts | 324 + admin/src/types/component/index.ts | 145 + admin/src/types/config/index.ts | 211 + admin/src/types/directive/directive.d.ts | 15 + admin/src/types/index.ts | 22 + admin/src/types/router/index.ts | 80 + admin/src/types/store/index.ts | 157 + admin/src/utils/form/index.ts | 12 + admin/src/utils/form/responsive.ts | 122 + admin/src/utils/form/validator.ts | 316 + admin/src/utils/http/error.ts | 182 + admin/src/utils/http/index.ts | 214 + admin/src/utils/http/status.ts | 18 + admin/src/utils/index.ts | 31 + admin/src/utils/navigation/index.ts | 10 + admin/src/utils/navigation/jump.ts | 68 + admin/src/utils/navigation/route.ts | 85 + admin/src/utils/navigation/worktab.ts | 67 + admin/src/utils/router.ts | 61 + admin/src/utils/socket/index.ts | 423 ++ admin/src/utils/storage/index.ts | 7 + admin/src/utils/storage/storage-config.ts | 125 + .../src/utils/storage/storage-key-manager.ts | 97 + admin/src/utils/storage/storage.ts | 250 + admin/src/utils/sys/console.ts | 9 + admin/src/utils/sys/error-handle.ts | 143 + admin/src/utils/sys/index.ts | 5 + admin/src/utils/sys/mittBus.ts | 63 + admin/src/utils/table/tableCache.ts | 266 + admin/src/utils/table/tableConfig.ts | 55 + admin/src/utils/table/tableUtils.ts | 297 + admin/src/utils/ui/animation.ts | 80 + admin/src/utils/ui/colors.ts | 273 + admin/src/utils/ui/emojo.ts | 24 + admin/src/utils/ui/iconify-loader.ts | 31 + admin/src/utils/ui/index.ts | 11 + admin/src/utils/ui/loading.ts | 84 + admin/src/utils/ui/tabs.ts | 60 + .../auth/force-change-password/index.vue | 95 + admin/src/views/auth/login/index.vue | 221 + admin/src/views/auth/login/style.css | 38 + admin/src/views/config/center/index.vue | 25 + admin/src/views/conversations/list/index.vue | 25 + admin/src/views/dashboard/console/index.vue | 25 + admin/src/views/exception/403/index.vue | 16 + admin/src/views/exception/404/index.vue | 16 + admin/src/views/exception/500/index.vue | 16 + admin/src/views/index/index.vue | 29 + admin/src/views/index/style.scss | 93 + admin/src/views/outside/Iframe.vue | 42 + admin/src/views/settings/platform/index.vue | 25 + admin/src/views/stats/overview/index.vue | 25 + admin/src/views/tenants/list/index.vue | 25 + admin/tsconfig.json | 28 + admin/vite.config.ts | 175 + 290 files changed, 38981 insertions(+) create mode 100644 admin/.env.development create mode 100644 admin/.env.production create mode 100644 admin/.gitattributes create mode 100644 admin/.gitignore create mode 100644 admin/.prettierignore create mode 100644 admin/.prettierrc create mode 100644 admin/.stylelintignore create mode 100644 admin/.stylelintrc.cjs create mode 100644 admin/.vscode/extensions.json create mode 100644 admin/.vscode/settings.json create mode 100644 admin/CHANGELOG.md create mode 100644 admin/CHANGELOG.zh-CN.md create mode 100644 admin/LICENSE create mode 100644 admin/README.md create mode 100644 admin/README.zh-CN.md create mode 100644 admin/eslint.config.mjs create mode 100644 admin/index.html create mode 100644 admin/package.json create mode 100644 admin/pnpm-lock.yaml create mode 100644 admin/pnpm-workspace.yaml create mode 100644 admin/public/favicon.png create mode 100644 admin/scripts/clean-dev.ts create mode 100644 admin/src/App.vue create mode 100644 admin/src/api/auth.ts create mode 100644 admin/src/assets/images/3d/icon1.webp create mode 100644 admin/src/assets/images/3d/icon2.webp create mode 100644 admin/src/assets/images/3d/icon3.webp create mode 100644 admin/src/assets/images/3d/icon4.webp create mode 100644 admin/src/assets/images/3d/icon5.webp create mode 100644 admin/src/assets/images/3d/icon6.webp create mode 100644 admin/src/assets/images/3d/icon7.webp create mode 100644 admin/src/assets/images/3d/icon8.webp create mode 100644 admin/src/assets/images/avatar/avatar.webp create mode 100644 admin/src/assets/images/avatar/avatar1.webp create mode 100644 admin/src/assets/images/avatar/avatar10.webp create mode 100644 admin/src/assets/images/avatar/avatar2.webp create mode 100644 admin/src/assets/images/avatar/avatar3.webp create mode 100644 admin/src/assets/images/avatar/avatar4.webp create mode 100644 admin/src/assets/images/avatar/avatar5.webp create mode 100644 admin/src/assets/images/avatar/avatar6.webp create mode 100644 admin/src/assets/images/avatar/avatar7.webp create mode 100644 admin/src/assets/images/avatar/avatar8.webp create mode 100644 admin/src/assets/images/avatar/avatar9.webp create mode 100644 admin/src/assets/images/ceremony/hb.png create mode 100644 admin/src/assets/images/ceremony/sd.png create mode 100644 admin/src/assets/images/ceremony/xc.png create mode 100644 admin/src/assets/images/ceremony/yd.png create mode 100644 admin/src/assets/images/common/logo-large.png create mode 100644 admin/src/assets/images/common/logo.png create mode 100644 admin/src/assets/images/cover/img1.webp create mode 100644 admin/src/assets/images/cover/img10.webp create mode 100644 admin/src/assets/images/cover/img2.webp create mode 100644 admin/src/assets/images/cover/img3.webp create mode 100644 admin/src/assets/images/cover/img4.webp create mode 100644 admin/src/assets/images/cover/img5.webp create mode 100644 admin/src/assets/images/cover/img6.webp create mode 100644 admin/src/assets/images/cover/img7.webp create mode 100644 admin/src/assets/images/cover/img8.webp create mode 100644 admin/src/assets/images/cover/img9.webp create mode 100644 admin/src/assets/images/draw/draw1.png create mode 100644 admin/src/assets/images/lock/bg_dark.webp create mode 100644 admin/src/assets/images/lock/bg_light.webp create mode 100644 admin/src/assets/images/login/lf_icon2.webp create mode 100644 admin/src/assets/images/safeguard/server.png create mode 100644 admin/src/assets/images/settings/menu_layouts/dual_column.png create mode 100644 admin/src/assets/images/settings/menu_layouts/horizontal.png create mode 100644 admin/src/assets/images/settings/menu_layouts/mixed.png create mode 100644 admin/src/assets/images/settings/menu_layouts/vertical.png create mode 100644 admin/src/assets/images/settings/menu_styles/dark.png create mode 100644 admin/src/assets/images/settings/menu_styles/design.png create mode 100644 admin/src/assets/images/settings/menu_styles/light.png create mode 100644 admin/src/assets/images/settings/theme_styles/dark.png create mode 100644 admin/src/assets/images/settings/theme_styles/light.png create mode 100644 admin/src/assets/images/settings/theme_styles/system.png create mode 100644 admin/src/assets/images/svg/403.svg create mode 100644 admin/src/assets/images/svg/404.svg create mode 100644 admin/src/assets/images/svg/500.svg create mode 100644 admin/src/assets/images/svg/login_icon.svg create mode 100644 admin/src/assets/images/user/avatar.webp create mode 100644 admin/src/assets/images/user/bg.webp create mode 100644 admin/src/assets/styles/core/app.scss create mode 100644 admin/src/assets/styles/core/dark.scss create mode 100644 admin/src/assets/styles/core/el-dark.scss create mode 100644 admin/src/assets/styles/core/el-light.scss create mode 100644 admin/src/assets/styles/core/el-ui.scss create mode 100644 admin/src/assets/styles/core/md.scss create mode 100644 admin/src/assets/styles/core/mixin.scss create mode 100644 admin/src/assets/styles/core/reset.scss create mode 100644 admin/src/assets/styles/core/router-transition.scss create mode 100644 admin/src/assets/styles/core/tailwind.css create mode 100644 admin/src/assets/styles/core/theme-animation.scss create mode 100644 admin/src/assets/styles/core/theme-change.scss create mode 100644 admin/src/assets/styles/custom/one-dark-pro.scss create mode 100644 admin/src/assets/styles/index.scss create mode 100644 admin/src/assets/svg/loading.ts create mode 100644 admin/src/components/core/banners/art-basic-banner/index.vue create mode 100644 admin/src/components/core/banners/art-card-banner/index.vue create mode 100644 admin/src/components/core/base/art-back-to-top/index.vue create mode 100644 admin/src/components/core/base/art-logo/index.vue create mode 100644 admin/src/components/core/base/art-svg-icon/index.vue create mode 100644 admin/src/components/core/cards/art-bar-chart-card/index.vue create mode 100644 admin/src/components/core/cards/art-data-list-card/index.vue create mode 100644 admin/src/components/core/cards/art-donut-chart-card/index.vue create mode 100644 admin/src/components/core/cards/art-image-card/index.vue create mode 100644 admin/src/components/core/cards/art-line-chart-card/index.vue create mode 100644 admin/src/components/core/cards/art-progress-card/index.vue create mode 100644 admin/src/components/core/cards/art-stats-card/index.vue create mode 100644 admin/src/components/core/cards/art-timeline-list-card/index.vue create mode 100644 admin/src/components/core/charts/art-bar-chart/index.vue create mode 100644 admin/src/components/core/charts/art-dual-bar-compare-chart/index.vue create mode 100644 admin/src/components/core/charts/art-h-bar-chart/index.vue create mode 100644 admin/src/components/core/charts/art-k-line-chart/index.vue create mode 100644 admin/src/components/core/charts/art-line-chart/index.vue create mode 100644 admin/src/components/core/charts/art-radar-chart/index.vue create mode 100644 admin/src/components/core/charts/art-ring-chart/index.vue create mode 100644 admin/src/components/core/charts/art-scatter-chart/index.vue create mode 100644 admin/src/components/core/forms/art-button-more/index.vue create mode 100644 admin/src/components/core/forms/art-button-table/index.vue create mode 100644 admin/src/components/core/forms/art-drag-verify/index.vue create mode 100644 admin/src/components/core/forms/art-form/index.vue create mode 100644 admin/src/components/core/forms/art-search-bar/index.vue create mode 100644 admin/src/components/core/layouts/art-breadcrumb/index.vue create mode 100644 admin/src/components/core/layouts/art-chat-window/index.vue create mode 100644 admin/src/components/core/layouts/art-fast-enter/index.vue create mode 100644 admin/src/components/core/layouts/art-fireworks-effect/index.vue create mode 100644 admin/src/components/core/layouts/art-global-component/index.vue create mode 100644 admin/src/components/core/layouts/art-global-search/index.vue create mode 100644 admin/src/components/core/layouts/art-header-bar/index.vue create mode 100644 admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue create mode 100644 admin/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue create mode 100644 admin/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue create mode 100644 admin/src/components/core/layouts/art-menus/art-mixed-menu/index.vue create mode 100644 admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue create mode 100644 admin/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss create mode 100644 admin/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss create mode 100644 admin/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue create mode 100644 admin/src/components/core/layouts/art-notification/index.vue create mode 100644 admin/src/components/core/layouts/art-page-content/index.vue create mode 100644 admin/src/components/core/layouts/art-screen-lock/index.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts create mode 100644 admin/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts create mode 100644 admin/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts create mode 100644 admin/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts create mode 100644 admin/src/components/core/layouts/art-settings-panel/index.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/style.scss create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue create mode 100644 admin/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue create mode 100644 admin/src/components/core/layouts/art-work-tab/index.vue create mode 100644 admin/src/components/core/others/art-menu-right/index.vue create mode 100644 admin/src/components/core/others/art-watermark/index.vue create mode 100644 admin/src/components/core/tables/art-table-header/index.vue create mode 100644 admin/src/components/core/tables/art-table/index.vue create mode 100644 admin/src/components/core/tables/art-table/style.scss create mode 100644 admin/src/components/core/text-effect/art-count-to/index.vue create mode 100644 admin/src/components/core/text-effect/art-festival-text-scroll/index.vue create mode 100644 admin/src/components/core/text-effect/art-text-scroll/index.vue create mode 100644 admin/src/components/core/theme/theme-svg/index.vue create mode 100644 admin/src/components/core/views/exception/ArtException.vue create mode 100644 admin/src/components/core/views/login/AuthTopBar.vue create mode 100644 admin/src/components/core/views/login/LoginLeftView.vue create mode 100644 admin/src/components/core/views/result/ArtResultPage.vue create mode 100644 admin/src/components/core/widget/art-icon-button/index.vue create mode 100644 admin/src/config/assets/images.ts create mode 100644 admin/src/config/index.ts create mode 100644 admin/src/config/modules/component.ts create mode 100644 admin/src/config/modules/fastEnter.ts create mode 100644 admin/src/config/modules/festival.ts create mode 100644 admin/src/config/modules/headerBar.ts create mode 100644 admin/src/config/setting.ts create mode 100644 admin/src/directives/business/ripple.ts create mode 100644 admin/src/directives/core/auth.ts create mode 100644 admin/src/directives/core/roles.ts create mode 100644 admin/src/directives/index.ts create mode 100644 admin/src/enums/appEnum.ts create mode 100644 admin/src/enums/formEnum.ts create mode 100644 admin/src/env.d.ts create mode 100644 admin/src/hooks/core/useAppMode.ts create mode 100644 admin/src/hooks/core/useAuth.ts create mode 100644 admin/src/hooks/core/useCeremony.ts create mode 100644 admin/src/hooks/core/useChart.ts create mode 100644 admin/src/hooks/core/useCommon.ts create mode 100644 admin/src/hooks/core/useFastEnter.ts create mode 100644 admin/src/hooks/core/useHeaderBar.ts create mode 100644 admin/src/hooks/core/useLayoutHeight.ts create mode 100644 admin/src/hooks/core/useTable.ts create mode 100644 admin/src/hooks/core/useTableColumns.ts create mode 100644 admin/src/hooks/core/useTableHeight.ts create mode 100644 admin/src/hooks/core/useTheme.ts create mode 100644 admin/src/hooks/index.ts create mode 100644 admin/src/locales/index.ts create mode 100644 admin/src/locales/langs/en.json create mode 100644 admin/src/locales/langs/zh.json create mode 100644 admin/src/main.ts create mode 100644 admin/src/plugins/echarts.ts create mode 100644 admin/src/plugins/index.ts create mode 100644 admin/src/router/core/ComponentLoader.ts create mode 100644 admin/src/router/core/IframeRouteManager.ts create mode 100644 admin/src/router/core/MenuProcessor.ts create mode 100644 admin/src/router/core/RoutePermissionValidator.ts create mode 100644 admin/src/router/core/RouteRegistry.ts create mode 100644 admin/src/router/core/RouteTransformer.ts create mode 100644 admin/src/router/core/RouteValidator.ts create mode 100644 admin/src/router/core/index.ts create mode 100644 admin/src/router/guards/afterEach.ts create mode 100644 admin/src/router/guards/beforeEach.ts create mode 100644 admin/src/router/index.ts create mode 100644 admin/src/router/modules/config.ts create mode 100644 admin/src/router/modules/conversations.ts create mode 100644 admin/src/router/modules/dashboard.ts create mode 100644 admin/src/router/modules/index.ts create mode 100644 admin/src/router/modules/settings.ts create mode 100644 admin/src/router/modules/stats.ts create mode 100644 admin/src/router/modules/tenants.ts create mode 100644 admin/src/router/routes/asyncRoutes.ts create mode 100644 admin/src/router/routes/staticRoutes.ts create mode 100644 admin/src/router/routesAlias.ts create mode 100644 admin/src/store/index.ts create mode 100644 admin/src/store/modules/menu.ts create mode 100644 admin/src/store/modules/setting.ts create mode 100644 admin/src/store/modules/table.ts create mode 100644 admin/src/store/modules/user.ts create mode 100644 admin/src/store/modules/worktab.ts create mode 100644 admin/src/types/api/api.d.ts create mode 100644 admin/src/types/common/index.ts create mode 100644 admin/src/types/common/response.ts create mode 100644 admin/src/types/component/chart.ts create mode 100644 admin/src/types/component/index.ts create mode 100644 admin/src/types/config/index.ts create mode 100644 admin/src/types/directive/directive.d.ts create mode 100644 admin/src/types/index.ts create mode 100644 admin/src/types/router/index.ts create mode 100644 admin/src/types/store/index.ts create mode 100644 admin/src/utils/form/index.ts create mode 100644 admin/src/utils/form/responsive.ts create mode 100644 admin/src/utils/form/validator.ts create mode 100644 admin/src/utils/http/error.ts create mode 100644 admin/src/utils/http/index.ts create mode 100644 admin/src/utils/http/status.ts create mode 100644 admin/src/utils/index.ts create mode 100644 admin/src/utils/navigation/index.ts create mode 100644 admin/src/utils/navigation/jump.ts create mode 100644 admin/src/utils/navigation/route.ts create mode 100644 admin/src/utils/navigation/worktab.ts create mode 100644 admin/src/utils/router.ts create mode 100644 admin/src/utils/socket/index.ts create mode 100644 admin/src/utils/storage/index.ts create mode 100644 admin/src/utils/storage/storage-config.ts create mode 100644 admin/src/utils/storage/storage-key-manager.ts create mode 100644 admin/src/utils/storage/storage.ts create mode 100644 admin/src/utils/sys/console.ts create mode 100644 admin/src/utils/sys/error-handle.ts create mode 100644 admin/src/utils/sys/index.ts create mode 100644 admin/src/utils/sys/mittBus.ts create mode 100644 admin/src/utils/table/tableCache.ts create mode 100644 admin/src/utils/table/tableConfig.ts create mode 100644 admin/src/utils/table/tableUtils.ts create mode 100644 admin/src/utils/ui/animation.ts create mode 100644 admin/src/utils/ui/colors.ts create mode 100644 admin/src/utils/ui/emojo.ts create mode 100644 admin/src/utils/ui/iconify-loader.ts create mode 100644 admin/src/utils/ui/index.ts create mode 100644 admin/src/utils/ui/loading.ts create mode 100644 admin/src/utils/ui/tabs.ts create mode 100644 admin/src/views/auth/force-change-password/index.vue create mode 100644 admin/src/views/auth/login/index.vue create mode 100644 admin/src/views/auth/login/style.css create mode 100644 admin/src/views/config/center/index.vue create mode 100644 admin/src/views/conversations/list/index.vue create mode 100644 admin/src/views/dashboard/console/index.vue create mode 100644 admin/src/views/exception/403/index.vue create mode 100644 admin/src/views/exception/404/index.vue create mode 100644 admin/src/views/exception/500/index.vue create mode 100644 admin/src/views/index/index.vue create mode 100644 admin/src/views/index/style.scss create mode 100644 admin/src/views/outside/Iframe.vue create mode 100644 admin/src/views/settings/platform/index.vue create mode 100644 admin/src/views/stats/overview/index.vue create mode 100644 admin/src/views/tenants/list/index.vue create mode 100644 admin/tsconfig.json create mode 100644 admin/vite.config.ts diff --git a/admin/.env.development b/admin/.env.development new file mode 100644 index 0000000..a88ef43 --- /dev/null +++ b/admin/.env.development @@ -0,0 +1,19 @@ +# 【开发】环境变量 + +# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/) +VITE_BASE_URL = / + +# API 请求基础路径(开发环境使用 / 走 Vite 代理,见 vite.config.ts 的 server.proxy) +VITE_API_URL = / + +# 代理目标地址(后端 FastAPI 本地服务) +VITE_API_PROXY_URL = http://127.0.0.1:8018 + +# 开发期租户预览域名——/api/public 与 /static 的代理 Host 改写目标。 +# 后端 TenantHostMiddleware 只豁免 /health 与 /api/platform/ 前缀, +# 其余路径按 Host 解析租户;经代理直连 127.0.0.1:8018 会因未知 Host 404, +# 故这两条代理用 headers.Host 指向种子租户(jiancai)的域名。 +VITE_TENANT_PREVIEW_HOST = chat2.agent.hnxinyouhe.com + +# Delete console +VITE_DROP_CONSOLE = false diff --git a/admin/.env.production b/admin/.env.production new file mode 100644 index 0000000..44972c8 --- /dev/null +++ b/admin/.env.production @@ -0,0 +1,10 @@ +# 【生产】环境变量 + +# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/) +VITE_BASE_URL = / + +# API 地址前缀(生产由反代把 /api 与 /static 指向后端,前端同源相对路径) +VITE_API_URL = / + +# Delete console +VITE_DROP_CONSOLE = true diff --git a/admin/.gitattributes b/admin/.gitattributes new file mode 100644 index 0000000..866e8ee --- /dev/null +++ b/admin/.gitattributes @@ -0,0 +1,2 @@ +*.html linguist-detectable=false +*.vue linguist-detectable=true diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 0000000..e48d2e9 --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,11 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.cursorrules + +# Auto-generated files +src/types/import/auto-imports.d.ts +src/types/import/components.d.ts +.auto-import.json diff --git a/admin/.prettierignore b/admin/.prettierignore new file mode 100644 index 0000000..9e96efc --- /dev/null +++ b/admin/.prettierignore @@ -0,0 +1,3 @@ +/node_modules/* +/dist/* +/src/main.ts \ No newline at end of file diff --git a/admin/.prettierrc b/admin/.prettierrc new file mode 100644 index 0000000..f3d6ad5 --- /dev/null +++ b/admin/.prettierrc @@ -0,0 +1,20 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "vueIndentScriptAndStyle": true, + "singleQuote": true, + "quoteProps": "as-needed", + "bracketSpacing": true, + "trailingComma": "none", + "bracketSameLine": false, + "jsxSingleQuote": false, + "arrowParens": "always", + "insertPragma": false, + "requirePragma": false, + "proseWrap": "never", + "htmlWhitespaceSensitivity": "strict", + "endOfLine": "auto", + "rangeStart": 0 +} diff --git a/admin/.stylelintignore b/admin/.stylelintignore new file mode 100644 index 0000000..476ea45 --- /dev/null +++ b/admin/.stylelintignore @@ -0,0 +1,9 @@ +dist +node_modules +public +.husky +.vscode + +src/components/Layout/MenuLeft/index.vue +src/assets +stats.html \ No newline at end of file diff --git a/admin/.stylelintrc.cjs b/admin/.stylelintrc.cjs new file mode 100644 index 0000000..9dbea0b --- /dev/null +++ b/admin/.stylelintrc.cjs @@ -0,0 +1,82 @@ +module.exports = { + // 继承推荐规范配置 + extends: [ + 'stylelint-config-standard', + 'stylelint-config-recommended-scss', + 'stylelint-config-recommended-vue/scss', + 'stylelint-config-html/vue', + 'stylelint-config-recess-order' + ], + // 指定不同文件对应的解析器 + overrides: [ + { + files: ['**/*.{vue,html}'], + customSyntax: 'postcss-html' + }, + { + files: ['**/*.{css,scss}'], + customSyntax: 'postcss-scss' + } + ], + // 自定义规则 + rules: { + 'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url") + 'selector-class-pattern': null, // 选择器类名命名规则 + 'custom-property-pattern': null, // 自定义属性命名规则 + 'keyframes-name-pattern': null, // 动画帧节点样式命名规则 + 'no-descending-specificity': null, // 允许无降序特异性 + 'no-empty-source': null, // 允许空样式 + 'property-no-vendor-prefix': null, // 允许属性前缀 + // 允许 global 、export 、deep伪类 + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global', 'export', 'deep'] + } + ], + // 允许未知属性 + 'property-no-unknown': [ + true, + { + ignoreProperties: [] + } + ], + // 允许未知规则 + 'at-rule-no-unknown': [ + true, + { + ignoreAtRules: [ + 'apply', + 'use', + 'mixin', + 'include', + 'extend', + 'each', + 'if', + 'else', + 'for', + 'while', + 'reference' + ] + } + ], + 'scss/at-rule-no-unknown': [ + true, + { + ignoreAtRules: [ + 'apply', + 'use', + 'mixin', + 'include', + 'extend', + 'each', + 'if', + 'else', + 'for', + 'while', + 'reference' + ] + } + ] + } +} diff --git a/admin/.vscode/extensions.json b/admin/.vscode/extensions.json new file mode 100644 index 0000000..d7766fa --- /dev/null +++ b/admin/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "vue.volar", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "stylelint.vscode-stylelint", + "lokalise.i18n-ally", + "bradlc.vscode-tailwindcss" + ] +} diff --git a/admin/.vscode/settings.json b/admin/.vscode/settings.json new file mode 100644 index 0000000..5268fc2 --- /dev/null +++ b/admin/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "volar.inlayHints.eventArgumentInInlineHandlers": true, + "css.lint.unknownAtRules": "ignore", + "i18n-ally.localesPaths": ["src/locales/langs", "src/locales"], + "i18n-ally.enabledParsers": ["json"], + "i18n-ally.sourceLanguage": "en", + "i18n-ally.displayLanguage": "zh", + "i18n-ally.enabledFrameworks": ["vue", "react"], + "i18n-ally.keystyle": "nested", + "i18n-ally.sortKeys": true, + "i18n-ally.namespace": true +} diff --git a/admin/CHANGELOG.md b/admin/CHANGELOG.md new file mode 100644 index 0000000..05d5d90 --- /dev/null +++ b/admin/CHANGELOG.md @@ -0,0 +1,650 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Released] + +### v3.0.1 - 2025-11-15 + +#### Fixed + +- Fixed repeated API calls issue when accessing non-existent interfaces during route registration +- Fixed one-click cleanup script build failure issue + +#### Optimized + +- Enhanced route configuration validation mechanism, automatically detects and prompts path configuration errors for non-first-level menus +- Optimized top progress bar ghosting issue +- Optimized vite pre-build configuration +- Unified border radius and border consistency +- Redesigned lock screen page, improved visual experience +- Optimized visual experience issue caused by logout menu disappearing prematurely + +#### Added + +- ArtForm and ArtSearchBar component labels support custom render functions, enabling complex labels with tooltips +- Menu management form key fields added Tooltip hints, lowering user configuration threshold +- Iconify added offline icon loading mode +- Logout added redirect property for redirecting to corresponding page after re-login +- Logout and re-entry preserves user tabs, identified by userId +- Dual menu added collapse button +- Enhanced menu icons +- Multi-tab pages added icon display +- Added WebSocket connection example +- Lock screen password error triggers input box shake animation + +#### ⚠️ Important Notice + +> This version requires re-login + +--- + +### v3.0.0 - 2025-11-09 🎉 + +#### Refactored + +- Style system refactored: Sass fully migrated to Tailwind CSS, improving development efficiency and style consistency +- Icon solution upgraded: Iconfont replaced with Iconify, supporting richer icon libraries and on-demand loading +- Route registration refactored: Comprehensive refactoring of route registration system with object-oriented design, improving code maintainability, testability and extensibility +- Refactored ArtTextScroll component, improving performance and maintainability +- Refactored color system, unified UI visual specifications, improving interface consistency + +#### Optimized + +- Build optimization: Complete package size reduced by 1.3 MB, significantly improving loading performance +- Architecture optimization: Optimized directory structure with clearer responsibility division, reducing user learning curve and onboarding difficulty +- Comment optimization: Unified module comment standards, improved component descriptions, feature explanations and usage examples, reducing user understanding cost and onboarding difficulty +- Performance improvement: Optimized core code logic, improving system runtime efficiency +- Menu optimization: Refined menu styles, optimized interaction experience and visual presentation +- Responsive optimization: Optimized ArtForm and ArtSearchBar grid layout, adapting to various screen sizes +- Configuration management optimization: Added setting.ts configuration file, supporting one-click copy and reset of system default settings + +#### Fixed + +- Fixed ArtForm and ArtSearchBar custom component rendering issues + +#### Added + +- ArtForm and ArtSearchBar added render property, supporting custom component rendering +- Festival feature enhancement: Fireworks configuration supports cross-date range settings and custom playback counts + +#### Upgraded + +- Dependency updates: Upgraded core dependencies to latest stable versions + +#### ⚠️ Important Notice + +> This upgrade involves fundamental refactoring of the style system (Sass → Tailwind CSS) and icon library (Iconfont → Iconify), which is a breaking change. It is recommended to use v3.0 for new projects. Upgrading existing projects is not recommended. + +--- + +### v2.6.1 - 2025-10-19 + +#### Fixed + +- Fixed repeated API calls issue when accessing invalid addresses for user info and menu endpoints +- Fixed ElButton circle mode styles +- Fixed ElSelect keyboard selection issue +- Fixed static routes with parameters redirecting to login page issue + +#### Optimized + +- Upgraded some dependencies to be compatible with tailwindcss +- Optimized external link menu click selection state +- Added theme color switching feature to authorization pages + +--- + +### v2.6.0 - 2025-10-16 + +#### Added + +- Extended registration and password reset page top component support + +#### Optimized + +- Optimized streamlined version menu data structure, improved data consistency +- Optimized local development environment network request proxy configuration +- Optimized ElTree component default styles +- Added VsCode recommended plugin related configuration +- Optimized ElDropdown component click trigger mode interaction styles +- Optimized menu filtering logic +- Optimized page switching animation, improved loading speed +- Optimized dark mode text color + +#### Fixed + +- Fixed static route custom home page path first visit redirecting to login page issue +- Fixed logout briefly jumping to 500 page issue +- Fixed v2.5.9 version home route redirect configuration failure issue +- Fixed v2.5.9 auto-import mechanism causing build exception + +#### ⚠️ Important Notice + +> This version requires re-login + +--- + +### v2.5.9 - 2025-10-12 + +#### Code Optimized + +- Optimized views file directory, file names, and code +- Added global configuration for useTable pagination request fields (tableConfig.ts) +- Optimized routing configuration to modular structure +- Menu API now uses apifox mock data (set VITE_ACCESS_MODE to backend mode in .env) + +--- + +### v2.5.8 - 2025-09-29 + +#### Upgraded + +- Upgraded core libraries including vue, vite, element-plus + +#### Fixed + +- Fixed rich text editor fullscreen top bar z-index issue +- Fixed table column sorting component text overflow issue +- Fixed statistics card condition judgment +- Fixed infinite redirect when root path / and HOME_PAGE_PATH are both / + +#### Optimized + +- Optimized el-tag styles +- Optimized top progress bar color +- Optimized custom theme configuration +- Optimized ElementPlus custom theme issues + +#### ⚠️ Important Notice + +> Due to dependency upgrades, Node version needs to be upgraded to v20.19.0 or above + +--- + +### v2.5.7 - 2025-09-14 + +#### Added + +- Added ArtForm component + +#### Fixed + +- Fixed theme switching flicker issue in new Chrome versions + +#### Optimized + +- Optimized form label height alignment issue +- First screen startup performance optimization + +--- + +### v2.5.6 - 2025-09-06 + +#### Added + +- ArtTableHeader added search button to control top search bar visibility +- Added permission demonstration examples +- Added global error handling framework + +#### Optimized + +- useTable type inference optimization, no need to manually pass types for type hints +- useTable removeColumn supports multiple data deletion +- useTable auto-recognizes response body with custom configuration (src/utils/table/tableConfig.ts) +- useTable empty data browser warning optimization +- API request code optimization, api.d.ts type optimization +- Optimized ArtTable top button wrapping adaptive issue +- ArtTable pagination component selection style optimization +- ArtTable empty state height fills by default +- ArtButtonMore component added icon and color configuration +- ArtSearchBar label doesn't occupy space when empty +- Table operation column drag prohibits fixed column dragging +- Role management page API integration and code optimization +- Menu management page optimization +- Optimized settings center scroll following issue +- First-level route external link component validation logic optimization +- Optimized map bottom-right drag issue +- Optimized dark mode page refresh white background issue +- Optimized left menu collapse button spacing issue +- Mobile displays left menu logo +- Network request added showSuccessMessage to configure success message display +- Global components use async loading strategy to improve first screen loading performance + +#### Fixed + +- Fixed bug where batch deleting entire page data doesn't return to previous page +- Fixed dynamic route parameter issues +- Fixed dynamic route configuration first-level route iframe page fullscreen issue + +--- + +### v2.5.5 - 2025-08-17 + +#### Added + +- Added ArtSearchBar component examples +- useTable added excludeParams to exclude certain parameters from requests + +#### Refactored + +- Refactored ArtSearchBar component, supports more components and form validation + +#### Optimized + +- useTable column configuration: supports dynamic update capability +- Optimized color picker border radius +- Unified el-radio and el-checkbox sizes +- art-stats-card added decimal places and separator configuration +- Route configuration example optimization +- Advanced table added custom data fetching example +- Optimized path alias type issues +- Local development CORS configuration optimization +- useTable property and method naming optimization +- Login page UI upgrade +- 403, 404, 500 page UI upgrade + +#### Fixed + +- Fixed multiple rich text editor icon inconsistency issue +- Fixed useTable deleting last entire page data not returning to previous page issue +- Fixed echarts chart data initialization and update browser error +- Network request supports logout on HTTP status code 401 +- Optimized network request logout multiple prompt issue + +#### Removed + +- Removed art-chart-empty component + +--- + +### v2.5.4 - 2025-07-27 + +#### Added + +- Network request headers support custom configuration +- Expand row supports formatter rendering + +#### Optimized + +- Route registration added component validation + +#### Fixed + +- Fixed user info API timing issue causing route registration menu rendering error +- Fixed dynamic route validation issue causing iframe not to display +- Fixed reset file syntax error +- Fixed ArtTable data type error +- Fixed map scroll wheel zoom issue + +--- + +### v2.5.3 - 2025-07-20 + +#### Added + +- Added table left-right layout example + +#### Refactored + +- ArtTable component refactored + +#### Upgraded + +- Element Plus upgraded to v2.10.2 + +#### Optimized + +- Optimized useTable pagination parameter issue +- Optimized network request example: initialization parameters, pagination carrying parameters +- Optimized search date range parameter handling +- Optimized el-date-picker component border radius +- Optimized el-select component hover style +- Search component and pagination component height reduced +- Optimized login page slider animation interval +- Optimized menu without submenu display issue + +#### Fixed + +- Fixed ArtTable switching page size executing two requests bug + +--- + +### v2.5.2 - 2025-07-13 + +#### Added + +- Added one-click cleanup script for quick development environment preparation +- useTable hooks support custom pagination field name mapping +- Horizontal menu, mixed menu, double column menu support badge display +- Quick entry supports configuration file mode +- Top bar function supports configuration file mode +- Support custom home page path +- Route supports redirect and other attributes configuration + +#### Optimized + +- Global event bus mittBus type safety optimization +- Optimized mobile settings container width style +- Optimized login page verification slider text centering effect + +#### Fixed + +- Fixed table no data header not displaying issue +- Fixed v2.5.0 top progress bar not displaying issue +- Fixed left menu mask abnormal display issue +- Fixed hiding all submenus still showing parent menu issue +- Fixed stylelint causing login page slider style abnormality +- Fixed old mobile device loading positioning issue + +--- + +### v2.5.1 - 2025-07-08 + +#### Optimized + +- el-card and el-table background color consistent with system + +#### Fixed + +- Fixed first login system loading closing early bug +- Fixed v2.5.0 version causing fullscreen page style z-index too low bug +- Fixed v2.5.0 version causing table expand row collapse bug + +--- + +### v2.5.0 - 2025-07-06 🎉 + +#### Added + +- Added useTable hooks table encapsulation, supports data fetching, transformation, response adaptation, intelligent caching (based on LRU algorithm), error handling, column configuration and slots, pagination control, refresh strategies and other core functions, comprehensively improving development efficiency and user experience + +#### Refactored + +- Refactored ArtTable, ArtTableHeader, ArtNotification components + +#### Upgraded + +- Echarts version upgraded to 5.6.0 + +#### Removed + +- Removed CountTo plugin, replaced with ArtCountTo component + +#### Fixed + +- Fixed menu management search directly modifying pinia data issue +- Fixed route guard loading flicker issue + +#### ⚠️ Important Notice + +> Recommended upgrade for more efficient and intelligent table development experience + +--- + +### v2.4.2.9 - 2025-07-02 + +#### Refactored + +- Menu layout and top navigation code refactored + +#### Optimized + +- Optimized mobile menu scroll user experience +- Optimized top menu style issues +- Top menu width adaptive, can display more content, mixed menu supports mouse scroll +- asyncRoutes route configuration auth_mark field changed to authMark +- Removed duplicate components.d.ts file, components.d.ts and auto-imports.d.ts ignored from commits +- Optimized i18n language file loading method, changed from async to sync mode +- Optimized el-pagination size inconsistency issue + +#### Fixed + +- Fixed mobile lock screen page some browsers unable to unlock bug + +--- + +### v2.4.2.8 - 2025-06-26 + +#### Fixed + +- Fixed v2.4.2.7 version accessing / path showing 404 issue + +--- + +### v2.4.2.7 - 2025-06-25 + +#### Added + +- Route supports fullscreen mode configuration +- Route supports auto-jump to first valid route of menu +- Dynamic route added removeAllDynamicRoutes method for completely clearing all dynamic routes +- Permission custom directive optimization, added role permission directive v-roles for controlling element visibility +- Added tab operation examples + +#### Optimized + +- CORS request carrying cookie configuration from environment variables, disabled by default +- Some optimizations for SEO and accessibility + +#### Fixed + +- Fixed login page drag component ArtDragVerify width and color abnormal bug +- Fixed iframe page mixed mode and double column mode abnormal bug +- Optimized lock screen page being penetrated by el-loading bug + +--- + +### v2.4.2.6 - 2025-06-23 + +#### Refactored + +- Refactored form-related components in components/core/forms folder, improved maintainability and consistency +- Refactored ArtBreadcrumb breadcrumb navigation component, optimized logic structure and styles +- Refactored ArtFireworksEffect fireworks effect component, significantly improved rendering performance and animation smoothness + +#### Optimized + +- Optimized ArtChatWindow and ArtFastEnter component code, improved readability and performance + +#### Added + +- README documentation added official website link for easy access to project documentation + +--- + +### v2.4.2.5 - 2025-06-22 + +#### Refactored + +- Refactored chart components, optimized code structure and maintainability +- Fine-tuned chart animations and theme color schemes, improved visual consistency + +--- + +### v2.4.2.4 - 2025-06-18 + +#### Refactored + +- ArtMenuRight component refactored +- Components under components/core/cards refactored, code optimized + +#### Optimized + +- ArtWatermark added type annotations + +--- + +### v2.4.2.3 - 2025-06-18 + +#### Refactored + +- ArtResultPage component refactored + +#### Optimized + +- ArtTextScroll component code optimization +- ArtException component added type hints +- ArtCutterImg component style optimization, added type definitions +- ArtVideoPlayer component added type definitions + +--- + +### v2.4.2.2 - 2025-06-16 + +#### Refactored + +- Back to top component refactored +- Icon selector component refactored + +#### Changed + +- System Logo component property changes + +--- + +### v2.4.2.1 - 2025-06-16 + +#### Refactored + +- Banner component refactored and optimized + +#### Fixed + +- Fixed mixed menu first menu nested menu jump bug + +--- + +### v2.4.2 - 2025-06-14 + +#### Refactored + +- Refactored network request module, enhanced error handling, type safety and multi-language support + +#### Added + +- Route configuration added activePath active menu path property + +#### Optimized + +- Guide and column settings multi-language improvement +- Removed invalid code from user list and menu management pages +- Updated technical support link + +#### Fixed + +- Fixed mobile search bar unable to scroll, iPad page scroll abnormal issue +- Fixed el-dialog enabling draggable property causing custom animation failure issue +- Fixed v2.3.0 local storage refactoring causing login, registration page multi-language settings unable to persist issue +- Fixed table fixed column not working bug +- Fixed infinite redirect when root path / and HOME_PAGE_PATH are both / + +#### ⚠️ Important Notice + +> This version requires re-login + +--- + +### v2.4.1.1 - 2025-06-07 + +#### Optimized + +- Optimized role management page code +- el-dialog visual effect optimization, supports line configuration +- System theme mode changed from Light to follow system mode + +#### Fixed + +- Fixed menu management collapse bug +- Fixed table empty data height infinitely growing bug + +--- + +### v2.4.1 - 2025-06-07 + +#### Added + +- Prohibit using developer tools to crack lock screen when locked + +#### Optimized + +- Improved menu operation responsiveness +- Page entrance animation time reduced by 0.04s +- Echarts chart performance optimization, added visible area initialization, memory leak protection, debounce handling + +#### Fixed + +- Fixed Echarts chart component not displaying in dialog bug + +--- + +### v2.4.0 - 2025-06-06 🎉 + +#### Refactored + +- Global TypeScript type system refactored, improved type accuracy and maintainability +- Refactored utils toolkit, unified utility method structure, enhanced readability and reusability + +#### Added + +- utils added form validation and Cookie operation related utility functions +- Page components added defineOptions for explicit component naming +- HTTP request added token expiration auto-handling logic, improved security and user experience + +#### Optimized + +- Deleted unused utility modules and invalid resources, streamlined project size +- Optimized views page structure, removed redundant page files +- Exception page multi-language support, improved internationalization experience +- Image resources uniformly converted to webp format, overall resource size reduced by about 50% +- Build output reduced by about 1MB, improved loading efficiency + +#### ⚠️ Important Notice + +> This version requires re-login + +--- + +## Version Guidelines + +### Version Number Rules + +This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html): + +- **Major**: Incompatible API changes +- **Minor**: Backwards-compatible functionality additions +- **Patch**: Backwards-compatible bug fixes + +### Change Types + +- **Added**: New features +- **Changed**: Changes to existing functionality +- **Deprecated**: Soon-to-be removed features +- **Removed**: Removed features +- **Fixed**: Bug fixes +- **Security**: Security-related fixes +- **Optimized**: Performance or experience optimizations +- **Refactored**: Code refactoring +- **Upgraded**: Dependency upgrades + +### Important Notice Markers + +- 🎉 **Major Update**: Versions with important new features +- ⚠️ **Breaking Changes**: Updates requiring special attention for incompatibility +- 🔒 **Security Update**: Versions fixing security vulnerabilities + +--- + +## Contributing + +If you want to contribute to the project, please check [CONTRIBUTING.md](./CONTRIBUTING.md). + +## License + +[MIT License](./LICENSE) + +--- + +**Note**: This changelog starts recording from version v2.4.0. For earlier version history, please check Git commit records. diff --git a/admin/CHANGELOG.zh-CN.md b/admin/CHANGELOG.zh-CN.md new file mode 100644 index 0000000..d1b43f2 --- /dev/null +++ b/admin/CHANGELOG.zh-CN.md @@ -0,0 +1,654 @@ +# 更新日志 + +所有重要的项目变更都将记录在此文件中。 + +格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 + +## [已发布] + +### v3.0.1 - 2025-11-15 + +#### 修复 + +- 修复路由注册时不存在接口的重复请求问题 +- 修复一键精简脚本打包失败的问题 + +#### 优化 + +- 完善路由配置验证机制,自动检测并提示非一级菜单的路径配置错误 +- 优化顶部进度条残影问题 +- 优化 vite 预构建配置 +- 优化圆角、边框统一性 +- 重新设计锁屏页面,提升视觉体验 +- 优化退出登录菜单提前消失造成的视觉体验不好问题 + +#### 新增 + +- ArtForm 和 ArtSearchBar 组件的 label 支持自定义渲染函数,可实现带 tooltip 等复杂标签 +- 菜单管理表单关键字段新增 Tooltip 提示,降低用户配置门槛 +- iconify 新增离线图标加载模式 +- 退出登录新增 redirect 属性,用于重新登录后跳转到对应页面 +- 退出登录重新进入系统保留用户标签页,根据 userId 进行身份识别 +- 双列菜单新增折叠按钮 +- 完善菜单图标 +- 多标签页增加图标显示 +- 新增 WebSocket 连接示例 +- 锁屏密码错误时增加输入框抖动动效 + +#### ⚠️ 重要提示 + +> 此版本需要重新登录 + +--- + +### v3.0.0 - 2025-11-09 🎉 + +#### 重构 + +- 样式系统重构:Sass 全面迁移至 Tailwind CSS,提升开发效率与样式一致性 +- 图标方案升级:Iconfont 替换为 Iconify,支持更丰富的图标库与按需加载 +- 路由注册重构:全面重构路由注册系统,引入面向对象设计,提高代码的可维护性、可测试性和扩展能力 +- 重构 ArtTextScroll 组件,提升性能与可维护性 +- 重构颜色体系,统一 UI 视觉规范,提升界面一致性 + +#### 优化 + +- 构建优化:完整包体积减少 1.3 MB,显著提升加载性能 +- 架构优化:优化目录结构,职责划分更清晰,降低用户学习成本与上手难度 +- 注释优化:统一模块注释规范,完善每一个组件介绍、功能说明与使用示例,降低用户理解成本与上手难度 +- 性能提升:优化核心代码逻辑,提升系统运行效率 +- 菜单优化:细化菜单样式,优化交互体验与视觉呈现 +- 响应式优化:优化 ArtForm、ArtSearchBar 栅格布局,适配多种屏幕尺寸 +- 配置管理优化:新增 setting.ts 配置文件,支持一键复制与重置系统默认设置 + +#### 修复 + +- 修复 ArtForm、ArtSearchBar 自定义组件渲染异常 + +#### 新增 + +- ArtForm、ArtSearchBar 新增 render 属性,支持自定义组件渲染 +- 节日功能增强:礼花配置支持跨日期范围设置与自定义播放次数 + +#### 升级 + +- 依赖更新:升级核心依赖至最新稳定版本 + +#### ⚠️ 重要提示 + +> 本次升级涉及样式系统(Sass → Tailwind CSS)与图标库(Iconfont → Iconify)的底层重构,属于破坏性更新。建议新项目直接使用 v3.0,旧版本项目不建议升级。 + +--- + +### v2.6.1 - 2025-10-19 + +#### 修复 + +- 修复获取用户信息、获取菜单接口访问无效地址重复调用问题 +- 修复 ElButton circle 模式样式 +- 修复 ElSelect 无法通过键盘选择问题 +- 修复带参数静态路由跳转登录页面问题 + +#### 优化 + +- 升级部分依赖兼容 tailwindcss +- 优化外部链接菜单点击选中状态 +- 授权页增加主题色切换功能 + +--- + +### v2.6.0 - 2025-10-16 + +#### 新增 + +- 扩展注册、密码重置页面顶部组件支持 + +#### 优化 + +- 优化精简版本菜单数据结构,提升数据一致性 +- 优化本地开发环境网络请求代理配置 +- 优化 ElTree 组件默认样式 +- 新增 VsCode 推荐插件相关配置 +- 优化 ElDropdown 组件点击触发模式下的交互样式 +- 优化菜单过滤逻辑 +- 优化页面切换动画,提升加载速度 +- 优化暗黑模式文字颜色 + +#### 修复 + +- 修复静态路由自定义首页路径首次访问跳转登录页问题 +- 修复退出登录时短暂跳转至 500 页的问题 +- 修复 v2.5.9 版本首页路由跳转配置失效问题 +- 修复 v2.5.9 自动导包机制导致的构建异常 + +#### ⚠️ 重要提示 + +> 此版本需要重新登录 + +--- + +### v2.5.9 - 2025-10-12 + +#### 代码优化 + +- views 文件目录、文件名、代码优化 +- useTable 分页请求字段增加全局配置 tableConfig.ts +- 优化路由配置为模块化结构 +- 获取菜单接口使用 apifox mock 数据(需在 .env 中将 VITE_ACCESS_MODE 设为 backend 模式) + +--- + +### v2.5.8 - 2025-09-29 + +#### 升级 + +- vue、vite、element-plus 等核心库升级 + +#### 修复 + +- 修复富文本编辑器全屏顶栏层级问题 +- 修复表格列排序组件文字溢出问题 +- 修复统计卡片条件判断 +- 修复根路径 / 与 HOME_PAGE_PATH 同为 / 时出现的无限重定向 + +#### 优化 + +- 优化 el-tag 样式 +- 优化顶部进度条颜色 +- 优化自定义主题配置 +- 优化 ElementPlus 自定义主题问题 + +#### ⚠️ 重要提示 + +> 由于项目依赖升级,node 版本需要升级到 v20.19.0 或以上 + +--- + +### v2.5.7 - 2025-09-14 + +#### 新增 + +- 新增 ArtForm 组件 + +#### 修复 + +- 修复新版本谷歌浏览器切换主题闪烁问题 + +#### 优化 + +- 优化表单 label 高度没有对齐问题 +- 首屏启动性能优化 + +--- + +### v2.5.6 - 2025-09-06 + +#### 新增 + +- ArtTableHeader 新增搜索按钮,用于控制顶部搜索栏的显示与隐藏 +- 新增权限演示示例 +- 添加全局错误处理基础框架 + +#### 优化 + +- useTable 类型推导优化,不需要手动传递类型即可实现类型提示 +- useTable removeColumn 支持多数据删除 +- useTable 自动识别响应体支持自定义配置 (src/utils/table/tableConfig.ts) +- useTable 空数据浏览器警告优化 +- api 接口请求代码优化、api.d.ts 类型优化 +- 优化 ArtTable 顶部按钮换行无法自适应问题(示例:功能示例 / 左右布局表格) +- ArtTable 分页组件选中样式优化 +- ArtTable 空状态高度默认撑满 +- ArtButtonMore 组件新增图标、颜色配置 +- ArtSearchBar label 为空时不占空间 +- 表格操作栏拖拽禁止固定列拖拽 +- 角色管理页面接口对接、代码优化 +- 菜单管理页面优化 +- 优化设置中心滚动页面跟随滚动问题 +- 一级路由是外链时,component 校验逻辑优化 +- 优化地图右下角拖动问题 +- 优化暗黑模式刷新页面白色背景问题 +- 优化左侧菜单折叠按钮间距问题 +- 移动端显示左侧菜单 logo +- 网络请求新增 showSuccessMessage,用于配置是否显示成功消息 +- 全局组件采用异步加载策略,提升首屏加载性能 + +#### 修复 + +- 修复批量删除整页数据没有返回上一页的bug +- 修复动态路由参数导致的问题 +- 修复动态路由配置一级路由是 iframe 页面时,全屏问题 + +--- + +### v2.5.5 - 2025-08-17 + +#### 新增 + +- 新增 ArtSearchBar 组件示例 +- useTable 新增 excludeParams,用于排除某些参数不参与请求 + +#### 重构 + +- 重构 ArtSearchBar 组件,支持更多组件、表单校验等能力 + +#### 优化 + +- useTable 列配置:支持动态更新能力 +- 优化颜色选择器圆角 +- el-radio、el-checkbox 统一大小 +- art-stats-card 新增小数位、分隔符配置 +- 路由配置示例优化 +- 高级表格新增自定义获取数据示例(等待其他请求完成后执行 useTable 数据获取) +- 优化路径别名类型问题 +- 本地开发跨域配置优化 +- useTable 属性、方法命名优化 +- 登录页UI升级 +- 403、404、500 页面UI升级 + +#### 修复 + +- 修复多个富文本编辑器图标不统一问题 +- 修复 useTable 删除最后一整页数据没有返回上一页的问题 +- 修复 echarts 图表数据初始化、更新数据浏览器报错 +- 网络请求支持 http 状态码为 401 时退出登录 +- 优化网络请求退出登录多次提示问题 + +#### 移除 + +- 删除 art-chart-empty 组件 + +--- + +### v2.5.4 - 2025-07-27 + +#### 新增 + +- 网络请求 headers 支持自定义配置 +- 展开行支持 formatter 渲染 + +#### 优化 + +- 路由注册新增 component 校验 + +#### 修复 + +- 修复获取用户信息接口时序问题导致路由注册菜单渲染错误bug +- 修复动态路由校验问题导致的 iframe 不显示bug +- 修复 reset 文件语法错误 +- 修复 ArtTable 数据类型错误 +- 修复地图滚轮滚动放大问题 + +--- + +### v2.5.3 - 2025-07-20 + +#### 新增 + +- 新增表格左右布局示例 + +#### 重构 + +- ArtTable 组件重构 + +#### 升级 + +- Element Plus 升级到 v2.10.2 + +#### 优化 + +- 优化 useTable 分页参数问题 +- 优化网络请求示例:初始化参数、分页携带参数问题 +- 优化搜索日期范围参数处理 +- 优化 el-date-picker 组件圆角问题 +- 优化 el-select 组件 hover 样式 +- 搜索组件、分页组件高度降低 +- 优化登录页面滑块动画间隔时长 +- 优化菜单没有子菜单显示的问题 + +#### 修复 + +- 修复 ArtTable 切换分页大小时执行两次请求bug + +--- + +### v2.5.2 - 2025-07-13 + +#### 新增 + +- 新增一键精简脚本,快速准备开发环境 +- useTable hooks 支持分页字段名自定义映射 +- 水平菜单、混合菜单、双列菜单支持徽章显示 +- 快速入口支持配置文件模式 +- 顶栏功能支持配置文件模式 +- 支持自定义首页路径 +- 路由支持配置 redirect 等属性 + +#### 优化 + +- 全局事件总线 mittBus 类型安全优化 +- 优化移动端设置中容器宽度样式 +- 优化登录页验证滑块文字居中效果 + +#### 修复 + +- 修复表格无数据时表头不显示问题 +- 修复 v2.5.0 顶部进度条不显示问题 +- 修复左侧菜单遮罩异常显示问题 +- 修复隐藏所有子菜单时仍显示父级菜单的问题 +- 修复 stylelint 导致的登录页滑块样式异常 +- 修复老旧移动端设备 loading 定位问题 + +--- + +### v2.5.1 - 2025-07-08 + +#### 优化 + +- el-card、el-table 背景色跟系统保持一致 + +#### 修复 + +- 修复首次登录系统时 loading 提前关闭bug +- 修复 v2.5.0 版本引起的全屏页样式层级过低bug +- 修复 v2.5.0 版本引起的表格展开行折叠bug + +--- + +### v2.5.0 - 2025-07-06 🎉 + +#### 新增 + +- 新增 useTable hooks 表格封装,支持数据获取、转换、响应适配、智能缓存(基于 LRU 算法)、错误处理、列配置与插槽、分页控制、刷新策略等核心功能,全面提升开发效率与用户体验 + +#### 重构 + +- 重构 ArtTable、ArtTableHeader、ArtNotification 组件 + +#### 升级 + +- Echarts 版本升级到 5.6.0 + +#### 移除 + +- 移除 CountTo 插件,替换为 ArtCountTo 组件 + +#### 修复 + +- 修复菜单管理搜索直接修改 pinia 数据的问题 +- 修复路由守卫 loading 闪烁问题 + +#### ⚠️ 重要提示 + +> 建议升级,带来更高效、更智能的表格开发体验 + +--- + +### v2.4.2.9 - 2025-07-02 + +#### 重构 + +- 菜单布局、顶部导航代码重构 + +#### 优化 + +- 优化移动端菜单滚动用户体验 +- 优化顶部菜单样式问题 +- 顶部菜单宽度自适应,可显示更多内容,混合菜单支持鼠标滚动 +- asyncRoutes 路由配置 auth_mark 字段改为 authMark +- 去除重复的 components.d.ts 文件,components.d.ts、auto-imports.d.ts 忽略提交 +- 优化国际化语言文件加载方式,异步改成同步模式 +- 优化 el-pagination 大小不一致问题 + +#### 修复 + +- 修复移动端锁屏页部分浏览器无法解锁bug + +--- + +### v2.4.2.8 - 2025-06-26 + +#### 修复 + +- 修复 v2.4.2.7 版本访问 / 路径时显示 404 的问题 + +--- + +### v2.4.2.7 - 2025-06-25 + +#### 新增 + +- 路由支持配置全屏模式 +- 路由支持自动跳转到菜单的第一个有效路由 +- 动态路由新增 removeAllDynamicRoutes 方法,可用于彻底清除所有动态路由 +- 权限自定义指令优化、新增角色权限指令 v-roles、可用于控制元素的显示与隐藏 +- 新增标签页操作示例 + +#### 优化 + +- 跨域请求携带 cookie 配置从环境变量中获取,默认关闭 +- 针对SEO、可访问性做一些优化 + +#### 修复 + +- 修复登录页面拖拽组件 ArtDragVerify 宽度、颜色异常bug +- 修复 iframe 页面混合模式、双列模式异常bug +- 优化锁屏页面被 el-loading 穿透bug + +--- + +### v2.4.2.6 - 2025-06-23 + +#### 重构 + +- 重构 components/core/forms 文件夹下的表单相关组件,提升可维护性与一致性 +- 重构 ArtBreadcrumb 面包屑导航组件,优化逻辑结构与样式 +- 重构 ArtFireworksEffect 烟花效果组件,显著提升渲染性能与动画流畅度 + +#### 优化 + +- 优化 ArtChatWindow 与 ArtFastEnter 组件代码,提升可读性与性能 + +#### 新增 + +- README 文档新增官方网站链接,便于用户查看项目文档 + +--- + +### v2.4.2.5 - 2025-06-22 + +#### 重构 + +- 重构图表组件,优化代码结构与可维护性 +- 精细调整图表动画与主题配色方案,提升视觉一致性 + +--- + +### v2.4.2.4 - 2025-06-18 + +#### 重构 + +- ArtMenuRight 组件重构 +- components/core/cards 下面的组件重构,代码优化 + +#### 优化 + +- ArtWatermark 增加类型注释 + +--- + +### v2.4.2.3 - 2025-06-18 + +#### 重构 + +- ArtResultPage 组件重构 + +#### 优化 + +- ArtTextScroll 组件代码优化 +- ArtException 组件增加类型提示 +- ArtCutterImg 组件样式优化、增加类型定义 +- ArtVideoPlayer 组件增加类型定义 + +--- + +### v2.4.2.2 - 2025-06-16 + +#### 重构 + +- 返回顶部组件重构 +- 图标选择器组件重构 + +#### 修改 + +- 系统Logo组件属性变更 + +--- + +### v2.4.2.1 - 2025-06-16 + +#### 重构 + +- 横幅组件重构以及优化 + +#### 修复 + +- 修复混合菜单下第一个菜单是嵌套菜单跳转bug + +--- + +### v2.4.2 - 2025-06-14 + +#### 重构 + +- 重构网络请求模块,增强错误处理、类型安全与多语言支持 + +#### 新增 + +- 路由配置新增 activePath 激活菜单路径属性 + +#### 优化 + +- 引导、列设置多语言完善 +- 去除用户列表、菜单管理页面无效代码 +- 更新技术支持链接 + +#### 修复 + +- 修复移动端搜索栏无法滚动、iPad端页面滚动异常问题 +- 修复 el-dialog 启用 draggable 属性后,自定义动画失效的问题 +- 修复 2.3.0 版本本地存储重构后,导致登录、注册等页面多语言设置无法持久化的问题 +- 修复表格固定列不起作用bug +- 修复根路径 / 与 HOME_PAGE_PATH 同为 / 时出现的无限重定向 + +#### ⚠️ 重要提示 + +> 此版本需要重新登录 + +--- + +### v2.4.1.1 - 2025-06-07 + +#### 优化 + +- 优化角色管理页面代码 +- el-dialog视觉效果优化,支持配置线条 +- 系统主题模式从Light改成跟随系统模式 + +#### 修复 + +- 修复菜单管理折叠 bug +- 修复表格数据为空高度无限变大bug + +--- + +### v2.4.1 - 2025-06-07 + +#### 新增 + +- 锁屏状态下禁止使用开发者工具破解锁屏 + +#### 优化 + +- 提升菜单操作跟手感 +- 页面入场动画时间减少0.04s +- Echarts 图表性能优化,新增可视区域初始化、内存泄漏防护、防抖处理 + +#### 修复 + +- 修复 Echarts 图表组件在弹窗中不显示的 bug + +--- + +### v2.4.0 - 2025-06-06 🎉 + +#### 重构 + +- 全局 TypeScript 类型体系重构,提升类型准确性与可维护性 +- 重构 utils 工具包,统一工具方法结构,增强可读性与复用性 + +#### 新增 + +- utils 新增表单验证与 Cookie 操作相关工具函数 +- 页面组件增加 defineOptions,明确组件命名 +- HTTP 请求增加 token 过期自动处理逻辑,提升安全性与用户体验 + +#### 优化 + +- 删除未使用的工具模块与无效资源,精简项目体积 +- 优化 views 页面结构,移除冗余页面文件 +- 异常页面多语言支持, 提升国际化体验 +- 图片资源统一转换为 webp 格式,整体资源体积减少约 50% +- 打包产物减少约 1MB,提高加载效率 + +#### ⚠️ 重要提示 + +> 此版本需要重新登录 + +--- + +## 版本说明 + +### 版本号规则 + +本项目遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) 规范: + +- **主版本号(Major)**:当你做了不兼容的 API 修改 +- **次版本号(Minor)**:当你做了向下兼容的功能性新增 +- **修订号(Patch)**:当你做了向下兼容的问题修正 + +### 变更类型 + +- **新增(Added)**:新功能 +- **修改(Changed)**:对现有功能的变更 +- **弃用(Deprecated)**:即将移除的功能 +- **移除(Removed)**:已移除的功能 +- **修复(Fixed)**:Bug 修复 +- **安全(Security)**:安全相关的修复 +- **优化(Optimized)**:性能优化或体验优化 +- **重构(Refactored)**:代码重构 +- **升级(Upgraded)**:依赖升级 + +### 重要提示标记 + +- 🎉 **重大更新**:包含重要新特性的版本 +- ⚠️ **破坏性变更**:需要特别注意的不兼容更新 +- 🔒 **安全更新**:修复安全漏洞的版本 + +--- + +## 贡献指南 + +如果你想为项目做出贡献,请查看 [CONTRIBUTING.md](./CONTRIBUTING.md)。 + +## 许可证 + +[MIT License](./LICENSE) + +--- + +**注意**:本更新日志从 v2.4.0 版本开始记录。更早的版本历史请查看 Git 提交记录。 + +--- + +[English Version](./CHANGELOG.en.md) diff --git a/admin/LICENSE b/admin/LICENSE new file mode 100644 index 0000000..68322de --- /dev/null +++ b/admin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 SuperManTT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/admin/README.md b/admin/README.md new file mode 100644 index 0000000..a1f4676 --- /dev/null +++ b/admin/README.md @@ -0,0 +1,104 @@ + + +
+

Art Design Pro

+

A backend system template that combines design aesthetics with efficient development, helping you quickly build professional-grade applications

+
English | 简体中文
+ +
+
+ +[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE) [![github stars](https://img.shields.io/github/stars/Daymychen/art-design-pro)](https://github.com/Daymychen/art-design-pro/stargazers) [![github forks](https://img.shields.io/github/forks/Daymychen/art-design-pro)](https://github.com/Daymychen/art-design-pro/network/members) + +
+
+ +## What makes this project special? + +**Interface Design**: Modern UI design with smooth interactions, focusing on user experience and visual design + +**Quick Start**: Clean architecture + comprehensive documentation, easy for backend developers to use + +**Rich Components**: Built-in high-quality components for data display, forms, and more to meet different business scenarios + +**Smooth Interactions**: Button clicks, theme switching, page transitions, chart animations - experience comparable to commercial products + +**Efficient Development**: Built-in practical APIs like useTable and ArtForm to significantly improve development efficiency + +**Clean Scripts**: Built-in one-click cleanup script to quickly remove demo data and get a ready-to-develop base project + +## Tech Stack + +Development Framework: Vue3, TypeScript, Vite, Element-Plus, Tailwind CSS + +Code Standards: Eslint, Prettier, Stylelint, Husky, Lint-staged, cz-git + +## Preview + +Light Theme + +Light Theme + +Dark Theme + +Dark Theme + +## Quick Access + +[Live Demo](https://www.artd.pro) | [Official Documentation](https://www.artd.pro/docs) | [Changelog](./CHANGELOG.en.md) + +## Installation & Setup + +```bash +# Install dependencies +pnpm install + +# If pnpm install fails, try using the command below +pnpm install --ignore-scripts + +# Start local development environment +pnpm dev + +# Build for production +pnpm build +``` + +## Clean Version + +The project includes a cleanup script to quickly remove demo data and provide developers with a ready-to-develop base project + +```bash +pnpm clean:dev +``` + +## Technical Support + +QQ Group: 1038930070 (Click the link to join the group chat) + +## Browser Compatibility + +Supports modern mainstream browsers including Chrome, Safari, Firefox, and more. + +## Contributing + +We sincerely welcome and appreciate the support of every contributor! Whether you have new ideas, feature suggestions, or code optimizations, you can participate in the following ways: + +Submit Pull Requests: Share your code and help the project grow. + +Create GitHub Issues: Provide bug feedback or new feature suggestions to help us improve together. + +Every contribution you make takes this project one step further! Come join our open source community! + +## Continuous Optimization & Extension + +The project maintains active updates, supports the latest frontend tech stack, is compatible with mainstream frameworks, and ensures long-term stability and extensibility. Community-driven feedback mechanisms allow your needs to be quickly integrated into project iterations. + +## Donation + +If you feel this project has reduced your development costs and solved problems in your work/life, you can support us through the following ways: + +Donation QR Code + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=Daymychen/art-design-pro&type=Date)](https://www.star-history.com/#Daymychen/art-design-pro&Date) diff --git a/admin/README.zh-CN.md b/admin/README.zh-CN.md new file mode 100644 index 0000000..aad3c54 --- /dev/null +++ b/admin/README.zh-CN.md @@ -0,0 +1,104 @@ + + +
+

Art Design Pro

+

一款兼具设计美学与高效开发的后台系统模版,助你快速构建专业级应用

+
简体中文 | English
+ +
+
+ +[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE) [![github stars](https://img.shields.io/github/stars/Daymychen/art-design-pro)](https://github.com/Daymychen/art-design-pro/stargazers) [![github forks](https://img.shields.io/github/forks/Daymychen/art-design-pro)](https://github.com/Daymychen/art-design-pro/network/members) + +
+
+ +## 这个项目有什么特别的呢? + +**界面设计**:现代化 UI 设计,流畅交互,以用户体验与视觉设计为核心 + +**极速上手**:简洁架构 + 完整文档,后端开发者也能轻松使用 + +**丰富组件**:内置数据展示、表单等多种高质量组件,满足不同业务场景的需求 + +**丝滑交互**:按钮点击、主题切换、页面过渡、图表动画,体验媲美商业产品 + +**高效开发**:内置 useTable、ArtForm 等实用 API,显著提升开发效率 + +**精简脚本**:内置一键清理脚本,可快速清理演示数据,立即得到可开发的基础项目 + +## 技术栈 + +开发框架:Vue3、TypeScript、Vite、Element-Plus、Tailwind CSS + +代码规范:Eslint、Prettier、Stylelint、Husky、Lint-staged、cz-git + +## 预览 + +浅色主题 + +浅色主题 + +暗黑主题 + +暗黑主题 + +## 快速访问 + +[演示地址](https://www.artd.pro) | [官方文档](https://www.artd.pro/docs) | [更新日志](./CHANGELOG.md) + +## 安装运行 + +```bash +# 安装依赖 +pnpm install + +# 如果 pnpm install 安装失败,尝试使用下面的命令安装依赖 +pnpm install --ignore-scripts + +# 本地开发环境启动 +pnpm dev + +# 生产环境打包 +pnpm build +``` + +## 精简版本 + +项目内置精简脚本,可快速移除项目中的演示数据,让开发者获得一个可快速开发的基础项目 + +```bash +pnpm clean:dev +``` + +## 技术支持 + +QQ群:1038930070(点击链接加入群聊) + +## 兼容性 + +支持 Chrome、Safari、Firefox 等现代主流浏览器。 + +## 贡献 + +我们真诚欢迎并感谢每一位贡献者的支持!无论您有新想法、功能建议还是代码优化,都可以通过以下方式参与: + +提交 Pull Request:分享您的代码,助力项目成长。 + +创建 GitHub Issue:提出 bug 反馈或新功能建议,让我们一起完善。 + +您的每一点贡献都让这个项目更进一步!快来加入我们的开源社区吧! + +## 持续优化与扩展 + +项目保持活跃更新,支持最新前端技术栈,兼容主流框架,确保长期稳定性和扩展性。社区驱动的反馈机制,让你的需求快速融入项目迭代。 + +## 捐赠 + +如果你觉得这个项目为你减少了开发成本、化解了工作 / 生活里的难题,可以通过以下方式支持一下~ + +捐赠二维码 + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=Daymychen/art-design-pro&type=Date)](https://www.star-history.com/#Daymychen/art-design-pro&Date) diff --git a/admin/eslint.config.mjs b/admin/eslint.config.mjs new file mode 100644 index 0000000..ed2d4ba --- /dev/null +++ b/admin/eslint.config.mjs @@ -0,0 +1,83 @@ +// 从 URL 和路径模块中导入必要的功能 +import fs from 'fs' +import path, { dirname } from 'path' +import { fileURLToPath } from 'url' + +// 从 ESLint 插件中导入推荐配置 +import pluginJs from '@eslint/js' +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' +import pluginVue from 'eslint-plugin-vue' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +// 使用 import.meta.url 获取当前模块的路径 +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +// 读取 .auto-import.json 文件的内容,并将其解析为 JSON 对象 +const autoImportConfig = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '.auto-import.json'), 'utf-8') +) + +export default [ + // 指定文件匹配规则 + { + files: ['**/*.{js,mjs,cjs,ts,tsx,vue}'] + }, + // 指定全局变量和环境 + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + // 扩展配置 + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + ...pluginVue.configs['flat/essential'], + // 自定义规则 + { + // 针对所有 JavaScript、TypeScript 和 Vue 文件应用以下配置 + files: ['**/*.{js,mjs,cjs,ts,tsx,vue}'], + + languageOptions: { + globals: { + // 合并从 autoImportConfig 中读取的全局变量配置 + ...autoImportConfig.globals, + // TypeScript 全局命名空间 + Api: 'readonly' + } + }, + rules: { + quotes: ['error', 'single'], // 使用单引号 + semi: ['error', 'never'], // 语句末尾不加分号 + 'no-var': 'error', // 要求使用 let 或 const 而不是 var + '@typescript-eslint/no-explicit-any': 'off', // 禁用 any 检查 + 'vue/multi-word-component-names': 'off', // 禁用对 Vue 组件名称的多词要求检查 + 'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行 + 'no-unexpected-multiline': 'error' // 禁止空余的多行 + } + }, + // vue 规则 + { + files: ['**/*.vue'], + languageOptions: { + parserOptions: { parser: tseslint.parser } + } + }, + // 忽略文件 + { + ignores: [ + 'node_modules', + 'dist', + 'public', + '.vscode/**', + 'src/assets/**', + 'src/utils/console.ts' + ] + }, + // prettier 配置 + eslintPluginPrettierRecommended +] diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..6b72611 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,44 @@ + + + + all-kefu 管理后台 + + + + + + + + + + + +
+ + + diff --git a/admin/package.json b/admin/package.json new file mode 100644 index 0000000..f3b544b --- /dev/null +++ b/admin/package.json @@ -0,0 +1,84 @@ +{ + "name": "all-kefu-admin", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=20.19.0", + "pnpm": ">=8.8.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@parcel/watcher", + "@tailwindcss/oxide", + "core-js", + "esbuild", + "es5-ext", + "vue-demi" + ] + }, + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "serve": "vite preview", + "lint": "eslint", + "fix": "eslint --fix", + "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,vue,html,md}\"", + "lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix", + "clean:dev": "tsx scripts/clean-dev.ts" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@iconify/vue": "^5.0.0", + "@tailwindcss/vite": "^4.1.14", + "@vue/reactivity": "^3.5.21", + "@vueuse/core": "^13.9.0", + "axios": "^1.12.2", + "crypto-js": "^4.2.0", + "echarts": "^6.0.0", + "element-plus": "^2.11.2", + "mitt": "^3.0.1", + "nprogress": "^0.2.0", + "ohash": "^2.0.11", + "pinia": "^3.0.3", + "pinia-plugin-persistedstate": "^4.3.0", + "tailwindcss": "^4.1.14", + "vue": "^3.5.21", + "vue-draggable-plus": "^0.6.0", + "vue-i18n": "^9.14.0", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@eslint/js": "^9.9.1", + "@types/node": "^24.0.5", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/compiler-sfc": "^3.0.5", + "eslint": "^9.9.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-vue": "^9.27.0", + "globals": "^15.9.0", + "prettier": "^3.5.3", + "rollup-plugin-visualizer": "^5.12.0", + "sass": "^1.81.0", + "stylelint": "^16.20.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-recess-order": "^4.6.0", + "stylelint-config-recommended-scss": "^14.1.0", + "stylelint-config-recommended-vue": "^1.5.0", + "stylelint-config-standard": "^36.0.1", + "terser": "^5.36.0", + "tsx": "^4.20.3", + "typescript": "~5.6.3", + "typescript-eslint": "^8.9.0", + "unplugin-auto-import": "^20.2.0", + "unplugin-element-plus": "^0.10.0", + "unplugin-vue-components": "^29.1.0", + "vite": "^7.1.5", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-vue-devtools": "^7.7.6", + "vue-tsc": "~2.1.6" + } +} diff --git a/admin/pnpm-lock.yaml b/admin/pnpm-lock.yaml new file mode 100644 index 0000000..b88b1d6 --- /dev/null +++ b/admin/pnpm-lock.yaml @@ -0,0 +1,5500 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.22(typescript@5.6.3)) + '@iconify/vue': + specifier: ^5.0.0 + version: 5.0.0(vue@3.5.22(typescript@5.6.3)) + '@tailwindcss/vite': + specifier: ^4.1.14 + version: 4.1.14(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vue/reactivity': + specifier: ^3.5.21 + version: 3.5.22 + '@vueuse/core': + specifier: ^13.9.0 + version: 13.9.0(vue@3.5.22(typescript@5.6.3)) + axios: + specifier: ^1.12.2 + version: 1.12.2 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + echarts: + specifier: ^6.0.0 + version: 6.0.0 + element-plus: + specifier: ^2.11.2 + version: 2.11.4(vue@3.5.22(typescript@5.6.3)) + mitt: + specifier: ^3.0.1 + version: 3.0.1 + nprogress: + specifier: ^0.2.0 + version: 0.2.0 + ohash: + specifier: ^2.0.11 + version: 2.0.11 + pinia: + specifier: ^3.0.3 + version: 3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)) + pinia-plugin-persistedstate: + specifier: ^4.3.0 + version: 4.5.0(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3))) + tailwindcss: + specifier: ^4.1.14 + version: 4.1.14 + vue: + specifier: ^3.5.21 + version: 3.5.22(typescript@5.6.3) + vue-draggable-plus: + specifier: ^0.6.0 + version: 0.6.0(@types/sortablejs@1.15.8) + vue-i18n: + specifier: ^9.14.0 + version: 9.14.5(vue@3.5.22(typescript@5.6.3)) + vue-router: + specifier: ^4.5.1 + version: 4.5.1(vue@3.5.22(typescript@5.6.3)) + devDependencies: + '@eslint/js': + specifier: ^9.9.1 + version: 9.36.0 + '@types/node': + specifier: ^24.0.5 + version: 24.8.1 + '@typescript-eslint/eslint-plugin': + specifier: ^8.3.0 + version: 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: ^8.3.0 + version: 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + '@vue/compiler-sfc': + specifier: ^3.0.5 + version: 3.5.22 + eslint: + specifier: ^9.9.1 + version: 9.36.0(jiti@2.6.0) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.2(eslint@9.36.0(jiti@2.6.0)) + eslint-plugin-prettier: + specifier: ^5.2.1 + version: 5.5.4(eslint-config-prettier@9.1.2(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(prettier@3.6.2) + eslint-plugin-vue: + specifier: ^9.27.0 + version: 9.33.0(eslint@9.36.0(jiti@2.6.0)) + globals: + specifier: ^15.9.0 + version: 15.15.0 + prettier: + specifier: ^3.5.3 + version: 3.6.2 + rollup-plugin-visualizer: + specifier: ^5.12.0 + version: 5.14.0(rollup@4.52.3) + sass: + specifier: ^1.81.0 + version: 1.93.2 + stylelint: + specifier: ^16.20.0 + version: 16.24.0(typescript@5.6.3) + stylelint-config-html: + specifier: ^1.1.0 + version: 1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recess-order: + specifier: ^4.6.0 + version: 4.6.0(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recommended-scss: + specifier: ^14.1.0 + version: 14.1.0(postcss@8.5.6)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recommended-vue: + specifier: ^1.5.0 + version: 1.6.1(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-standard: + specifier: ^36.0.1 + version: 36.0.1(stylelint@16.24.0(typescript@5.6.3)) + terser: + specifier: ^5.36.0 + version: 5.44.0 + tsx: + specifier: ^4.20.3 + version: 4.20.6 + typescript: + specifier: ~5.6.3 + version: 5.6.3 + typescript-eslint: + specifier: ^8.9.0 + version: 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + unplugin-auto-import: + specifier: ^20.2.0 + version: 20.2.0(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.6.3))) + unplugin-element-plus: + specifier: ^0.10.0 + version: 0.10.0 + unplugin-vue-components: + specifier: ^29.1.0 + version: 29.1.0(@babel/parser@7.28.4)(vue@3.5.22(typescript@5.6.3)) + vite: + specifier: ^7.1.5 + version: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-compression: + specifier: ^0.5.1 + version: 0.5.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-vue-devtools: + specifier: ^7.7.6 + version: 7.7.7(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + vue-tsc: + specifier: ~2.1.6 + version: 2.1.10(typescript@5.6.3) + +packages: + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.28.0': + resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@cacheable/memoize@2.0.2': + resolution: {integrity: sha512-wPrr7FUiq3Qt4yQyda2/NcOLTJCFcQSU3Am2adP+WLy+sz93/fKTokVTHmtz+rjp4PD7ee0AEOeRVNN6IvIfsg==} + + '@cacheable/memory@2.0.2': + resolution: {integrity: sha512-sJTITLfeCI1rg7P3ssaGmQryq235EGT8dXGcx6oZwX5NRnKq9IE6lddlllcOl+oXW+yaeTRddCjo0xrfU6ZySA==} + + '@cacheable/utils@2.0.2': + resolution: {integrity: sha512-JTFM3raFhVv8LH95T7YnZbf2YoE9wEtkPPStuRF9a6ExZ103hFvs+QyCuYJ6r0hA9wRtbzgZtwUCoDWxssZd4Q==} + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.36.0': + resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/vue@5.0.0': + resolution: {integrity: sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==} + peerDependencies: + vue: '>=3' + + '@intlify/core-base@9.14.5': + resolution: {integrity: sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@9.14.5': + resolution: {integrity: sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==} + engines: {node: '>= 16'} + + '@intlify/shared@9.14.5': + resolution: {integrity: sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==} + engines: {node: '>= 16'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.0.2': + resolution: {integrity: sha512-KR03xkEZlAZNF4IxXgVXb+uNIVNvwdh8UwI0cnc7WI6a+aQcDp8GL80qVfeB4E5NpsKJzou5jU0r6yLSSbMOtA==} + engines: {node: '>= 18'} + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/pluginutils@1.0.0-beta.29': + resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.52.3': + resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.3': + resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.3': + resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.3': + resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.3': + resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.3': + resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.3': + resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.3': + resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.3': + resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.3': + resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.3': + resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.3': + resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@tailwindcss/node@4.1.14': + resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==} + + '@tailwindcss/oxide-android-arm64@4.1.14': + resolution: {integrity: sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + resolution: {integrity: sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.14': + resolution: {integrity: sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + resolution: {integrity: sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + resolution: {integrity: sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + resolution: {integrity: sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + resolution: {integrity: sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.14': + resolution: {integrity: sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.14': + resolution: {integrity: sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/node@24.8.1': + resolution: {integrity: sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==} + + '@types/sortablejs@1.15.8': + resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@typescript-eslint/eslint-plugin@8.44.1': + resolution: {integrity: sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.44.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.44.1': + resolution: {integrity: sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.44.1': + resolution: {integrity: sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.44.1': + resolution: {integrity: sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.44.1': + resolution: {integrity: sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.44.1': + resolution: {integrity: sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.44.1': + resolution: {integrity: sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.44.1': + resolution: {integrity: sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.44.1': + resolution: {integrity: sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.44.1': + resolution: {integrity: sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@6.0.1': + resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.22': + resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + + '@vue/compiler-dom@3.5.22': + resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} + + '@vue/compiler-sfc@3.5.22': + resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==} + + '@vue/compiler-ssr@3.5.22': + resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} + + '@vue/devtools-core@7.7.7': + resolution: {integrity: sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.7.7': + resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + + '@vue/devtools-shared@7.7.7': + resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + + '@vue/language-core@2.1.10': + resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.22': + resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==} + + '@vue/runtime-core@3.5.22': + resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==} + + '@vue/runtime-dom@3.5.22': + resolution: {integrity: sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==} + + '@vue/server-renderer@3.5.22': + resolution: {integrity: sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==} + peerDependencies: + vue: 3.5.22 + + '@vue/shared@3.5.22': + resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + + '@vueuse/core@13.9.0': + resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + + '@vueuse/metadata@13.9.0': + resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==} + + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + + '@vueuse/shared@13.9.0': + resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + alien-signals@0.2.2: + resolution: {integrity: sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + baseline-browser-mapping@2.8.8: + resolution: {integrity: sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + birpc@2.6.1: + resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.26.2: + resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cacheable@2.0.2: + resolution: {integrity: sha512-dWjhLx8RWnPsAWVKwW/wI6OJpQ/hSVb1qS0NUif8TR9vRiSwci7Gey8x04kRU9iAF+Rnbtex5Kjjfg/aB5w8Pg==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001745: + resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deep-pick-omit@1.2.1: + resolution: {integrity: sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + + electron-to-chromium@1.5.227: + resolution: {integrity: sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==} + + element-plus@2.11.4: + resolution: {integrity: sha512-sLq+Ypd0cIVilv8wGGMEGvzRVBBsRpJjnAS5PsI/1JU1COZXqzH3N1UYMUc/HCdvdjf6dfrBy80Sj7KcACsT7w==} + peerDependencies: + vue: ^3.2.0 + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + error-stack-parser-es@0.1.5: + resolution: {integrity: sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@9.1.2: + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@9.33.0: + resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.36.0: + resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@10.1.4: + resolution: {integrity: sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat-cache@6.1.14: + resolution: {integrity: sha512-ExZSCSV9e7v/Zt7RzCbX57lY2dnPdxzU/h3UE6WJ6NtEMfwBd8jmi1n4otDEUfz+T/R+zxrFDpICFdjhD3H/zw==} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hookified@1.12.1: + resolution: {integrity: sha512-xnKGl+iMIlhrZmGHB729MqlmPoWBznctSQTYCpFKqNsCgimJQmithcW0xSQMMFzYnV2iKUh25alswn6epgxS0Q==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@5.1.3: + resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.6.0: + resolution: {integrity: sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + keyv@5.5.3: + resolution: {integrity: sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + known-css-properties@0.36.0: + resolution: {integrity: sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdn-data@2.24.0: + resolution: {integrity: sha512-i97fklrJl03tL1tdRVw0ZfLLvuDsdb6wxL+TrJ+PKkCbLrp2PCu2+OYdCKychIUm19nSM/35S6qz7pJpnXttoA==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinia-plugin-persistedstate@4.5.0: + resolution: {integrity: sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==} + peerDependencies: + '@nuxt/kit': '>=3.0.0' + '@pinia/nuxt': '>=0.10.0' + pinia: '>=3.0.0' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@pinia/nuxt': + optional: true + pinia: + optional: true + + pinia@3.0.3: + resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + postcss-html@1.8.0: + resolution: {integrity: sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==} + engines: {node: ^12 || >=14} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-sorting@8.0.2: + resolution: {integrity: sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==} + peerDependencies: + postcss: ^8.4.20 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup-plugin-visualizer@5.14.0: + resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + rolldown: 1.x + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup@4.52.3: + resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.93.2: + resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==} + engines: {node: '>=14.0.0'} + hasBin: true + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + stylelint-config-html@1.1.0: + resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==} + engines: {node: ^12 || >=14} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recess-order@4.6.0: + resolution: {integrity: sha512-V76fhv3YtcNXh/hyAuAdSzi5FmcrG54Mp2AThJ3D/PTMTSYzUPd7GIhP6z9mTqnRhmkk6YTfcu/JWB8h+Yrcaw==} + peerDependencies: + stylelint: '>=15' + + stylelint-config-recommended-scss@14.1.0: + resolution: {integrity: sha512-bhaMhh1u5dQqSsf6ri2GVWWQW5iUjBYgcHkh7SgDDn92ijoItC/cfO/W+fpXshgTQWhwFkP1rVcewcv4jaftRg==} + engines: {node: '>=18.12.0'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.6.1 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended-vue@1.6.1: + resolution: {integrity: sha512-lLW7hTIMBiTfjenGuDq2kyHA6fBWd/+Df7MO4/AWOxiFeXP9clbpKgg27kHfwA3H7UNMGC7aeP3mNlZB5LMmEQ==} + engines: {node: ^12 || >=14} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recommended@14.0.1: + resolution: {integrity: sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-config-recommended@17.0.0: + resolution: {integrity: sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-config-standard@36.0.1: + resolution: {integrity: sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-order@6.0.4: + resolution: {integrity: sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==} + peerDependencies: + stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1 + + stylelint-scss@6.12.1: + resolution: {integrity: sha512-UJUfBFIvXfly8WKIgmqfmkGKPilKB4L5j38JfsDd+OCg2GBdU0vGUV08Uw82tsRZzd4TbsUURVVNGeOhJVF7pA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.0.2 + + stylelint@16.24.0: + resolution: {integrity: sha512-7ksgz3zJaSbTUGr/ujMXvLVKdDhLbGl3R/3arNudH7z88+XZZGNLMTepsY28WlnvEFcuOmUe7fg40Q3lfhOfSQ==} + engines: {node: '>=18.12.0'} + hasBin: true + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tailwindcss@4.1.14: + resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} + engines: {node: '>=18'} + + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} + engines: {node: '>=10'} + hasBin: true + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript-eslint@8.44.1: + resolution: {integrity: sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unimport@5.4.0: + resolution: {integrity: sha512-g/OLFZR2mEfqbC6NC9b2225eCJGvufxq34mj6kM3OmI5gdSL0qyqtnv+9qmsGpAmnzSl6x0IWZj4W+8j2hLkMA==} + engines: {node: '>=18.12.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin-auto-import@20.2.0: + resolution: {integrity: sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^4.0.0 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-element-plus@0.10.0: + resolution: {integrity: sha512-oRSW0x6U58xBOWKy8TcoVZNA8ElIpfp3TUJRLQI6ey/E9PpjHl9/deeTAZNt8D57Li4OA4pCJtM6p2cb4Ff4ZA==} + engines: {node: '>=18.12.0'} + + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} + + unplugin-utils@0.3.0: + resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==} + engines: {node: '>=20.19.0'} + + unplugin-vue-components@29.1.0: + resolution: {integrity: sha512-z/9ACPXth199s9aCTCdKZAhe5QGOpvzJYP+Hkd0GN1/PpAmsu+W3UlRY3BJAewPqQxh5xi56+Og6mfiCV1Jzpg==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + '@nuxt/kit': ^3.2.2 || ^4.0.0 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + '@nuxt/kit': + optional: true + + unplugin@2.3.10: + resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-inspect@0.8.9: + resolution: {integrity: sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@7.7.7: + resolution: {integrity: sha512-d0fIh3wRcgSlr4Vz7bAk4va1MkdqhQgj9ANE/rBhsAjOnRfTLs2ocjFMvSUOsv6SRRXU9G+VM7yMgqDb6yI4iQ==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-plugin-vue-inspector@5.3.2: + resolution: {integrity: sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite@7.1.7: + resolution: {integrity: sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-draggable-plus@0.6.0: + resolution: {integrity: sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==} + peerDependencies: + '@types/sortablejs': ^1.15.0 + '@vue/composition-api': '*' + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-i18n@9.14.5: + resolution: {integrity: sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue-router@4.5.1: + resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} + peerDependencies: + vue: ^3.2.0 + + vue-tsc@2.1.10: + resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.22: + resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + +snapshots: + + '@antfu/utils@0.7.10': {} + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@cacheable/memoize@2.0.2': + dependencies: + '@cacheable/utils': 2.0.2 + + '@cacheable/memory@2.0.2': + dependencies: + '@cacheable/memoize': 2.0.2 + '@cacheable/utils': 2.0.2 + '@keyv/bigmap': 1.0.2 + hookified: 1.12.1 + keyv: 5.5.3 + + '@cacheable/utils@2.0.2': {} + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@ctrl/tinycolor@3.6.1': {} + + '@dual-bundle/import-meta-resolve@4.2.1': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.22(typescript@5.6.3))': + dependencies: + vue: 3.5.22(typescript@5.6.3) + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.0))': + dependencies: + eslint: 9.36.0(jiti@2.6.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.36.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify/types@2.0.0': {} + + '@iconify/vue@5.0.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.22(typescript@5.6.3) + + '@intlify/core-base@9.14.5': + dependencies: + '@intlify/message-compiler': 9.14.5 + '@intlify/shared': 9.14.5 + + '@intlify/message-compiler@9.14.5': + dependencies: + '@intlify/shared': 9.14.5 + source-map-js: 1.2.1 + + '@intlify/shared@9.14.5': {} + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.0.2': + dependencies: + hookified: 1.12.1 + + '@keyv/serialize@1.1.1': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@pkgr/core@0.2.9': {} + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/pluginutils@1.0.0-beta.29': {} + + '@rollup/pluginutils@5.3.0(rollup@4.52.3)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.52.3 + + '@rollup/rollup-android-arm-eabi@4.52.3': + optional: true + + '@rollup/rollup-android-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-x64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.3': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@sxzz/popperjs-es@2.11.7': {} + + '@tailwindcss/node@4.1.14': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.0 + lightningcss: 1.30.1 + magic-string: 0.30.19 + source-map-js: 1.2.1 + tailwindcss: 4.1.14 + + '@tailwindcss/oxide-android-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide@4.1.14': + dependencies: + detect-libc: 2.1.2 + tar: 7.5.1 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-x64': 4.1.14 + '@tailwindcss/oxide-freebsd-x64': 4.1.14 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.14 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.14 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-x64-musl': 4.1.14 + '@tailwindcss/oxide-wasm32-wasi': 4.1.14 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.14 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.14 + + '@tailwindcss/vite@4.1.14(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@tailwindcss/node': 4.1.14 + '@tailwindcss/oxide': 4.1.14 + tailwindcss: 4.1.14 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/node@24.8.1': + dependencies: + undici-types: 7.14.0 + + '@types/sortablejs@1.15.8': {} + + '@types/web-bluetooth@0.0.16': {} + + '@types/web-bluetooth@0.0.21': {} + + '@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.44.1 + eslint: 9.36.0(jiti@2.6.0) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.44.1 + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.0) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.44.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.6.3) + '@typescript-eslint/types': 8.44.1 + debug: 4.4.3 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.44.1': + dependencies: + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/visitor-keys': 8.44.1 + + '@typescript-eslint/tsconfig-utils@8.44.1(typescript@5.6.3)': + dependencies: + typescript: 5.6.3 + + '@typescript-eslint/type-utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.0) + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.44.1': {} + + '@typescript-eslint/typescript-estree@8.44.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/project-service': 8.44.1(typescript@5.6.3) + '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.6.3) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/visitor-keys': 8.44.1 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + eslint: 9.36.0(jiti@2.6.0) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.44.1': + dependencies: + '@typescript-eslint/types': 8.44.1 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-vue@6.0.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vue: 3.5.22(typescript@5.6.3) + + '@volar/language-core@2.4.23': + dependencies: + '@volar/source-map': 2.4.23 + + '@volar/source-map@2.4.23': {} + + '@volar/typescript@2.4.23': + dependencies: + '@volar/language-core': 2.4.23 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.4)': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.4) + '@vue/shared': 3.5.22 + optionalDependencies: + '@babel/core': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.28.4)': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/parser': 7.28.4 + '@vue/compiler-sfc': 3.5.22 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.22': + dependencies: + '@babel/parser': 7.28.4 + '@vue/shared': 3.5.22 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.22': + dependencies: + '@vue/compiler-core': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/compiler-sfc@3.5.22': + dependencies: + '@babel/parser': 7.28.4 + '@vue/compiler-core': 3.5.22 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + estree-walker: 2.0.2 + magic-string: 0.30.19 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.22': + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.7': + dependencies: + '@vue/devtools-kit': 7.7.7 + + '@vue/devtools-core@7.7.7(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@vue/devtools-kit': 7.7.7 + '@vue/devtools-shared': 7.7.7 + mitt: 3.0.1 + nanoid: 5.1.6 + pathe: 2.0.3 + vite-hot-client: 2.1.0(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vue: 3.5.22(typescript@5.6.3) + transitivePeerDependencies: + - vite + + '@vue/devtools-kit@7.7.7': + dependencies: + '@vue/devtools-shared': 7.7.7 + birpc: 2.6.1 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.7': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@2.1.10(typescript@5.6.3)': + dependencies: + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.22 + alien-signals: 0.2.2 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.6.3 + + '@vue/reactivity@3.5.22': + dependencies: + '@vue/shared': 3.5.22 + + '@vue/runtime-core@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/runtime-dom@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/runtime-core': 3.5.22 + '@vue/shared': 3.5.22 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + vue: 3.5.22(typescript@5.6.3) + + '@vue/shared@3.5.22': {} + + '@vueuse/core@13.9.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.9.0 + '@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.6.3)) + vue: 3.5.22(typescript@5.6.3) + + '@vueuse/core@9.13.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.22(typescript@5.6.3)) + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@13.9.0': {} + + '@vueuse/metadata@9.13.0': {} + + '@vueuse/shared@13.9.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + vue: 3.5.22(typescript@5.6.3) + + '@vueuse/shared@9.13.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@0.2.2: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + astral-regex@2.0.0: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + baseline-browser-mapping@2.8.8: {} + + binary-extensions@2.3.0: {} + + birpc@2.6.1: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.26.2: + dependencies: + baseline-browser-mapping: 2.8.8 + caniuse-lite: 1.0.30001745 + electron-to-chromium: 1.5.227 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.2) + + buffer-from@1.1.2: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cacheable@2.0.2: + dependencies: + '@cacheable/memoize': 2.0.2 + '@cacheable/memory': 2.0.2 + '@cacheable/utils': 2.0.2 + hookified: 1.12.1 + keyv: 5.5.3 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001745: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@3.0.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.20.3: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + convert-source-map@2.0.0: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + cosmiconfig@9.0.0(typescript@5.6.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.6.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-js@4.2.0: {} + + css-functions-list@3.2.3: {} + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + dayjs@1.11.18: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + deep-pick-omit@1.2.1: {} + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + define-lazy-prop@2.0.0: {} + + define-lazy-prop@3.0.0: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + destr@2.0.5: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.1.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + + electron-to-chromium@1.5.227: {} + + element-plus@2.11.4(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.22(typescript@5.6.3)) + '@floating-ui/dom': 1.7.4 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.20 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.22(typescript@5.6.3)) + async-validator: 4.2.5 + dayjs: 1.11.18 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.22(typescript@5.6.3) + transitivePeerDependencies: + - '@vue/composition-api' + + emoji-regex@8.0.0: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser-es@0.1.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-prettier@9.1.2(eslint@9.36.0(jiti@2.6.0)): + dependencies: + eslint: 9.36.0(jiti@2.6.0) + + eslint-plugin-prettier@5.5.4(eslint-config-prettier@9.1.2(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(prettier@3.6.2): + dependencies: + eslint: 9.36.0(jiti@2.6.0) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 9.1.2(eslint@9.36.0(jiti@2.6.0)) + + eslint-plugin-vue@9.33.0(eslint@9.36.0(jiti@2.6.0)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) + eslint: 9.36.0(jiti@2.6.0) + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.2 + vue-eslint-parser: 9.4.3(eslint@9.36.0(jiti@2.6.0)) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.36.0(jiti@2.6.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.36.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + exsolve@1.0.7: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@10.1.4: + dependencies: + flat-cache: 6.1.14 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flat-cache@6.1.14: + dependencies: + cacheable: 2.0.2 + flatted: 3.3.3 + hookified: 1.12.1 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@15.15.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globjoin@0.1.4: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + hookable@5.5.3: {} + + hookified@1.12.1: {} + + html-tags@3.3.1: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + human-signals@8.0.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immutable@5.1.3: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@5.0.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + is-what@4.1.16: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + jiti@2.6.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + keyv@5.5.3: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + known-css-properties@0.36.0: {} + + known-css-properties@0.37.0: {} + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lines-and-columns@1.2.4: {} + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + + lodash.merge@4.6.2: {} + + lodash.truncate@4.4.2: {} + + lodash@4.17.21: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.12.2: {} + + mdn-data@2.24.0: {} + + memoize-one@6.0.0: {} + + meow@13.2.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + mitt@3.0.1: {} + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + nanoid@5.1.6: {} + + natural-compare@1.4.0: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.21: {} + + normalize-path@3.0.0: {} + + normalize-wheel-es@1.2.0: {} + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nprogress@0.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + ohash@2.0.11: {} + + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pinia-plugin-persistedstate@4.5.0(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3))): + dependencies: + deep-pick-omit: 1.2.1 + defu: 6.1.4 + destr: 2.0.5 + optionalDependencies: + pinia: 3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)) + + pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 7.7.7 + vue: 3.5.22(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + postcss-html@1.8.0: + dependencies: + htmlparser2: 8.0.2 + js-tokens: 9.0.1 + postcss: 8.5.6 + postcss-safe-parser: 6.0.0(postcss@8.5.6) + + postcss-media-query-parser@0.2.3: {} + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sorting@8.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rollup-plugin-visualizer@5.14.0(rollup@4.52.3): + dependencies: + open: 8.4.2 + picomatch: 4.0.3 + source-map: 0.7.6 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.52.3 + + rollup@4.52.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.3 + '@rollup/rollup-android-arm64': 4.52.3 + '@rollup/rollup-darwin-arm64': 4.52.3 + '@rollup/rollup-darwin-x64': 4.52.3 + '@rollup/rollup-freebsd-arm64': 4.52.3 + '@rollup/rollup-freebsd-x64': 4.52.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 + '@rollup/rollup-linux-arm-musleabihf': 4.52.3 + '@rollup/rollup-linux-arm64-gnu': 4.52.3 + '@rollup/rollup-linux-arm64-musl': 4.52.3 + '@rollup/rollup-linux-loong64-gnu': 4.52.3 + '@rollup/rollup-linux-ppc64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-musl': 4.52.3 + '@rollup/rollup-linux-s390x-gnu': 4.52.3 + '@rollup/rollup-linux-x64-gnu': 4.52.3 + '@rollup/rollup-linux-x64-musl': 4.52.3 + '@rollup/rollup-openharmony-arm64': 4.52.3 + '@rollup/rollup-win32-arm64-msvc': 4.52.3 + '@rollup/rollup-win32-ia32-msvc': 4.52.3 + '@rollup/rollup-win32-x64-gnu': 4.52.3 + '@rollup/rollup-win32-x64-msvc': 4.52.3 + fsevents: 2.3.3 + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.93.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + scule@1.3.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + slash@3.0.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + speakingurl@14.0.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss-html: 1.8.0 + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-config-recess-order@4.6.0(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + stylelint-order: 6.0.4(stylelint@16.24.0(typescript@5.6.3)) + + stylelint-config-recommended-scss@14.1.0(postcss@8.5.6)(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.6) + stylelint: 16.24.0(typescript@5.6.3) + stylelint-config-recommended: 14.0.1(stylelint@16.24.0(typescript@5.6.3)) + stylelint-scss: 6.12.1(stylelint@16.24.0(typescript@5.6.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss-html: 1.8.0 + semver: 7.7.2 + stylelint: 16.24.0(typescript@5.6.3) + stylelint-config-html: 1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recommended: 17.0.0(stylelint@16.24.0(typescript@5.6.3)) + + stylelint-config-recommended@14.0.1(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-config-recommended@17.0.0(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-config-standard@36.0.1(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + stylelint-config-recommended: 14.0.1(stylelint@16.24.0(typescript@5.6.3)) + + stylelint-order@6.0.4(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss: 8.5.6 + postcss-sorting: 8.0.2(postcss@8.5.6) + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-scss@6.12.1(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.36.0 + mdn-data: 2.24.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + stylelint: 16.24.0(typescript@5.6.3) + + stylelint@16.24.0(typescript@5.6.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + '@dual-bundle/import-meta-resolve': 4.2.1 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.6.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 10.1.4 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.5 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + svg-tags@1.0.0: {} + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tailwindcss@4.1.14: {} + + tapable@2.3.0: {} + + tar@7.5.1: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + terser@5.44.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + ts-api-utils@2.1.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + tslib@2.3.0: {} + + tsx@4.20.6: + dependencies: + esbuild: 0.25.10 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript-eslint@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + eslint: 9.36.0(jiti@2.6.0) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + typescript@5.6.3: {} + + ufo@1.6.1: {} + + undici-types@7.14.0: {} + + unicorn-magic@0.3.0: {} + + unimport@5.4.0: + dependencies: + acorn: 8.15.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.2 + magic-string: 0.30.19 + mlly: 1.8.0 + pathe: 2.0.3 + picomatch: 4.0.3 + pkg-types: 2.3.0 + scule: 1.3.0 + strip-literal: 3.1.0 + tinyglobby: 0.2.15 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + + universalify@2.0.1: {} + + unplugin-auto-import@20.2.0(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.6.3))): + dependencies: + local-pkg: 1.1.2 + magic-string: 0.30.19 + picomatch: 4.0.3 + unimport: 5.4.0 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + optionalDependencies: + '@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.6.3)) + + unplugin-element-plus@0.10.0: + dependencies: + es-module-lexer: 1.7.0 + magic-string: 0.30.19 + unplugin: 2.3.10 + unplugin-utils: 0.2.5 + + unplugin-utils@0.2.5: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-utils@0.3.0: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-vue-components@29.1.0(@babel/parser@7.28.4)(vue@3.5.22(typescript@5.6.3)): + dependencies: + chokidar: 3.6.0 + debug: 4.4.3 + local-pkg: 1.1.2 + magic-string: 0.30.19 + mlly: 1.8.0 + tinyglobby: 0.2.15 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + vue: 3.5.22(typescript@5.6.3) + optionalDependencies: + '@babel/parser': 7.28.4 + transitivePeerDependencies: + - supports-color + + unplugin@2.3.10: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.1.3(browserslist@4.26.2): + dependencies: + browserslist: 4.26.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite-hot-client@2.1.0(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + + vite-plugin-compression@0.5.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + vite-plugin-inspect@0.8.9(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.3.0(rollup@4.52.3) + debug: 4.4.3 + error-stack-parser-es: 0.1.5 + fs-extra: 11.3.2 + open: 10.2.0 + perfect-debounce: 1.0.0 + picocolors: 1.1.1 + sirv: 3.0.2 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - rollup + - supports-color + + vite-plugin-vue-devtools@7.7.7(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@vue/devtools-core': 7.7.7(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + '@vue/devtools-kit': 7.7.7 + '@vue/devtools-shared': 7.7.7 + execa: 9.6.0 + sirv: 3.0.2 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-inspect: 0.8.9(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + transitivePeerDependencies: + - '@nuxt/kit' + - rollup + - supports-color + - vue + + vite-plugin-vue-inspector@5.3.2(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.4) + '@vue/compiler-dom': 3.5.22 + kolorist: 1.8.0 + magic-string: 0.30.19 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.8.1 + fsevents: 2.3.3 + jiti: 2.6.0 + lightningcss: 1.30.1 + sass: 1.93.2 + terser: 5.44.0 + tsx: 4.20.6 + yaml: 2.8.1 + + vscode-uri@3.1.0: {} + + vue-demi@0.14.10(vue@3.5.22(typescript@5.6.3)): + dependencies: + vue: 3.5.22(typescript@5.6.3) + + vue-draggable-plus@0.6.0(@types/sortablejs@1.15.8): + dependencies: + '@types/sortablejs': 1.15.8 + + vue-eslint-parser@9.4.3(eslint@9.36.0(jiti@2.6.0)): + dependencies: + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.0) + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + vue-i18n@9.14.5(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@intlify/core-base': 9.14.5 + '@intlify/shared': 9.14.5 + '@vue/devtools-api': 6.6.4 + vue: 3.5.22(typescript@5.6.3) + + vue-router@4.5.1(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.22(typescript@5.6.3) + + vue-tsc@2.1.10(typescript@5.6.3): + dependencies: + '@volar/typescript': 2.4.23 + '@vue/language-core': 2.1.10(typescript@5.6.3) + semver: 7.7.2 + typescript: 5.6.3 + + vue@3.5.22(typescript@5.6.3): + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-sfc': 3.5.22 + '@vue/runtime-dom': 3.5.22 + '@vue/server-renderer': 3.5.22(vue@3.5.22(typescript@5.6.3)) + '@vue/shared': 3.5.22 + optionalDependencies: + typescript: 5.6.3 + + webpack-virtual-modules@0.6.2: {} + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + + xml-name-validator@4.0.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yaml@2.8.1: + optional: true + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yoctocolors@2.1.2: {} + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 diff --git a/admin/pnpm-workspace.yaml b/admin/pnpm-workspace.yaml new file mode 100644 index 0000000..d77c0f0 --- /dev/null +++ b/admin/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +onlyBuiltDependencies: + - "@parcel/watcher" + - "@tailwindcss/oxide" + - core-js + - esbuild + - es5-ext + - vue-demi diff --git a/admin/public/favicon.png b/admin/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..11cba749bc5eeaa66b2f2877e725994ebefa7888 GIT binary patch literal 20288 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 X6(T1O#cdT?00000NkvXXu0mjf7PF-X literal 0 HcmV?d00001 diff --git a/admin/scripts/clean-dev.ts b/admin/scripts/clean-dev.ts new file mode 100644 index 0000000..cc0b9bc --- /dev/null +++ b/admin/scripts/clean-dev.ts @@ -0,0 +1,838 @@ +// scripts/clean-dev.ts +import fs from 'fs/promises' +import path from 'path' + +// 现代化颜色主题 +const theme = { + // 基础颜色 + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + + // 前景色 + primary: '\x1b[38;5;75m', // 亮蓝色 + success: '\x1b[38;5;82m', // 亮绿色 + warning: '\x1b[38;5;220m', // 亮黄色 + error: '\x1b[38;5;196m', // 亮红色 + info: '\x1b[38;5;159m', // 青色 + purple: '\x1b[38;5;141m', // 紫色 + orange: '\x1b[38;5;208m', // 橙色 + gray: '\x1b[38;5;245m', // 灰色 + white: '\x1b[38;5;255m', // 白色 + + // 背景色 + bgDark: '\x1b[48;5;235m', // 深灰背景 + bgBlue: '\x1b[48;5;24m', // 蓝色背景 + bgGreen: '\x1b[48;5;22m', // 绿色背景 + bgRed: '\x1b[48;5;52m' // 红色背景 +} + +// 现代化图标集 +const icons = { + rocket: '🚀', + fire: '🔥', + star: '⭐', + gem: '💎', + crown: '👑', + magic: '✨', + warning: '⚠️', + success: '✅', + error: '❌', + info: 'ℹ️', + folder: '📁', + file: '📄', + image: '🖼️', + code: '💻', + data: '📊', + globe: '🌐', + map: '🗺️', + chat: '💬', + bolt: '⚡', + shield: '🛡️', + key: '🔑', + link: '🔗', + clean: '🧹', + trash: '🗑️', + check: '✓', + cross: '✗', + arrow: '→', + loading: '⏳' +} + +// 格式化工具 +const fmt = { + title: (text: string) => `${theme.bold}${theme.primary}${text}${theme.reset}`, + subtitle: (text: string) => `${theme.purple}${text}${theme.reset}`, + success: (text: string) => `${theme.success}${text}${theme.reset}`, + error: (text: string) => `${theme.error}${text}${theme.reset}`, + warning: (text: string) => `${theme.warning}${text}${theme.reset}`, + info: (text: string) => `${theme.info}${text}${theme.reset}`, + highlight: (text: string) => `${theme.bold}${theme.white}${text}${theme.reset}`, + dim: (text: string) => `${theme.dim}${theme.gray}${text}${theme.reset}`, + orange: (text: string) => `${theme.orange}${text}${theme.reset}`, + + // 带背景的文本 + badge: (text: string, bg: string = theme.bgBlue) => + `${bg}${theme.white}${theme.bold} ${text} ${theme.reset}`, + + // 渐变效果模拟 + gradient: (text: string) => { + const colors = ['\x1b[38;5;75m', '\x1b[38;5;81m', '\x1b[38;5;87m', '\x1b[38;5;159m'] + const chars = text.split('') + return chars.map((char, i) => `${colors[i % colors.length]}${char}`).join('') + theme.reset + } +} + +// 创建现代化标题横幅 +function createModernBanner() { + console.log() + console.log( + fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗') + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + ` ║ ${icons.rocket} ${fmt.title('ART DESIGN PRO')} ${fmt.subtitle('· 代码精简程序')} ${icons.magic} ║` + ) + console.log( + ` ║ ${fmt.dim('为项目移除演示数据,快速切换至开发模式')} ║` + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝') + ) + console.log() +} + +// 创建分割线 +function createDivider(char = '─', color = theme.primary) { + console.log(`${color}${' ' + char.repeat(66)}${theme.reset}`) +} + +// 创建卡片样式容器 +function createCard(title: string, content: string[]) { + console.log(` ${fmt.badge('', theme.bgBlue)} ${fmt.title(title)}`) + console.log() + content.forEach((line) => { + console.log(` ${line}`) + }) + console.log() +} + +// 进度条动画 +function createProgressBar(current: number, total: number, text: string, width = 40) { + const percentage = Math.round((current / total) * 100) + const filled = Math.round((current / total) * width) + const empty = width - filled + + const filledBar = '█'.repeat(filled) + const emptyBar = '░'.repeat(empty) + + process.stdout.write( + `\r ${fmt.info('进度')} [${theme.success}${filledBar}${theme.gray}${emptyBar}${theme.reset}] ${fmt.highlight(percentage + '%')})}` + ) + + if (current === total) { + console.log() + } +} + +// 统计信息 +const stats = { + deletedFiles: 0, + deletedPaths: 0, + failedPaths: 0, + startTime: Date.now(), + totalFiles: 0 +} + +// 清理目标 +const targets = [ + 'README.md', + 'README.zh-CN.md', + 'CHANGELOG.md', + 'CHANGELOG.zh-CN.md', + 'src/views/change', + 'src/views/safeguard', + 'src/views/article', + 'src/views/examples', + 'src/views/system/nested', + 'src/views/widgets', + 'src/views/template', + 'src/views/dashboard/analysis', + 'src/views/dashboard/ecommerce', + 'src/mock/json', + 'src/mock/temp/articleList.ts', + 'src/mock/temp/commentDetail.ts', + 'src/mock/temp/commentList.ts', + 'src/assets/images/cover', + 'src/assets/images/safeguard', + 'src/assets/images/3d', + 'src/components/core/charts/art-map-chart', + 'src/components/business/comment-widget' +] + +// 递归统计文件数量 +async function countFiles(targetPath: string): Promise { + const fullPath = path.resolve(process.cwd(), targetPath) + + try { + const stat = await fs.stat(fullPath) + + if (stat.isFile()) { + return 1 + } else if (stat.isDirectory()) { + const entries = await fs.readdir(fullPath) + let count = 0 + + for (const entry of entries) { + const entryPath = path.join(targetPath, entry) + count += await countFiles(entryPath) + } + + return count + } + } catch { + return 0 + } + + return 0 +} + +// 统计所有目标的文件数量 +async function countAllFiles(): Promise { + let totalCount = 0 + + for (const target of targets) { + const count = await countFiles(target) + totalCount += count + } + + return totalCount +} + +// 删除文件和目录 +async function remove(targetPath: string, index: number) { + const fullPath = path.resolve(process.cwd(), targetPath) + + createProgressBar(index + 1, targets.length, targetPath) + + try { + const fileCount = await countFiles(targetPath) + await fs.rm(fullPath, { recursive: true, force: true }) + stats.deletedFiles += fileCount + stats.deletedPaths++ + await new Promise((resolve) => setTimeout(resolve, 50)) + } catch (err) { + stats.failedPaths++ + console.log() + console.log(` ${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(targetPath)}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理路由模块 +async function cleanRouteModules() { + const modulesPath = path.resolve(process.cwd(), 'src/router/modules') + + try { + // 删除演示相关的路由模块 + const modulesToRemove = [ + 'template.ts', + 'widgets.ts', + 'examples.ts', + 'article.ts', + 'safeguard.ts', + 'help.ts' + ] + + for (const module of modulesToRemove) { + const modulePath = path.join(modulesPath, module) + try { + await fs.rm(modulePath, { force: true }) + } catch { + // 文件不存在时忽略错误 + } + } + + // 重写 dashboard.ts - 只保留 console + const dashboardContent = `import { AppRouteRecord } from '@/types/router' + +export const dashboardRoutes: AppRouteRecord = { + name: 'Dashboard', + path: '/dashboard', + component: '/index/index', + meta: { + title: 'menus.dashboard.title', + icon: 'ri:pie-chart-line', + roles: ['R_SUPER', 'R_ADMIN'] + }, + children: [ + { + path: 'console', + name: 'Console', + component: '/dashboard/console', + meta: { + title: 'menus.dashboard.console', + keepAlive: false, + fixedTab: true + } + } + ] +} +` + await fs.writeFile(path.join(modulesPath, 'dashboard.ts'), dashboardContent, 'utf-8') + + // 重写 system.ts - 移除 nested 嵌套菜单 + const systemContent = `import { AppRouteRecord } from '@/types/router' + +export const systemRoutes: AppRouteRecord = { + path: '/system', + name: 'System', + component: '/index/index', + meta: { + title: 'menus.system.title', + icon: 'ri:user-3-line', + roles: ['R_SUPER', 'R_ADMIN'] + }, + children: [ + { + path: 'user', + name: 'User', + component: '/system/user', + meta: { + title: 'menus.system.user', + keepAlive: true, + roles: ['R_SUPER', 'R_ADMIN'] + } + }, + { + path: 'role', + name: 'Role', + component: '/system/role', + meta: { + title: 'menus.system.role', + keepAlive: true, + roles: ['R_SUPER'] + } + }, + { + path: 'user-center', + name: 'UserCenter', + component: '/system/user-center', + meta: { + title: 'menus.system.userCenter', + isHide: true, + keepAlive: true, + isHideTab: true + } + }, + { + path: 'menu', + name: 'Menus', + component: '/system/menu', + meta: { + title: 'menus.system.menu', + keepAlive: true, + roles: ['R_SUPER'], + authList: [ + { title: '新增', authMark: 'add' }, + { title: '编辑', authMark: 'edit' }, + { title: '删除', authMark: 'delete' } + ] + } + } + ] +} +` + await fs.writeFile(path.join(modulesPath, 'system.ts'), systemContent, 'utf-8') + + // 重写 index.ts - 只导入保留的模块 + const indexContent = `import { AppRouteRecord } from '@/types/router' +import { dashboardRoutes } from './dashboard' +import { systemRoutes } from './system' +import { resultRoutes } from './result' +import { exceptionRoutes } from './exception' + +/** + * 导出所有模块化路由 + */ +export const routeModules: AppRouteRecord[] = [ + dashboardRoutes, + systemRoutes, + resultRoutes, + exceptionRoutes +] +` + await fs.writeFile(path.join(modulesPath, 'index.ts'), indexContent, 'utf-8') + + console.log(` ${icons.success} ${fmt.success('清理路由模块完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理路由模块失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理路由别名 +async function cleanRoutesAlias() { + const routesAliasPath = path.resolve(process.cwd(), 'src/router/routesAlias.ts') + + try { + const cleanedAlias = `/** + * 公共路由别名 + # 存放系统级公共路由路径,如布局容器、登录页等 + */ +export enum RoutesAlias { + Layout = '/index/index', // 布局容器 + Login = '/auth/login' // 登录页 +} +` + + await fs.writeFile(routesAliasPath, cleanedAlias, 'utf-8') + console.log(` ${icons.success} ${fmt.success('重写路由别名配置完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理路由别名失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理变更日志 +async function cleanChangeLog() { + const changeLogPath = path.resolve(process.cwd(), 'src/mock/upgrade/changeLog.ts') + + try { + const cleanedChangeLog = `import { ref } from 'vue' + +interface UpgradeLog { + version: string // 版本号 + title: string // 更新标题 + date: string // 更新日期 + detail?: string[] // 更新内容 + requireReLogin?: boolean // 是否需要重新登录 + remark?: string // 备注 +} + +export const upgradeLogList = ref([]) +` + + await fs.writeFile(changeLogPath, cleanedChangeLog, 'utf-8') + console.log(` ${icons.success} ${fmt.success('清空变更日志数据完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理变更日志失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理语言文件 +async function cleanLanguageFiles() { + const languageFiles = [ + { path: 'src/locales/langs/zh.json', name: '中文语言文件' }, + { path: 'src/locales/langs/en.json', name: '英文语言文件' } + ] + + for (const { path: langPath, name } of languageFiles) { + try { + const fullPath = path.resolve(process.cwd(), langPath) + const content = await fs.readFile(fullPath, 'utf-8') + const langData = JSON.parse(content) + + const menusToRemove = [ + 'widgets', + 'template', + 'article', + 'examples', + 'safeguard', + 'plan', + 'help' + ] + + if (langData.menus) { + menusToRemove.forEach((menuKey) => { + if (langData.menus[menuKey]) { + delete langData.menus[menuKey] + } + }) + + if (langData.menus.dashboard) { + if (langData.menus.dashboard.analysis) { + delete langData.menus.dashboard.analysis + } + if (langData.menus.dashboard.ecommerce) { + delete langData.menus.dashboard.ecommerce + } + } + + if (langData.menus.system) { + const systemKeysToRemove = [ + 'nested', + 'menu1', + 'menu2', + 'menu21', + 'menu3', + 'menu31', + 'menu32', + 'menu321' + ] + systemKeysToRemove.forEach((key) => { + if (langData.menus.system[key]) { + delete langData.menus.system[key] + } + }) + } + } + + await fs.writeFile(fullPath, JSON.stringify(langData, null, 2), 'utf-8') + console.log(` ${icons.success} ${fmt.success(`清理${name}完成`)}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error(`清理${name}失败`)}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } + } +} + +// 清理快速入口组件 +async function cleanFastEnterComponent() { + const fastEnterPath = path.resolve(process.cwd(), 'src/config/fastEnter.ts') + + try { + const cleanedFastEnter = `/** + * 快速入口配置 + * 包含:应用列表、快速链接等配置 + */ +import { WEB_LINKS } from '@/utils/constants' +import type { FastEnterConfig } from '@/types/config' + +const fastEnterConfig: FastEnterConfig = { + // 显示条件(屏幕宽度) + minWidth: 1200, + // 应用列表 + applications: [ + { + name: '工作台', + description: '系统概览与数据统计', + icon: 'ri:pie-chart-line', + iconColor: '#377dff', + enabled: true, + order: 1, + routeName: 'Console' + }, + { + name: '官方文档', + description: '使用指南与开发文档', + icon: 'ri:bill-line', + iconColor: '#ffb100', + enabled: true, + order: 2, + link: WEB_LINKS.DOCS + }, + { + name: '技术支持', + description: '技术支持与问题反馈', + icon: 'ri:user-location-line', + iconColor: '#ff6b6b', + enabled: true, + order: 3, + link: WEB_LINKS.COMMUNITY + }, + { + name: '哔哩哔哩', + description: '技术分享与交流', + icon: 'ri:bilibili-line', + iconColor: '#FB7299', + enabled: true, + order: 4, + link: WEB_LINKS.BILIBILI + } + ], + // 快速链接 + quickLinks: [ + { + name: '登录', + enabled: true, + order: 1, + routeName: 'Login' + }, + { + name: '注册', + enabled: true, + order: 2, + routeName: 'Register' + }, + { + name: '忘记密码', + enabled: true, + order: 3, + routeName: 'ForgetPassword' + }, + { + name: '个人中心', + enabled: true, + order: 4, + routeName: 'UserCenter' + } + ] +} + +export default Object.freeze(fastEnterConfig) +` + + await fs.writeFile(fastEnterPath, cleanedFastEnter, 'utf-8') + console.log(` ${icons.success} ${fmt.success('清理快速入口配置完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理快速入口配置失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 更新菜单接口 +async function updateMenuApi() { + const apiPath = path.resolve(process.cwd(), 'src/api/system-manage.ts') + + try { + const content = await fs.readFile(apiPath, 'utf-8') + const updatedContent = content.replace( + "url: '/api/v3/system/menus'", + "url: '/api/v3/system/menus/simple'" + ) + + await fs.writeFile(apiPath, updatedContent, 'utf-8') + console.log(` ${icons.success} ${fmt.success('更新菜单接口完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('更新菜单接口失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 用户确认函数 +async function getUserConfirmation(): Promise { + const { createInterface } = await import('readline') + + return new Promise((resolve) => { + const rl = createInterface({ + input: process.stdin, + output: process.stdout + }) + + console.log( + ` ${fmt.highlight('请输入')} ${fmt.success('yes')} ${fmt.highlight('确认执行清理操作,或按 Enter 取消')}` + ) + console.log() + process.stdout.write(` ${icons.arrow} `) + + rl.question('', (answer: string) => { + rl.close() + resolve(answer.toLowerCase().trim() === 'yes') + }) + }) +} + +// 显示清理警告 +async function showCleanupWarning() { + createCard('安全警告', [ + `${fmt.warning('此操作将永久删除以下演示内容,且无法恢复!')}`, + `${fmt.dim('请仔细阅读清理列表,确认后再继续操作')}` + ]) + + const cleanupItems = [ + { + icon: icons.image, + name: '图片资源', + desc: '演示用的封面图片、3D图片、运维图片等', + color: theme.orange + }, + { + icon: icons.file, + name: '演示页面', + desc: 'widgets、template、article、examples、safeguard等页面', + color: theme.purple + }, + { + icon: icons.code, + name: '路由模块文件', + desc: '删除演示路由模块,只保留核心模块(dashboard、system、result、exception)', + color: theme.primary + }, + { + icon: icons.link, + name: '路由别名', + desc: '重写routesAlias.ts,移除演示路由别名', + color: theme.info + }, + { + icon: icons.data, + name: 'Mock数据', + desc: '演示用的JSON数据、文章列表、评论数据等', + color: theme.success + }, + { + icon: icons.globe, + name: '多语言文件', + desc: '清理中英文语言包中的演示菜单项', + color: theme.warning + }, + { icon: icons.map, name: '地图组件', desc: '移除art-map-chart地图组件', color: theme.error }, + { icon: icons.chat, name: '评论组件', desc: '移除comment-widget评论组件', color: theme.orange }, + { + icon: icons.bolt, + name: '快速入口', + desc: '移除分析页、礼花效果、聊天、更新日志、定价、留言管理等无效项目', + color: theme.purple + } + ] + + console.log(` ${fmt.badge('', theme.bgRed)} ${fmt.title('将要清理的内容')}`) + console.log() + + cleanupItems.forEach((item, index) => { + console.log(` ${item.color}${theme.reset} ${fmt.highlight(`${index + 1}. ${item.name}`)}`) + console.log(` ${fmt.dim(item.desc)}`) + }) + + console.log() + console.log(` ${fmt.badge('', theme.bgGreen)} ${fmt.title('保留的功能模块')}`) + console.log() + + const preservedModules = [ + { name: 'Dashboard', desc: '工作台页面' }, + { name: 'System', desc: '系统管理模块' }, + { name: 'Result', desc: '结果页面' }, + { name: 'Exception', desc: '异常页面' }, + { name: 'Auth', desc: '登录注册功能' }, + { name: 'Core Components', desc: '核心组件库' } + ] + + preservedModules.forEach((module) => { + console.log(` ${icons.check} ${fmt.success(module.name)} ${fmt.dim(`- ${module.desc}`)}`) + }) + + console.log() + createDivider() + console.log() +} + +// 显示统计信息 +async function showStats() { + const duration = Date.now() - stats.startTime + const seconds = (duration / 1000).toFixed(2) + + console.log() + createCard('清理统计', [ + `${fmt.success('成功删除')}: ${fmt.highlight(stats.deletedFiles.toString())} 个文件`, + `${fmt.info('涉及路径')}: ${fmt.highlight(stats.deletedPaths.toString())} 个目录/文件`, + ...(stats.failedPaths > 0 + ? [ + `${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(stats.failedPaths.toString())} 个路径` + ] + : []), + `${fmt.info('耗时')}: ${fmt.highlight(seconds)} 秒` + ]) +} + +// 创建成功横幅 +function createSuccessBanner() { + console.log() + console.log( + fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗') + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + ` ║ ${icons.star} ${fmt.success('清理完成!项目已准备就绪')} ${icons.rocket} ║` + ) + console.log( + ` ║ ${fmt.dim('现在可以开始您的开发之旅了!')} ║` + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝') + ) + console.log() +} + +// 主函数 +async function main() { + // 清屏并显示横幅 + console.clear() + createModernBanner() + + // 显示清理警告 + await showCleanupWarning() + + // 统计文件数量 + console.log(` ${fmt.info('正在统计文件数量...')}`) + stats.totalFiles = await countAllFiles() + + console.log(` ${fmt.info('即将清理')}: ${fmt.highlight(stats.totalFiles.toString())} 个文件`) + console.log(` ${fmt.dim(`涉及 ${targets.length} 个目录/文件路径`)}`) + console.log() + + // 用户确认 + const confirmed = await getUserConfirmation() + + if (!confirmed) { + console.log(` ${fmt.warning('操作已取消,清理中止')}`) + console.log() + return + } + + console.log() + console.log(` ${icons.check} ${fmt.success('确认成功,开始清理...')}`) + console.log() + + // 开始清理过程 + console.log(` ${fmt.badge('步骤 1/6', theme.bgBlue)} ${fmt.title('删除演示文件')}`) + console.log() + for (let i = 0; i < targets.length; i++) { + await remove(targets[i], i) + } + console.log() + + console.log(` ${fmt.badge('步骤 2/6', theme.bgBlue)} ${fmt.title('清理路由模块')}`) + console.log() + await cleanRouteModules() + console.log() + + console.log(` ${fmt.badge('步骤 3/6', theme.bgBlue)} ${fmt.title('重写路由别名')}`) + console.log() + await cleanRoutesAlias() + console.log() + + console.log(` ${fmt.badge('步骤 4/6', theme.bgBlue)} ${fmt.title('清空变更日志')}`) + console.log() + await cleanChangeLog() + console.log() + + console.log(` ${fmt.badge('步骤 5/6', theme.bgBlue)} ${fmt.title('清理语言文件')}`) + console.log() + await cleanLanguageFiles() + console.log() + + console.log(` ${fmt.badge('步骤 6/7', theme.bgBlue)} ${fmt.title('清理快速入口')}`) + console.log() + await cleanFastEnterComponent() + console.log() + + console.log(` ${fmt.badge('步骤 7/7', theme.bgBlue)} ${fmt.title('更新菜单接口')}`) + console.log() + await updateMenuApi() + + // 显示统计信息 + await showStats() + + // 显示成功横幅 + createSuccessBanner() +} + +main().catch((err) => { + console.log() + console.log(` ${icons.error} ${fmt.error('清理脚本执行出错')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + console.log() + process.exit(1) +}) diff --git a/admin/src/App.vue b/admin/src/App.vue new file mode 100644 index 0000000..23745c9 --- /dev/null +++ b/admin/src/App.vue @@ -0,0 +1,39 @@ + + + diff --git a/admin/src/api/auth.ts b/admin/src/api/auth.ts new file mode 100644 index 0000000..fc35461 --- /dev/null +++ b/admin/src/api/auth.ts @@ -0,0 +1,60 @@ +/** + * 认证 API——脚手架阶段(Phase 3 Task 2)的开发桩。 + * + * 后端真实端点为 /api/platform/auth/*(登录 / 用户信息 / 改密), + * Task 3 对接真实登录时将本文件替换为真实请求。 + * + * 桩行为: + * - fetchLogin:开发态直接发放本地会话令牌,不发起网络请求; + * - fetchGetUserInfo:按登录页的角色调试开关(sessionStorage + * `all-kefu-dev-role`:super | tenant)返回用户信息,用于演示 + * 纯前端菜单权限控制——super 全量菜单,tenant 隐藏 + * 租户管理 / 平台设置(见 router/modules/*.ts 的 meta.roles); + * - 生产构建(非 DEV)下调用将抛出显式错误,避免带桩上线。 + */ +const DEV_ROLE_KEY = 'all-kefu-dev-role' +export type DevStubRole = 'super' | 'tenant' + +export function setDevStubRole(role: DevStubRole) { + sessionStorage.setItem(DEV_ROLE_KEY, role) +} + +export function getDevStubRole(): DevStubRole { + return sessionStorage.getItem(DEV_ROLE_KEY) === 'tenant' ? 'tenant' : 'super' +} + +function assertStubAllowed(): void { + if (!import.meta.env.DEV) { + throw new Error('认证 API 尚未对接真实后端(Task 3 实现),生产构建不可用') + } +} + +/** + * 登录(开发桩:任意账号密码直接获得开发会话) + * @param params 登录参数 + * @returns 登录响应 + */ +export function fetchLogin(params: Api.Auth.LoginParams) { + assertStubAllowed() + void params + return Promise.resolve({ + token: 'dev-stub-token', + refreshToken: '' + }) +} + +/** + * 获取用户信息(开发桩:角色取自登录页调试开关) + * @returns 用户信息 + */ +export function fetchGetUserInfo() { + assertStubAllowed() + const role = getDevStubRole() + return Promise.resolve({ + buttons: [], + roles: [role], + userId: 1, + userName: role === 'super' ? '平台超管(调试)' : '租户管理员(调试)', + email: 'dev@all-kefu.local' + }) +} diff --git a/admin/src/assets/images/3d/icon1.webp b/admin/src/assets/images/3d/icon1.webp new file mode 100644 index 0000000000000000000000000000000000000000..631094c535ee590cf85116f982025cdfa3fdd5cd GIT binary patch literal 10076 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 XXQx*3k}r&AvbXz>_KWy0`v>?RO2)Ex literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/3d/icon2.webp b/admin/src/assets/images/3d/icon2.webp new file mode 100644 index 0000000000000000000000000000000000000000..a9e47428dae9eabdf1c63adaeccce94990dd6f59 GIT binary patch literal 6712 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 z zV`^-s!C(E)<}8B7v6zaZxvtfS6bOFwmoLwZ^!+A*p$%lNv!qsS{``-0et818Ulr1O zY>YjXSTghE9!(66mThy)LuGyF;iB)X*+wi7@NU{4JsEMKJ}TSw`_HQTzxclZOxO_V literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/3d/icon4.webp b/admin/src/assets/images/3d/icon4.webp new file mode 100644 index 0000000000000000000000000000000000000000..522108f67aa47ed2d5a201fe615cca63802c9008 GIT binary patch literal 5762 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 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/3d/icon5.webp b/admin/src/assets/images/3d/icon5.webp new file mode 100644 index 0000000000000000000000000000000000000000..b8f794dac5a3e04a21f546391531e50b28eaeda5 GIT binary patch literal 7932 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 n0plREUR` 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 cmY(20IDjVb)DMN`D2T(p^ZM$jri0Q@7_2mk;8 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/avatar/avatar1.webp b/admin/src/assets/images/avatar/avatar1.webp new file mode 100644 index 0000000000000000000000000000000000000000..68e256c297cea6017efb892509f005290f32c95f GIT binary patch literal 2296 zcmVLHNCsINV&i=yrkQTpUuSn>Acb z2FE|`v?5W%`1-J1k%&b^{1D+E*v&*cgPXpOkC1j6c3zl~THPT33XotPrLtyQ6T5WG zK)!TAT}RO#s23>vzA4R_R4Z`A z)!tkNowc|9f8G#J-=@;b?8g;}XfMv3vXpQ%%8H}JEd>Q-E=Is*dOG0dKP#mpzK6Zu z!~hOswhdJQ>~$JV9YE{yxG0G7eyp!4WjSqX4`vFrIzpIQ0C7!XfcamHM$sc`M2fb8 zp?r)l(JQ^6s8qm)T%}1_SBx8Ei&#F=-z9}B6N}fuspI`o)nIez>LrpO#ZCv{{2?o7 zizMB-j`ht%neuFw9`x-5QLn<^-`6#2Q&Jjy+o`+KTIQK8(?v)43qCH3lBIY}J^8V+ z!&1a_htv&I@_M)c0092^2zg+Z2FA6yE){J(rlOXGqB`_f57m^A&4PKhbc!KFs7OB8 zdlaLr7TvuJI%#4#-P7{r$I-Vawb#sYrY1ZHul|y?8{3F>u|DwD9~|=ZB=TKNp#;aH z6p{8M)-)<+c{^=m6B0X{RudGJjVD&&L_)4Q_O-UM8aHDF436jm=6;o62nG$yl1N%V z^US=K*lNl}uT*xK!uV;kLRBU>FcxuN!aJ-6^T-zU<4BjZHoo!!CG`NKe=YJ;2G_dxc7RRuC!*ki8f7LL%n`-+KYn3a!J?P@;tfvk2Nst5@wK3y zRdIUSYB8(CZTDhk8nxBMCfFOiau)hb=Rx7ub2hR3WL{Hyrpsf+J+hGLMDG-~woLz+c}Z@!7eEkAC9e9%w>(!zlJ@E3xS^#||H1lMw>!0H5qz`? zDTk_akMd^!5{RFXFk>aW@yCUb(ne!t0QU(B9Kw(BX97mmD2irsOwo)JKe)-C9>6-W z8>zQiA&goy3?**OV)g6ha!+LWYa$kEDvBX5cFt_J}Y0=#0<)9Gxp~2QlLcB8}FqW>Rt=Zky2M5|a z;Hc#Pb0Ak4j|G(TYytIeThz9Q7q9nW1+bIw%qZ{hkGJdCN^(2I&!>>*jiM;zsNsay zfD%f*gMDH+xlIIpl2^zsEveXG-)Itxrc5f{VG z2+%x%J-=#LxVgp3INtBtb9Dls=&UII?9k3BspvyU-yDWy#ZZ~p{*3H`GxnG%ZA&CL z&*2_n6+ig<_r zr$au4SfLz_%DUXgc@3zY%{O#}L*SV!h@$ z0&J2lMSWRS?_2t+t%IovykIn^Gf0E(4)1S?`Q&xoPeYMB3I7AQVb*GGTG&qx)z35` zm-*_NYHX|F!A=SFfAm+@A_k_kST8zZ^As*W84Ywga!W-8E;52QU#|P1>A>0AG0ZKh z1{j-VQFdICVO6OER;VL$V>bR5N9PP03GmD;8Qdn}J)%-_?J)}HeZciZ5q@(VMY4g( z+?!k6V8t}Nw0vuxbc5?JCw4MK7peL5#PnJHvA}FX`~lUGz3J zY|qT!%$9q#ltNvl4zfC+1!n>208_(%8;BBBv?g=;kmh|_nCPRCC&3y^hj$-@-t&>} z-b^A#l{w#iwS?_Z7@c;6zG?p5>zH=`r^z&UF!0@+o>6G)n+?TjkXzz577X8g2C<5o SVD2lzkuUx<_ZG<9l>h)e2Ygfj literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/avatar/avatar10.webp b/admin/src/assets/images/avatar/avatar10.webp new file mode 100644 index 0000000000000000000000000000000000000000..a813d4c23ca9d2945fa089f0e4281a7e6e122e42 GIT binary patch literal 1410 zcmV-|1%3KbNk&F`1pok7MM6+kP&goN1pok0BmkWODxd(M06w);rA;R!qoSu4ia_8N ziDxRj@G(#c8w@MiZ2Pm1xtXg&SV}tO*upn_$e3T{3IKsG)2P7zl)<|FlG0beLZ66? zP8MKY$g2W!|C9%Jz7n>$67R(*k0AH;%P<`vmTFO)=V##pXZCZ)gtN(Z%p*SC`lfMv zD>&oYT&*hXU^Rn_m=qyJP9*z79qaCF(f*3BmcvomuMR=u8kHme^Vh+Y#SverH3t|FRmiPRS&TRUoCKe-bsYMg$7@O?MPgGwcid>XA$|n6Z=L) zZ2uB6qou3e=(axuwzL2Vp-2eWFAM$_IkRILs9hSH#S0Z+MXb;wb8oKdBte3;R)bA} z*+BGDjYM<{i3fa8{Zlse2Nb(xOK+^ybEndK`Zlp`kDA%hMHUtv;J0j=R{~n*D$iXB z&!JSJ=<`@=^kw-&i^Bsa0`G0K_b+M}`aMe#zB&dJWAP(c47c>965b2)B(4!Vx4$; zw1H=hX#>fy7P?nk_rl#aH{h1yQ$2BN$smP1u-VbsMnWv11? zLp058?xk1an|Sw(?NjDb;uebjVkWHHiQ7W(f`P#D#fli5R=&I_)-XU?fc6SBhQ?kN zJ$AQ42J^$gWWE%oe-N;M|ApbrPsxz_(cK-~ZAFgxPE2^B`Q75YhbMy^7Pw~YD-ChS zA=>F9_HN9N3FH?)0f>*-9{l#atdGpurvFT^8H)aFa0%PFn~F3`ubg8gY$SI3_!~2` zS_l(^H=T)7S7kzz(;dvwpf17sUZ}3lKcvMGGLfTwlHX5cAC;>SC~wT_CZ)TPLJ|+^ zO1^m4YAnp%qoe6yc=?;Ew9>7%+BplwY*&G+-T%ccSxkn1Qw7Y2OFWn*`da)d=D&$8 zPbb~qkF!m1wX=~@J>R*@ZYZ}fr6gbA|E>MzHvxRQN8k;s6giiV7hzBm=J2h|Fq!!k zd3*yobrI{Cf)}+vu8w>&2vWX5W68fofgrK9qFsRY-2?*b~wc7TUc73C>=31jaEDl4iGgxUWZ zB4rTdKzTY{)gGK&8Am&B2>R;B9wKiG*l5R=A=#t?3mmUkxUGS7kn QTF;h&;#lF?m{x!Q0CCo}*Z=?k literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/avatar/avatar2.webp b/admin/src/assets/images/avatar/avatar2.webp new file mode 100644 index 0000000000000000000000000000000000000000..6716e3ff9a7c07198562500a8b7b15d86eddd92a GIT binary patch literal 1214 zcmV;v1VQ^!Nk&Gt1ONb6MM6+kP&go}1ONcgApo5LDxd(M06w)=qfMtIqa!F1nSkIH ziDv`U9+vFLued9_Zk!^>Wl;K+9b7eCz6OCmMn0FL9D}b?UW*)0DvXH~pQ8`=L^k!S z;|hBW@IV2l)uK>R-FNYc+SYfQ!K{0UUoS)r!~usj>;<1jK?oJGott!`iwAyL7FRmC z-Y;KTTW2Hvfp-4GwV#D7zJ{Rb^7WeASBNz9cpAkZEiENA1V!S0czmN@w)?z{#g9Bj zN3L%T&9DMCz9B9tSrf*2Q|9%<m10@CpJ4Ct6;J zv8mABpXb@qe(jYp6o~dtX!0AgI_V{Zi+3d3Z)8yraRejLfT zD@9>8hzB?0f+b)9@uB)7bA?I+bS*?&N_P`8xId6*N%-Ke12MS=)A5_&$3|F!Dp&NjPIAOh^5n}*G|5}xvozF@|OCVXiUiEyRC zZ%Ds_62Qa634Q3s#Iz2)4XXyIAGFGu1-IZ_bt!rf$O|E@i>r zF<-&YyBTl_lA~9CR#Vor54ntOC1G*`**>@em6c83wTBuf^8@|WPQ1S`e$OUd8_vMl ze&sBh0h*=WJ9$}hK75OYJQcZGpIZi+958CfHG%-%6SkKaxCV;NU@%7d^hu;D|MBw| z2u&G`Xy2?R&bGLXM*+_ssX7Mdm-8?^PZAUxc(g!k5*f5Xntt)ggYz9mEY(JUi100< z@faGm-y}03=NMr?Hq1WP6Haj^I*l>^tX|+^e}49%tfQhC{#4Q0U5F3I30!07i!noo zI=*J&q{9NF2wCi3*P1wd;t{8^X;2f|!&fhmQVN}lP?=^gyZQoU`Q!NM5KDZ#5RXBRvh&VL<-w6$Rc!dFR_sKLV4`N3P zddw5_>mnzH4l*YuJO=aP{@^7(0?VqAv&Mdz+ApL5?=%74)z5pLHQ_0_`iEY)MQ{kR z5&aekzx`ibYi@bE3%O)z$QJFn7C2uRohw`6Nf{e>;6Q-nR;KZDu?ml|f`(5a&{&2B zpqO;qKTkklKK2$jMW}ywtwlGN3GxCI(roFFP3<)Jd*Qv}-;2BSfbM)ES%86q8dMw?vUDwqMYM>G%IWv`N@9rI`dc7#Gj{Uzb42OIt?}CeP?*^9S)a323-> cu%4;uhXBX2kjP0Se^bPUu_wSV$5U_s0J5P%eEMz=F3`AT=wqH+&a$SKB4qXaS_i%D-(o%yNG!s`o)X zk91gD21h%H676~e(%~${SOi;{J717%Auc79*ldGKtd`>r@Q$`yv=s>z-ObU>s%qBt zAZ7Jkp4zKS<-RlgAOQaL{TNNyc0LIRzipwVK2#XFRf*^9}G`HepvOhw){p(=!#^-yA3@Yd{K<&~{qCa8~s3rN5GfZa!MM znfAdByczWZkj&xK*&q*J`-aZc_=^`A=D#5Py+CdObiaTTq=x{N+0}Ejr%qEY_1-Wm zHDP41xO+vEMGZ=qAi}dY?%Z24{mE3`@lmMUr;chuoM1)Ldjp4~^Dgpd@aPjKp%UFE zK*T>*0`4$OYL8Y%gBd~_5_r9n$f}tUN3{GG(L6Eo$N5^S#R#$|XI{Sz%Gr=K+KX!n zc2vKL_jLjs)fKlOY&WH{7+v43?>t=!8di@%1GfjnI7@bq+>sw87ov71a;Qo_J{>sL^{)k){@fBhnZ7RIrv457L0h18k=3RtZ0Lx_nb^%fSpRX20r^LL1ogRg z#hglrU+UcR5%ILPEdzqb`tqtYWl^}~Kji7E?;&*?1|kJhgTdnv>$%mIoivq%zE(Wy zhacqcEbzs~JLO+V@bL_;CPT`mgk7QsW%l^IQMDLUfAtzu5i4%qw9O6Xv1H=o`Y=5~KgL(cIF`IBp)>f|$~~F^`5xGr*(Od8Xq)*Q&i&MSYQm;sQwV`lykK=3?|8G9Ukn(b zqYSvQ)Hct|qtMFPbi6P%xO}msUb`#LA3Pa%lXN6?up_p(0RH^zK$LLSn8Z(i_hjvQ zFeJ5H!-A>08*OMf7;azDq|#GihYfV(kZnXRqcMA|8s-ta8>bZ!33Q}Lk`Gvdoy-~n zWx2iV;DHl^)e&$AMIg-cB ziUU{xsj+o{o?aoq2p$m_akd z$h-K8oiS7)QUZi~KrmVXSL$VprP=+9;rp3~y6F)(#8@-dn?)VL*v!v8)6_+UA=U)BGWmO{2INtT@v(6cMvsj zw*{s|k#(}|^(rzTnK!)DSzR4p;OUz>h;@Sve7jR_Rd|5{2rl~xy0_pg5a=ock=wwR zhj4bAHi*QA!Vj`jciH+KH>S1(-Qox zJ-w>MqXc|wxPu?lP(HtW$9vOopparKy77&yRh=1`s#%Lg_>TG9( z&~paG39~fA@3RZ)UO9D8+p(b2q&w%-tE0X0_E}hj@`n`mxF5r>Jc+&sYVZVjxpErJ zJcO`(ESge*!TZrC%dT;0ou~Pjy33dLpEqQd#n!^cH~d>NzLev)0Z*%m3@9E2nQ+2d zIhGfIDA>Qu14ju1f;B){dEL$rDne7ups1fp>f#aXZViBD^Jh_zH{T38u6%bI8Vv@R zSIqt(eH#G+oQk`9yL?BXG@6jzPf|Y4KHT!!a-T9JoFvlm=#!xHx11D15AZVTT+}Pv SOT){6x=sa6x ziDv-LJda-J@i#;hzW8~^&ycs+x+Z)S_h@HPs{&NWIsvF}Yf|wew7S9Za;U>aQ$~y5id98~wmJMzcHjDBq)*uuBf~SLu z)Gp7IuZx!uI9P=5&_Dqy#yuEZD#{4@MMZH0ciM^|XF*;3r z9;A{NP`bRGi#ZJEeNWg`G5RMK9xvPpo%yTs_LyO5m9BZ6M8d=6DrzKBk#26MucKg% z@Jmlisk&6TglV1uE@+Mi6dU?WTlj^OWWD+VusOAR0QO@7&Is!osAf6}pPg{NW71Lz z;rRW1v6D6$EyDKb`U3Y#iU6JgDBoZ>M~GrxedkT`+jJu#XwKj42Uj3@#eB16DhPMe z?b(<`( zs`~_!4&_XB-I(Go!Ti!(B zzX28-dwQ8RJL`7&KAUMlIpDVwTj)zHMR}xDz-D@3~Cc z7|ZkGAVEdXA(ao{SKLu?NRz`qi;$@P>N24l+deOf>s))>An)p$#2X!_5;0xOako)X zjIOe+FA+F+H}$q69Y(8V+=}dvt+qbRSF%REpd_z<{gNTS|1wDRih&;=->ZRCM90dv%N9SSzr(f;PM-%fXrH3Pfe2f(DQ2NV&X2&-DORYw}T--4;V);q5 z_0DY^&XL%`xt(>cWVu58mGS#f*E!W%9W)`^Ht({sb?}UPP-X{41Jd+OelWn1DB=!? z?MC>k2#5dG-RGm{KTf5t%~`eqSg6fR1{uXI1M|uCGp8g4^^df%E{456Kx`7P(>#EL zmhGFzWA7={)deTE9PtU4F*C@=vOzZKQHFT0AgxVwj1%kfZW*%UC{wbm;GRO6K_yc< zw+EhfXW12v2HeMBD0!Hq=)pPKt|b&p`+N5s*oO##&@~ZAhA7o3S52Pz{bcveBBC5r zCvhxa8Nh;#y@aO<5eJyW?$Hl}R1WvJQx&b&?*|4N4T}!E?@yxeE3b2sV1`a@^R+7J zp27xGD+@x*xmx7d&{M%Yfd#$BB*qzx1LzqTkQa0)>ycf>%P|WAHa3;I)bi6;o@^&8 zlF!9&kvZXgXme;5J9SggsLI9x!bbkq`v&rTj4TS4=%It-3h6O>)z;jL+26!mg@0s7 z{4(NDv3IR6vYgZOFlTl!c!b?aLBP6L#|@u(xpJ83IMLF?7);-IWQly}m|w*of%ad$ zs4&NHOL!Vvik=DMMaQ-=WnunMXR#%% zIGbDlMeFoEBXYga#NI7n8!Wj(pa!rZe>u0q3$8d;_hEE_F!k`bcamCdsV5*a6aolrOAz(+B zd@P)%Sve@b1T5Gzk>0D?uqoxzL9g;Hq#>W|iLtr^A-5BQTpT9ebo8522y|Yi&}N}+ z!nlr^T4DNF?&R$Cvv9rm^Me9(!ik`_w@ulxS$XEX<#KKx{wypTfIc~YJvA0GHEjBa z{tEX`KyB>SnrEk(@oucu?SqXbBaEcn5w^tTc2WGK(%4D)aa56stfLL0kuMEM7Rqi; zo`xQal&o-#zU;IGUcZdFg?D_jdE*(s*c%p)6)*r98xXMy&ILWlQaYSn60FD<1I}dkS@TL!3$rhUX&agD&Hyk%~IJImH>Onl3MQU|Ay%bPf&#ECZX4~J>IhG$r&FUr~Xpd&X)r{G|={i?b_HnA@GVU z)1>)kALc5Yj)mf+#4Bd2-y+^^2?%cJf4F uD!N*}&DvQ+z%4JRZt z**wNaQ)XNFP~Hyc-5LfbIC&@l{_)m6_A6qduzFFQLe7hG z+`Dg7`MP=8PMs#W z>)Zvn?0Q`J3a~$cdz%M6qCYPRal2tH)ADr7pFDSr0hhEecfKJAnD?cGv8&)tngt<% zBEa;{SzSMNkCB(s_+f1EmeKCik^88@dGSs1>7b6hrH2O!%;F^AILl`PB#npS3=mrv ztu!z!I`*4f;;#|N@y$x9puJq@e%MLELvFmQ$7sQp+jYi8zXn z7!yB{(N+rF_;=ECz2YtbVQ{P8&hmx-SM^Px`+Fx?{VVD?*kb%yULK5fou&W;zx@57 ztjN5fd9K(%4A*ppn$C~0uwAicQHGUJqaXtTq-SEO3i*S5UtzkmT-;2S-*yKx^S4)p zu(xtY`uuGFdnEcF#&D-^5r5^8ea?Ikb(enTfiNd*6A`*r_w3nuP4|nOjv4Ar7>Bn* oum=sD9}=@(JYWHzN=bN_eTt*WCui4r!^|MQ?*(KtUpN2&0LE>RZvX%Q literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/avatar/avatar7.webp b/admin/src/assets/images/avatar/avatar7.webp new file mode 100644 index 0000000000000000000000000000000000000000..e5ef6fea28c4dea1f9e10e78c8183bbb544b75d6 GIT binary patch literal 2712 zcmV;J3TO3FNk&GH3IG6CMM6+kP&goj3IG6bGyt6eDxd(M06w);p-ZPFBO<9-OL*WG zi9i_aoN0I-4^TUVIodrc|E|84pR(K^4Ye}2him)-9_C)Uf2WJ@`4U!&jCB@!jQSWf zkWi1W!e|7=uqM|}&jWCK0bJPHdljLAMtSOBhK2FL9vIJwZ*jR0^qz{%o|;eu@jIID z>1Dho=M;OiIBQ6k90N7)U-LUB4fNbnB zcVd!#l@ge#a^5^>ovyb^=oxMdg}d1U^l`2Fm~d{`GrP+hqwRdlsSp zZ-P@AID|3okzavUmSMCE8gScpMng0Q`5{KtM0ZUi0ncfw=fKTHEdntzAfZJM&W&Uv z67K-k4KmgBg(%$Njk|Jg-*xprnCV_zKfhnobWW_!Q|jolDX+OMrq@H(J)qOVrR-qv zRIBNovB7=pht`u{?#HO!58IuZqG!54`tQ%x7=FyAh}uj_ZRsSbgQ2Z1#W=b?n({iD zm$Y-O(G)Itc^B80u=Sua!8J2(sb2BUsv8#G)g?fpX(OpesA<(_`VsuMZ6dfL41iYJ z?*M;4b?t@DbYfq${(Al({l4n!l089uXnn-u$MA2MX|98@2|Cx#>)`8d($l@QFME%7 z_6}QMOoeB?j@rwb*TH=HoDV(FiLvgaLY2@MYNJ=k3TkS{3csOEw_&C-HxHy;ajX(Z z@#lxXph1y}+iOME!-6i8f&P2>gA0==<@-?sKQ{II&j$D`7MMhkLpi2;N%;HPy+G{o z000uB!pDPiCpwuSQQiLY##czfeQr3R&d!)&9!I35n8R!_q{}a)ZBY|b^-u<_=bdjLM zsH;7oYDXr^TFEo<|H}DS%GT1C0L2i5=EqD1g9i2J^W;u>#;j@z=K^hzFU= zYY2QMrIAKg`G1?4OlL+SauH1y`A{Uz#84Uv44XbR0-O9-ol{Wgj|tx$nKLP-P2du% z-Q0jz6G7SVI+1s!A4*xZp5N|eZYSJHTa=>)Ajvyi?EeB#w59bKeTsO@(I;I(WI77R zTk~JMwUgwE@w2~AD^;ya94Kp&WMaW0WwLS@H`8wZvU0zjb@itK&I(FIx@VOp7hB0H zotLS1e03mJ6E`f%jAQB%^&LSPkvnigLijZK-J)3ej9wE3&g(7ejXm+7C@$Tjez_I- z&tVa7hX+KoAg}8LEcb@&UGwPMMlWWtOM7&0LePzZIRAMRmZmAs1`e%T@SSGa=*OGY zhQzw}Ca-M_vl&mQDqK~wClFH;H49m0Y^Ym{L4&A ztXLwKYl_@y2ljr2kkQYC=V5w0{+F69RiFdTm)%T&!mfzP-kw#`aCmmRG$H1>hu-6$F77UAgTvGa;v!U9uan0`(ci0>}JF8n)U zn-=c1mA3i;uD8W=(^j(nz4X`EN%~rB&EFjc;cV(?)IMy<#JIAZJKCn2H!jd#+FP3d zh|Te*k9)A5x_gw)Y-n@x$ zX==z!&?XI>ePW83uOwKEkDLxe*zeJA4Pq9#;q%y~fS0-v=()A!rQFSY%-}3u_+s~H zW9zimEqBTB=n_B`hS84Ispf(XDkch~Aqqj;#aFJNd-kp*#*uNX>22y$d5{l&6GXjo zj>~j?H5qFF3f<<()y8WFAO-OjT6Bgy#(Rr=E4LU_h_>6k79MC*;=Qq*6sN;;TC+an#IgsR6KrAO z$c@_{eBZ9#Y{_X^v3H=kdKtigpF(Gy(*r=wmktt#QMs28C30ForRlH#j}jMaWuk@E zA_pE51~<1C0n?zwLyZVm9FlRO5^l`sqbf$ri^uN7MVKB^fThgOshG+$Pouyye&KMJ zbG2%FWThe&(<80fo5owGc^_cOzvBH@`F?*~*BqZnd>}Yoo_nqw(`pk)V z*Z@dqLMOC}XJjR@tX1aZzBlMg5_@O^19uBchc*Buw}AJhXJgG{jpTle SlnZDgHxy*uU+B%U0002~4@gr0 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/avatar/avatar8.webp b/admin/src/assets/images/avatar/avatar8.webp new file mode 100644 index 0000000000000000000000000000000000000000..b66e48f7b056cc570d368c4e0f5d962216d29971 GIT binary patch literal 3946 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 E044g>D*ylh literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/avatar/avatar9.webp b/admin/src/assets/images/avatar/avatar9.webp new file mode 100644 index 0000000000000000000000000000000000000000..7974139777dcf5db9848b8f0b97b58708d60d369 GIT binary patch literal 1680 zcmV;B25Cq`s!Rw6r*sJG2T- zxVvRExAV}fjI%u($cwje4h=Qh8M^Vhm$-}w&Q^=5w2y(KN)o9n&3k|tlSOs7*31#L zZd*A}kBRj5Ao@<*ftTKbJz|QgG6yI1)!FB5ojbeD5SIT??x5_85>vF3(iY~_s}B6z zJIL03h+X)vz5D?ncg&2BI7M|1dFC`>e0JBim5qthgnd;jgMITvIsJRf`(5IKpu&FX z-rCs3S~Eqe$321H8YQh+4o=)+a^L}065GaGUCEK0%hpy+*Qx>$m=Rv}gtBWa=77lX z(VYa&S4AY~L9+k={`NoGgSxyFZfl)Pi^vA#KNDL6dX8Q>us5c5t;6L0o&x6W@m%lA zDM)$!t-!khhGc^`eu!>cuJy#x%f0VI5!yaa=td}Xunnnhr8ev;uMHIhcr0J$XyqK9 zuX57K{X^gJE0uk0OB+EWP-c7ns_RMON}bt%QVj{Q$m;#|WErNiIZFFI{kvAmC0JDA z6S~aC#;gaT!rVstFalV1u>i%0BG3(ck`%1_k7)7_k4dLbflBWF-$=y zYXuFm?Pkkw>>-p*ze|ub5=lw7qz?%ivp7%e~o)zuOB^QE*8~1b{~M=$X$;g4OB&<`_pQ(fAcH!qE9%DO$8BLT*BZq0o#qHcMiMyX~<| z-=+m)v%h3{wy>hD`#}La=L=Ej{j!TN*UL@gmGY6yNO_SKe0~V$KMCY{uAJ4ke=00S z#(Ss89O&bj<#0={Y6sz^ccLBU{iacl0AlStaKAP5^H&%N#arv<9wubKsR@^3#Bh3j zH^cK&-QW8*&El;j|62<;pvqeA-&I-=$SY3PHw%JH{W2HBMLgw~VZOlbad0jQB0lI? z&A2OQ_kK9>21w6V#o%fNugN!cbI}(?Mnr>8{3YLGv|JBl>tsulVv@2-!G>Bp(?L%z z_4KMfeT23VcUa&**Qq9gpKCv+thWy@(5FZ;SP-pYt;n#N8hgyGcT~yp0o7wWy%$XW zaP2yYd?+xSzH1i5u=5^{bo z<@blJ^<1?6)cNf7eacQ)dqC94Z}gOf7S(g?b+Z=2PdS8(tvK#JzU@=G-<7lJE^qf| zB`U!>-htj`Lio0dQ+Ic}ohghs09r;1A1!tJ0XI!YVxlj%Az=?yczeG5GPnkT{O zS^iGH_&v0;c>ZPbqB~?md!#IKb(FKUmVRUw=bZ%(nx>OTGVw(Tc|O;5oLA5MX`wQgQMaiXBB zpcHtwlRdTvXZ+CKlb4)N9T#5@t0>l_Y-OHSL)^qXurPW*__rJjBN4{NpNd!kblcd- a+aN;YfSUTyaGIA8bqP%u?tI%K&_Do!2TN1{ literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/ceremony/hb.png b/admin/src/assets/images/ceremony/hb.png new file mode 100644 index 0000000000000000000000000000000000000000..41033245879f1051f34c33766bb6fdb4fffb38a3 GIT binary patch literal 2275 zcmcImS5#B!8a)ttO(dY8U=WaA1Y@Yu5|SVSLls*nQRyN9At)NUNHK_l2oV%TMHq%6 zW~HUV&dd2T-CeP=QYuma z0LbETXb%7o0!tx40s`K`s*ZSpjlH{z7Y1zkPUy`(6yFK+V`I>pEOS2QpB60u2atRa z*&=|CHo&?cCD<6u+7|PZwe=5v|Ia}Tf6!8(^HbId%`rgn(T)P-2M!tue5VWrG=c1Y zkT+Nd_-qS47W*&AGXD__%+_x6kNBj)76=aZn@?`(2$HZY1yh1oaM~J2Z-T)_>suDMOUNnq>hN9D^-_6J|>Uy(NO0(zje=p;q63A<>}zMlWb#0TvU62S8ac z07zD!Aq9aSF*wG<)f1pZWe1pPJ%2*G6%G6F_GRUjI6r$kBgt=^G5T*;Bzg0ljC=>^ zTA$bQySC>KX({n%kGSrXof++Ks6K%aUFVLzA`*S^mMFw_IJ=zSVXphv=Z`g|v}=@v zkW=o5F-T**U1gO1`|gn_LJ|OoE#T4iUh%`+ku$z3_HZ#yp`3kNGF)E^iVL*YYTQv? z8+LCYGgG%>Y?9BXh&0l}l}5J}sGqsxre+&;o>0IpvWi1UB>&w09{yeJikG(;SxTLS z#Wn6tiR!tOM!#_~RsUW`wJIH9zsm|cIN*~Rvmf1v}8ir>tu{H8@G7<-_?S9f4WqpX>ZAj-RS!{3iHgYJB zUwD;AvY_vo`WkkWk(^XsHys=n)m2qKB41Wbsh5zOEWl?xo1jVXChu6YO<~L&VFjYw zw#cTr@2bd+kbW!cXN?C?T-A;JvN^iHmR^4TNyPNjBe~X$BU#ALLS7$a{CvEOYDRRV zG6%F*{<3bL?KU@yIzv>b<7oQjr)z$E;WZSrh^J8kV~U^F+JusB<=9WDqlDcRbn?lo zDw^VpRVgbFTqxPpSSGWpx|7=Vg`GJH({vk>Wl(p0=&mBKY>r9U9Y&BHDmvuUl><-- zke+~Sm&f6DQ|Cw)7o~azQXhx8IGA?04Egj`Qze(paIRz3;vJsB&Y1brzu-=qc%C*W zd2@Bwt3g;?GotQhY6qXmmG&yXwFUU)Yf$?(kc(W`pdCN5-v1v zToE&Uf$CjP!!rM9eto^UnAX)6u2_d=JoFBbCuX!>Up9};7BjBz&PorOhBeMurH0=f zJhhx@)-9aJV>Kk5tAI5H(RWDg%UMq1Vdil_oj?8Cn($3MxV$#rSQXuVp<QP8WMB>}Vqe`-kf-H`C_x$6V+`uqTu6i7O9 zesm99cek;XW0C5kb#mWzqqrL(Id-*-nH3{LNVOzgqb`tZiz|gS zah?EmIB}!yIU4$WTNq*NILvI~9UMQ^rq$Ss{Djx zCuivMP<2n!v=%s=P&EFXkiv{8Pww@7q^4wA($G!y(@*=7XW2erCPmTxN}3%W@7J@n zaUixi-xr&6qZq&N&WHO&%gqHbB?%`2iTK}Fk8vLLzZD8MFBRvK-fQCu49rUh$Mx+uW!0jUCcU-x}#WRz?c^S|49F*g^)J36r3gg%c2}Kte-E~7qe#2lqfT1 zofWC(7}s;Jxq}l?&*3doa{VHgsRu_M2W_J`=m3<@h2QhDWJ^j!7Kdz;Bt^7g4C!;^ rtK9?{oKY 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

tpi>TmU7os)QHMfyuNG0tG@L13>L&8; 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%0A?nmj(tQ|ERF82X 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 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/common/logo.png b/admin/src/assets/images/common/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..11cba749bc5eeaa66b2f2877e725994ebefa7888 GIT binary patch literal 20288 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 X6(T1O#cdT?00000NkvXXu0mjf7PF-X literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/cover/img1.webp b/admin/src/assets/images/cover/img1.webp new file mode 100644 index 0000000000000000000000000000000000000000..ebcd0c58e6c5463c104fb48fedaac8f7d0f40a14 GIT binary patch literal 7522 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 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/cover/img10.webp b/admin/src/assets/images/cover/img10.webp new file mode 100644 index 0000000000000000000000000000000000000000..c6f53e3d1faa7ec38ffc41a8daccec08d9917cfc GIT binary patch literal 3526 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 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/cover/img2.webp b/admin/src/assets/images/cover/img2.webp new file mode 100644 index 0000000000000000000000000000000000000000..747580f22be83d196022a35d811b5f5b04b74bce GIT binary patch literal 14024 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 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/cover/img3.webp b/admin/src/assets/images/cover/img3.webp new file mode 100644 index 0000000000000000000000000000000000000000..4fbe6387cf7ed3b55f3a7fda24eb85b16b6e055c GIT binary patch literal 12128 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 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/cover/img4.webp b/admin/src/assets/images/cover/img4.webp new file mode 100644 index 0000000000000000000000000000000000000000..2dd11391ce6c40450aa6b41a51b0aa44f3248d9b GIT binary patch literal 9832 zcmZ8_bySp5_w^7%Bi$e)4bt5(l%#-k4&5nTk}6%&<3tMPj{_cTu>XT*bQTCP+v%po#UV#s_xW7Xxof$^N@2>e%i--+5zh0BR!uv#1vKo51!v zi{-w~3tmd;Fz8`bcqc<(lWn0d;Ans&sr*Uv5^Mv`J*PezLFZcP5;io|gB>iex{Gq8 zAHQ)MAJ|2q2fQghma_{!8df2aD?POVf3Wz^39~N3_fU%h`%)IaUIDlgXI+)OgDAQv zK=1aV>OL^}OiEDx=Z;$->ti|OCJu3Zj)%pd2leZgKa09wut3*IYG`cx!3ueFYy={g z5_CA5%dib3O#a2E*rW#3ldP^ngy9R42@9>c1(aS(ucfOsQdqI_!KB1P3hk zBuo4zzz4o$?q$H((zvOC-CX3{1+4{d>w;yxPi`!rE~1xZzx7n^&V_a@x;&)ki9loz zbZx+yma!LA+%}O-9f;FQ&mZ3N`V6Qmb8DSZw&a0b>}(<@`!g=wB2gK1rB80}k!H`??j&%T!{xo^cGP78V!+kwbjUHB8Nfo|HZK zWJN?T6XPIZ*I?>rR8w+TFra@DtOc?bL0L&=WF=av@gdXL+>K!WHHV4%0T}E>#SO5r z=Orich{C^;&;)x-%!CERYVEoGMQghuEC!sSNa{%MOF+8ivG1|m3-Dpj=o%xz)LWP$ zyB`tYWqRg-C9`Y~NNSJ2%kLh|EHJVuW!95=D~ zDRVBIZNE)?a(6MKFaf??*Y7xZn|Z48GkLB@Sl@P6n6;|-+?s!~&_CS0O-5|ym@XfB z$WV}^0FC~>Y^>BRhiwnCD#OgmHj`{PT8nm(@Jycu79+ji8znTfE=_b6*`q3lrsR*5 zfH)M*G=31~p&6*cun#MIP2P*7u5A_-LxyaPYQ)({^^^0;R2q$6ujz1L5d$>cNxG~t zM439ms)d`WNs2I!sK4reE1y$f(&C7>oDTAi6H_jfaaKbjzQv05Gd3CW-wONrR5L6} zTrOxv_Fa;Yw}$Daw3kiW2JJ?lW1{7X`M(U~+uvl#I#HJwyiG@pyAy#bw~4*Z*;B%Q zI{EA6?JXpvZWBL00-Lpg-O6TlDHbV>G@`(l91F0)J4MT85U^Zv)S35G+5r z{iDHGpO;dg9IoVL2>8qnJ@}9`fi|+ZdGTut?ur{oU^1d~*fVpykWb%*6`nDNXYsM` z%irBp;(nUC<`=gdNLfTeL~t zI$>h9Igo6AMa$=pl3*QA)oQLrQY3upV_Uk-mFJa|5(CAs`gzF0y!jkH0&HfH39cK{Bi zk7*+O5KWidFSEH-F7eAnyl%vYpQZld8_K($y$fX}MtUyOfJP-q#JlIHCJ-HWKWxQ2 z2N573mUSg8LgkXe52>4E#r`1z6gWAZ~O2$=Q*Iyao`HsJgw3(z%~bs`nRnjIZ(x(HI65#q$?nV;+IF2jq$(I;{#Zu zTDgzF=rh@4_46SsYT7a022Ik{Gr^IG&_Qb$hK!F*el+B0Ovti)G=(Cq5u{aJ`eIla zJ3rOj@bS#KzwMGU(v%oRe%Pevz7I1uiGr6c%C=`nn>eJLrw-Kx60w}8Ew0zF*aXT=B^3tmbiG;fiU zz`lnWkS+?^wTK?&$arx5j)`sx)naf5Ob1K=807#|r*9)9tMt)^n$*U(aBX-4mpgh4 zwc^~`6}WI`R#}SY2 ziSyUlD(vvID(J^EG{lISo@NW@?sixlRyP<5&@MPK56O*A2jI=Cdxa?tU@`0QAZbup z04u4XPj^zcm>wE21m%2)8b&j0@K!hAg6qZqL0leldFhb(19kBaL2vtf%=(*U*%c)$!0(ild_0O+n0SPZe*PXS=4IZiC%i@*=)#K+_M5aIP|QBaH~;HM|oP6TEQHI;X9

jl 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�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 JNvI~k{{d==#Tftq literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/cover/img6.webp b/admin/src/assets/images/cover/img6.webp new file mode 100644 index 0000000000000000000000000000000000000000..bf3537af0667785dd9f51851bc83f272d67bf8bf GIT binary patch literal 10898 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 YpsJMVr313*o++Z0S;D(V008iR091e}^8f$< literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/cover/img7.webp b/admin/src/assets/images/cover/img7.webp new file mode 100644 index 0000000000000000000000000000000000000000..bebbade4da1a71cd9f9c45d14e4e62663cf8a0cf GIT binary patch literal 8694 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 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/cover/img8.webp b/admin/src/assets/images/cover/img8.webp new file mode 100644 index 0000000000000000000000000000000000000000..0b38bfca7fbbf479671e78a0b95143532c5b9d51 GIT binary patch literal 9580 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 PKda^^UIT^0gaf&~cSE&1}l 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 IaRA`|0QqZqxBvhE literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/draw/draw1.png b/admin/src/assets/images/draw/draw1.png new file mode 100644 index 0000000000000000000000000000000000000000..da5d87a3528ce62da611614d3cf3ed57c8b0288f GIT binary patch literal 11315 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 Q{0ji6DrrHR6s@EF2ReyiTmS$7 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/lock/bg_dark.webp b/admin/src/assets/images/lock/bg_dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..1c334354f0145e7ae1d91c8d51022fb8067d0b83 GIT binary patch literal 70592 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

TD-eWE$*|X9h61tDog6 z6HR*!77v>j)w_xORd7xbIqZrJwqH&&Q)rr~BO;YehR^*mkXgvgiEj;ez%1$mmD>uY z(ju$$32N|Uy7g;*gef!aikKv8@JT}xg!G30c2n9>*GgR}>2hAX-j0X)XUIspuBc&= zwzgo7h`6!}yzuXvyF30b1}5wo84!B0cnK`bEq?}b+i%vwIuk)WUTSPx4H{k~dVAq( z`>gjRL@00;T$8u3!i*JhhItniU-TN4k_ckr>#^wJiN$Dx2}|m%L-L0oKB}vTEEEFCbuR+I|vKk#K-Ts^qs4`|**yZ;x@~b)4xl;d{DB3iHo2LM~ z4Z3L=i%{Q+t6DpwL5~%5o3{t72&~`zdoWRb)sUUXNnlxg_h%i-8I;Nn+YzFR?wMyj z%@~P9&I_fRDr^{xEJGd{fyeJn(4%dt6RwxsdjTj&LlbHtJc0`Buul*@}mcj2qH7`FjlEvMncX4*!3hb_%VB(Tc=7hN5dSy*ASy}|lT=VE#0SRe# zVQXKhB)71!=*5eSVd+pqAgJ@&L$LR_13L=2n}R`mt4wWiW%80kXdZ7b1iXammCB@# z5{#;PeNi1Tnr!OAcP=Kk3aFo`5Z+l_Z2Xiciq=&m<=Pmwc})5;&6pp&%P(H? zL^CdkFBs{dVuh$*JcDqm1$i3NWC_D50J6z@h*Dt56sLK-m(G3lm{AZOvj)fRxR$oc zXElH?Zgd$hO<3DPdrmtOBuEmgd-GR}NBz!is9(unEw=Z!=#|}*Ac}7+Xji~c=IKeE zNGx+sYypyQMz6J!U=+1Md}I3Q{pTm?U4RS?bHnuSW1G~Fx=fuQq>qjcS|2Re>y$K# zNS{qhVb@@QKo)s$OJ}Obz=2T{mSn}KK4r`8P{d_gVPcYp!1q&3F9oK}&rNGnuz9l< zxo&CR1884;kF^>nF8UL3My<-~v)FjCf;%)+4nMbs%!8%V&jRL<$4?jn5{1Mk&gIr1 z`mPAf<2qQcXi9(SGd$*U1Ksy?6gbM}H$ zfQsVW+wWFq8QlY2-6rGA%lQ1=L4}AjBCUuUunNegare93R4!?#7TO# z#wd_{HVf724q|Jlj3mQ^4KTHD&SMuVup~|IQcc}+`|P9BvTRd@(q&35Y49L537(>y z{dNGu|Jn*HQ8tbXS&P8oCX-@uCrsW(B{aQ;=?&lj0000LMGo@EBwe!oR-F2pyD>!G zcHmdk<`*J@DSJ5Sez__u@C&Vc<|K$*&3mj}&iZ16rh4l~g;Ic&0gRW5Ig$G=+A2e1 zO_eE>oJ%AAE8+=!|8Ydh981jP5l$dq_}-)3PJcdUHvK2F)(ZlFP3SQG4vheB1=E0c z(b7yw195b??fraAE{AMI68xZ6sxX#ME3Hik8ROLR-*s0KO7>|!5NE@NYNR#>biqY; zJtULXch(ZDT&-AIiuHR_xo_b|lHJIBP#X7 zV(;d&7Ebxrzn~dPw$Y}izxP?P!d+(xwrTf1Bd%;<8MDaeAq`8nhj~E@H9JweBTYN% zcbwgKyI1CaFJ3I!aH@|Snr#V?#Tnx~g1={Z#{ZEoQU|qMBIeZM6CfuETse`sjpf2I zjD%A=s~{l^{A1@3F>%VHve5!nVn23pL}V_Tf0z704ce>W+U zVKkm3N9kY|V(vtK8G-n!lXUNe4*{639n{{D$cDdRrtt+p zbMyg5c+%uWov@Cf+ZF~`KmF(i%hB8Zp%%;1?t(>%-&HsMaQ?|0Ov^SR7C$?lz2g~C zG+M=ADs+smAKRSL5LKGnSyrCx__5vB4?e<=IbH}GkZ7LBo28E8DV!Hz}|hm zZouKjW0xA-b>VJpi}srB(Z}NFse-vSvCfNjjQI`rLjfxK@^Hrx3f@Y(ySWk+n>v$c z{3LzGjpa;pOufe`QU}T5x%2(WJydN#v@PE?8N;Gt_UVqJ5v@UD8141erE+yeF}<(! zO9{(ga`cvpKF*HG>or!9Y3b0qW}CrwdjNOOsdD0=yAf4SBz0~gb%gy9kOY^(;(X+* z%)$Vu$WjhHr^3?z%DbO|#>DN1`{kT|w`o+fy9L!;Nne+^MuXKrR7y*#JQ024710`B z{XHq=eXKIpFb#RrywoA;mn;mYk9>pI31qXWDc7C#5;<42tGRE9ve_oWtDAwcChTuud9mffX*a z0A*si&N^Ut#p3uLB&A7yWtEO0%mAlHF126fNqWr_MRp-!nYF{dC ziFcF%wd;#D2@v{IzQ&eEpmL3w6}8e;4~O+SEiIc>pW?+V+9)Q~%m;>32!&rle%#ms z|In@=9%0x53P9$s*k>(#DBlY{mgA;;h`0TX()qCGy~TTW-JUfx6s~h((|SdH7HZcC zs=>F>@~|zf)fGG4i$V#fa7SK`LW9w)oIlYf{A|qrp<2ptGA)GC!vh7Dy+$eWbF4#? z{?w@Ot7HU~muy~OZQY;dO5_pdjc~w*P(Hrjjxy!klZcdadG3s0I8=)}xgGYB|l%w{^X=V7Vq`t3=z>XQVwR@RKtHB1$AQvb|XSMbm3D}@M8EXr9G&aK{ z70ib$apB3Q?7CACoCpt{#^RYtQxdq=#pVadQUzT@OF+B%p09zJihHTq6>TczYShG_ z*O5A&Nk@a1qB2s;5N77-!Ok-$84KeOg0xxk`^QIe z5s--sf4)c1W#y!5jmq#Cp_Dh2sDandt1$} zr}pt8fOZIHvp`hfb&Or44r-vYi#YLJ=rY~Qs={cV82);gERM@y-6HF#00G@_U@ovV zl_>2el0RDx$V%<_DNumza5rIg8h>P{RZLK`tlu=PGO^3`oH{f$pYIHTk(hwIya_f! z+DqgkJYvBkJs3FOELRw*y$?#xaLs;uahsj{*$8MA%Bo|nHO!mVIQv$8cwCaC`R~C% zx9gE^JQ%@;EtP?p?ZqNzJ&WYso&TqqK8`nlN3!%fX95u5{T@2CvIkrC5+E1x_=D&x zYf2Oxl2!IllS$(Auu!XC?X1?Zui8_F8}nzoYC@PW%n&w*_t1n_1`&ka@2o}IBlJJKg<%OhE5F1 zI6DxYMc$FJLy+v|5m17B6Kj+KwnqEj#Wx{SE6GOa;60$d9$Wa{^T&Uif$Qb$J;aii zmsL!AjAbtB53JV!RRcxHjDh&Ec?@Xq2i6E*4>I{=a^`x>BMhgI25RM`;0#9nTGslF zkTLL{6Y1S%<&vliuB8zyI_#o!=?hUq)Fn~%_n;oUBR?VwT8*LZq zeg5l@b)nq%ZqO(#p>gLtYm&iznOS7jkUoM0`}?TNke(VzL;bU{OLJv~c6ElJQ+oM* zkuWlI=aR|V8#`t;hsBnNq-75))UPBL?XSi4ON9JaCQ&(!wuKrVqhv4}yYwY_D{7)d z#kXS+8Q7eUbT54nr9aM@U?kCV|4cA{Rjv!=a2-&45mn6GMY^gKo=NFY1QmlFYbeSj+VFPUjMUZpQR(_i>_+)K4@<)?&Mf-b(&T_xQoYA1I;^f5X922e zvo%2Fhmk-p?6mYpFaQ7m02(e}sU<^yBJ27p(s*>_i?l%x6~V}R^|9uzhF~f^6&s8w zRDhfaZ1~Si3Rjgi+px?E%OTo>gS#IDNk_sqU=)qTdZ_Qx7^=;m&0m>>=U~kH5alI7 zqq}&pb}vBGS=XBN8(W`Of`#C9}CY2(S(7 z*$h39C(h8*n6)6-L0{H^swMXo?Nn#q)J&&cpJ>XoWBz0p*nI-b z{GH(dE)ol^qz#<>1TZ&EW<`p ziQ16$aveSGiB?*mi3gvnc2ML)JK73bAnu`<3(a%jsM4O9DQ~zY@oI_}*tyJr?9>^M zsFlE0zR}zkb9Wm?yOLfZ=ueSV@u)i9fR1L$Mo-XD@;|t1ClXdEP_PIXLNoyfeuPK@ z`~t_HwffTIfQ7@j8=YNLk3xz^l~kSioMJa@bzHQ4CW9HTcKJ{;eB-+F+cvZNaUJfb zyDg;e^?ju3AazK%?P_nRbFPj*BObI$!MePljmP~Ol^*DN8dnCDZK=~gnT~e?zyps> zz@Ff7rSthl-%oJO&L=}NVWm0)47`WXJbDp(4R9ql^2+4E^}TAGmfx{4f{JvWT3l zFKl$(OzCcK;Rj(&N8iq$W|F3f?S1g~Y%E63#-CO5{!qK#zcTHiyfBL`cD(WBA;7`e zG@R^5qe_U4A@~J54oa*FWsciy*HK?fR+f-wFoLN57Q$sGI#JPtZes@9(TN)V<-Z&Q zCnJXj>?mD?mz0>>;EQ44mxOm;DihSm`Sq+!{mE{Y`~6pyh>Z|uPYuziLMq^+l*#7w zx5o|XoC_pZtKr|=WuSP&J<17JU^7o%IdV4XE%a{>+9qog#ElfJTW2U*gti{qVd7Pw z6E)CotvJ16(rgROK)>h&Ae*=d8Hu?Xq*0}8Eim4LraBOZy`v#)3SEZ|KQ~$IkPHdq6vbJFzNxW> z3`PN_UxbABe`9tts5H9@LJP^C*3Gf2JO8)r*~NCurMc#*_?S;G;h)rk_KU@RvN6qL zrFxmC@ygOsT#LFJSHcB6V*x&n4pr8&wMSQ$Z^(5T&-e_~iMuC?)A3{m0~=W2dfpC0 zfiK)NsHQ}XR3vjE;O?SGOizU>k5~Yy=55~R;j@1#PG_%Jlp;2{%&QSX-WWkm{_UB! z;1t$bGe$4AQ?X!7B&JKMocWya$M&TD{P7wEMe|yuy1>1=YK$m4{nSCwBzg@8Zz%F| z2f+ga@ox4TIN1U(nUPhur~va$V}#UgBF zJtZhw!#CVDsNW{ccu9S@I$eE%L|Ky|owes&cVExgaZ~J&BldpepYzj73#1c$XTp() zE2@$;+|DSpQXlGInUnJ%w-#5kItD7?6RjBLz#$3%00004X~MA6lUvn42}u-AR6jvE zs1WPVA5Hrw)j3#R4gX4mpLg);^faS5bUj7mWEyIhWh%@(U2gj@=AA>6eoNs94szj~ z1W5T6Rg3ffMz{Rgz}I~C4%Z3sI>(WEQ3ays6u(4Jpj81)n(7d+mQ)AXp*Ikb8B7DU z(h5hiHQqRXY0dJh5sQi%9R6tLdGNzZq|U$1ugy@7p87>eYz+v1=|F2yw-lT(t#+=1 z)P4?&wbmUCbm?M}0y>}C1omTzCg_S16pLlW*MliTnE(SuA!tufoI+DuBMSL;t1P&As~5>^V|^hG9KS83$0xNXkyO+=A@Q{t9X@Hvbi- z@J|sB1rGXrGM!4tjp~~BjtX1@y=kX4%B!&Z8x;dJGcAd;TFqh~55d_|9%ePex@3DA z9Hi!Y5CoRVkA=fS_Tqy90JP4zFRwTv4`lHumM9}mdL6p}AHXkly7U!|wdH>vv#43N zl~w)pf(^F}x)Q)=>_>yF0Hx`m)S7DWWT+gk&7C0?a9~-Hr#`h-0AeyY>#>2oy}`2$ za8NGKGgLNGeD2D>N+4h!hRem4v8nbgw_4OWy<@Yh*st4xz_53yVu@~Jir-{?NVuv( z?aoB@R_z6o57l{{y2SPnv%T6tq!;)^ob27@%m%Tvn4YI?mu* zR~S|e93lCFEN)Uol$3UEHo=ogO3wIkUM9UXHHg zxW|%NmK`9m4oT#;8qJCwWb$(*@j|o50&;DC*b;O^qDHP@BIs^9<=GZU5OnrR>0gUZ zXB$D`S@2;HM7KsrLh<%w%bV^TI(X&e<>v|&wxiixP1Dzsa2Px^4lVk}Hx=43)pQ&f9-vuK9H(Cc zh=kQA<6E7K>Y)$jh-+%Go`e@-c1G2xG;d#S?^K%0dG5GA z%y9QC5$fniBi_+(JwE-$(y}1WC-cvQ2DBiPb3-B12T@?2h@^k2p+^a1#CCjvWN5%93wCEPX{|ouDVj!A{l`vnf>vA}y{Pcy!!Tr>N{U7$L{KZ-0FCE0&MBZmUGCan%sPi3 zoqMeqLH^k1qG>Rx%_px^9PVTeh+3!44Qt2WYqY;7r3$6Za8NGK9Ez9k6giLk&3Q@| z^BWas8C%mJ6Bd%X4pzN0j)Dx_mz!H=f~$>cG%q{s@ME!imLIn4&n&T7UD*z>`$=2T z2qi+OSbVI-1`bubBn~?IFgEMjcw>>F8M~j3i{gfbT_VuZ9KZ;yCuW*OT0vQwEwh=-an8vAsu4P4X>mt_=xMg{HK)uf zfzp}8AV9g!DE^cb>v)HT>eR5-V0Yp&CV;+V)dB^SEG321000003ajHy%Y~3Oru_vd zc2C7t)J_>;f<)-wIItQ(Y{f9nBNc%x8m(9Q9nnek^vtt*G>)PJ|B0O21=`i(J zfA-tZ4Ku=Vf19>qP`_*5hi~NalJMq%Y8dpee_oUOdv=HLwoy(Kvc}{=<9T!F_14S8 zTLa{Aooz{&sWpVSMppR#N)j45*!E6t`R5#rTHAfKqGw9`(L|wfXt8rogNS^>D?On= z%~wX!Nr5v6X|i<%gAty92}sj09u=7mVr~P6wk)`8mn6t^e-cWo@~dqBJNEEEa<8C3rkQLimELW^0lyx?H&sO9n=n4D ztJrV?)6#K|m=*Fsn;J_saGq42T5dFrtXklm8hlBS$GxQlsfX00ki9(*48uAK_S@

IqfqHGHHy}CGLxQ`MFLvOnD78=d3BZT{d-r6x*!ati6 z&zF5rE|A+IT6Wf_YP|UhP@O*+)!bdmm81rERHJ?K-cu)1&g+aMn#Y%?Vo{i+gFw3b zsn>l;T2hiw7T}`FDagP>F{5wgkOTGS1GeqJd7;3bx39w>4p?A&P}QC>6}V@vMnx{v zKGqQz3W&3P7TJF3K7Fc7X2ijR5;5BLjL*vV@{jr_HXv;KG4;Cj{GY#d`3o zxrtUori<=e1Q`=qRJgzDp^6rQkl0Fji$&vf$bD)z>Y0MDGiy%Mhvwo&IC3Rfh{H;J z3uMPq&NcTCj0??M($Dvj<-SRh`Jo&ej4MmOXm1CtyWrElesuNkiJ|zJlwz(h0pnt@ zYDOF&ocL=OCp&Fa*N5CIq2-aso%F2BZFCN5^3ec3C^{(9vXY>fV%TCHEh8u#UR!1%Lr>TvWjOSa<)vfo&P ze<5+GhCvl%nY9Ux!-d(Qs+A?tWZbZ_Bjl#mD_z#{)~vi`gpLN4I8W zLQ-#Py@`R25sTdOX&}$#sAhM-zzvU&O_q0Z4N>|I6d1~LGCPqkk?6d4EBU$CEfkCz zbzGyXv8dSd>=6rjk=ty`hc&K42>!X-2{2%mh-FhTSuA$-Thg5BsYbMxRvY(<&g?r2 zcy{U|f7|Di7I_gmjiOC}z((X=>t=A^z!*zzw+CA{&a$cf&IWJE_pH7^i-8Blj-{?x zzc^l2+H!G>y?aohsvJ)@tg#xmCP?^tDl1c5fv{nWzIDdaHqt50W~$ETnSX)xT%gI& zGbQ;({=xGwEzW;L%mWsMC(^`hkP(QYS1B%4{EPTPtZko%;4p3p9ShFwRVo~ML+D>- zYzAil00000X*r^09TkT(7)c7{7QB5oE(IncXeZ5us@ffm#nV!FWa9kz-Ytl|B=Zpfx=ngyvyi!O1&lvrkYfbti zWi`xre68})3BA?k(Q%02h;M|K3cpx?qhTR^u+u_-G&n(>>$6H4^AI0NhKJvBa4~FD z05@KO59%D=om!vY0}bL0|Ne_vgU@^>-`=(Vuj#s3dCyBUvEN`UWoRWW=6+C6fd;yqu7(q{1+xCH9U_#N7Vg64h#+u+@UQ}P%2oszzIob` zS>jFSmKpVTIqnM^@54{|t_JdL+-5-}-bpaR_t}?w;J63VO+kJh*kLU+jUf2@VCR``+?y23XUdN&AR`}?(dV>z$!=ZLm-M^uZg*&W=HNgL^N`Hb%?xy8h9?pU z(CIoKbtnitf)s0LnYR~AgO~9jBmH@H4N+BdEIUoT<_Ie$%+$kk%d`aNBC>|QX~l-~ z@lKqa-YNg7is;G?fPN!EWvaK0k`#^W-8H3Sa-F5>w^7{mVZ@40U)^HOwUbAeCB$s1ACk#TB+Ew5H0000L@5!!Z zPiuw&_hd0l?kH+auHFyMmN>EzSdtdY;K2k0vMLtsA`0`O{!pW!`|wn_!dr;B%g^)t z?*rW3XRFM25q=Zve_)W`V6ea)YK$8Z#7eH&tj^g zq%=8KiNh(RP@nPFwABihCLw`rPQLM`_D5_88jCDW@YSCfaoHLMapvv~)1WGdX zw3EqAc{u6;JX`_QvleYsWrCm>qxDY6X>DYK2afm>(?0LLs{IYBwq>Q-IF=!}v@zCy@OY!+&lqFW?1 zRwD?klNM6iIiu{&IU6?vEqpU5W3lg(q30Ssa&a5_e_5nz< zaiwt@5&#AjXLgkLV+)HNG1~kL(M3<w`@>PRsl>%o39l1}f=X`~VK$8^#?eG*r z&XHHdZAj;o!OPSXf6ph?tu4Fq+K=5o11YN}Zw7$VEZIcwcK-CAF%T&bfu6&ZXrwY)hZjJTT&r-q7R{0w^&yM$#) zJ*9SJyyXvkD!jYwkWHPZe1uKPF_mv}_)d*cgl>xNO{bGgZslz!BeMYY7)@#k7!9a1 zaiwk86~axNedaLXwcf5Rg6mn~sbDLN3E2dfO{~WM4JE&}u7l}9PZ+p)Q&SDKc`)Zy zhHf`hFvmyMl+D4p4jtA!DEB^48IoAcQz;g%f2D&T9u1T;8n7F!wqYp#DFK!`v53gN z{#Y|hlGQB^p%^SX^vDHyczhb*ZEG4YmT5QE-PM_F+|A2%ihyM}4Eb=z7#LArU0B;o zN6HdG|CjIbBSP=tbZYRit;!z;tzgv_b#Pu#VNsLcd-ozfvhHD7c@~W-Z8f!T_Ag<^AZkagc0M*D2YxL>8gEC~8Wo6@_n?wG zrC~jG6L?_k)z>X|{dgv;-p0+Z>Cmpc5$iItgg2F{dQ5UG3Dl>$KwM1)vOhTD z^aIj_8FHP<)5Pk^utyb_i4Pghjl*Ec_)a!H#i=unXXY+?Rq-%Pv9WX36E6=sYfk~_ zR1bmsuU(e=LL5K7n!`XtN0~VED?n7|%FW`$Itd07ekI+!+BYjfe4nS;VJtE1?PGFo zsW<0)3I8q-#aiViRB2RZ2zE3*voZqL@^ws@_9CE9;Ti0hKVEEtAa>QS`@e7K{(nx} z@_wleEI0W+VK}U(2l4l?vIm+tmeERJ4%yvK)Cx<+Pv9E0L|QF3AOk!Y>N&cF)l!D9?>JM``AU{=1`dq-{Q z?H)!kg9I~yGH)Bnq!pVghgH~z&)#o!X4R}g8dJ)r+m~3`@neZ{3v-|HNYi>DZ`ffe+dd72@aQWB1L0JD?q=t^`fsV%-lQWtaOut1`&>re!Twjw zx`{G9Xb_REx7Q{YDJk;^*gKxZujmZrebRbtR?HB8XBVX)YNnzcM000 z)glFl-@`4Fg`sChaMSdn;pg~EZHpSW2tsPEYs`|#bkgl$&;D&3L`Z)U*wFOCQKbo2 z_^tUceZV_Srq?=s(qvoPiiOB~NN5%N&tm z{yXc<7Qg@i000H{cgMK(;CrcT&1C8ZLH2&&-(2R*;LxHYEg)?Hx znWk#7TFJ4}a+U^k^$LV(DVQ)Ps$y8}yHoLw$8fOt)ipu+BeD{vj|VRCGwUcQJ_N85 zuuLb5r`?IA<)oRQgkUHXSl}+)%mvBXO$Jw)gYYfKfTI;_O^-P1 zhE%MK_DM0BJ;&s8w+A2^tFTugE>dLE7X!*7)F`zIRveexop}o8dQ}S=7PK#jk7BS; zyxXUOhL-3H8*aPC+)RTDl^CWA7ab4!!58D1^nw&~y$+mQ?nSA+&t;EvgLIga?UjeVGZ%aMXG z-O#9AR5*hy*U&VA`vapV;ZWFNAB~G=!zB=4|9JFZ0QGdijk=^dw+Ri^NJy_z|39)L zLEx3t)4gNM$N@nOtN80TN=)B>z5qhQA{-l#UU`7Bo3IqyiW}4+osG7=R8*!zVmB<( zi$aGq&^7dx%FO&sdClZXHeW}fC5eyg&`1rd#QQSk%bs3kPOdZ|*7P^9SE0%RI1Vf$ zsF$~OnfDT_C_y8zrdDRL;(-oat~0`ny;>rA*mj6N+*8N=DFBCBF$;Jw0AcQHTDYre zA|Uu3CT45W0(P_wxu{)7i)?@e00wg@P-dBuLZQdPdS}C-U#BAzerJVO%GBaibN!1X ZSO66fIy%JW+RF@^AI^XP00000008fkeAui8$xP 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 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/login/lf_icon2.webp b/admin/src/assets/images/login/lf_icon2.webp new file mode 100644 index 0000000000000000000000000000000000000000..5e4f3fd0dd461d4e92ab3222aa9f546e2c4ca2bd GIT binary patch literal 25016 zcmeFXV~}Lg_a&NTTV1xaT$z1jc*>wMY-W<8s+!7cJ=z@gRSxP^bQfI z9W+oWj=>OEDa??GZvhNLM3{;Hv%ZMI&jMHr8p8sXH4rtJW%D%ij1jcMgd4;FTzMI^ z3SSFc_}UeC&yed2sM-u^#l6e>n3RVY9t8Hi&dK(JcwNFC$I%i3DQo>ej z0JP`#m(LU5*WLfq^0of#?W$REpGCC$M|G!S=gcuKxv*Z0L|kt;nf zzF*Vi0#YSQWifbcd&Z}nOT|>BqFkVK4$jpCF`nU+DUgut~#yYP; z?U{bGWLo!DuRzDi3#T4m(=%Aqs8qBs2VLDjLautOaB4&Yt8$?ReG4)RZP1< zPrauh(E63o^2TOVfl1vOuF3`#mCF1BD(V%bSyl2Y3kswcs7bt)xfcbyVhi;Tme41`B_RH@z2NgUcs!f2nFwHhrlYu);QB zXD>p-omVGv1~o$m{YGDehCQE}*TC)7;t@W-#K7>KOT+4)!|FH?f`x$voa0Muh3snK z+pE(v7U}!-XjGnt)N3vvge397Qv0Y^ygUEr8kE#+aA2qM(*H5GrEqZ7aEsyng#UGe z{}q;6`i^G*xySdv6ee(Vm46R1q)?Lhtd6?7Rq}7m=H4N!zCWg_$f$fC|5|?E&U{-N zS48>qJmc#*=Bs#YvKcwwf2l9hUsieHRb4w{07UgIlk}99N)JNa8hZdp^<7i_#wxEb zKYmfr1Z)f)tr8u5dvkg?Tn~SOs$9*BW+*Bz2CbV{_5D@-=FDiY`v35SgMvzimS(py zHwdj88K@dT{&o{O@}t3-A10`u3_NJu&^jstDD5&YYrmC( zf~CPDy0P@%I1PqCr(Wd`Yub@E)?|@QU_pEZU>@UBAyteh3fIsuCZbhx&7k`0 zXcO5M@`*HF{)@ram4EH@aZnG&fO`9}lX*xsm=MDi!SL#Kde_podW0z?lh6gIY2AtI z&B#zq!i8%7!rhQ{(*z|KJwEbGcJ1z$bR|I`cdj-@S z{$UVM6d*2L?6Tyw{i@cPW}OF!SjZ>*inSQf+60wJbqy!QT=e68YUNQr7#Mb0>b-NrYomr7A4@zLkK*Ny=4zKRqT^cQ4Lr8>7oD;~#s^XU^ zQBVyu)$a~v~TJSF; zVgm|F!Bxlx1ek38Ml-S8>!roZZq{QubwgiE=mUn>U1FQ_Q;4^IL2?aa4QGLeWI}6> zKLD95SLAh;2~Gep9$Z9vZ1M|c;+7FL{ZI5EWHNDVKT4~T{uOJL2GH`acT;_TaQs*i zaqRXXdAwSP#jPI&$>qJ>>WAvRR?P0g`@{}_mxU%?vuO9t@F^Oddg(S~fBAj5=4F2M7RyciY_ zRcm=r)Eyylxq2ju0bvlaTjZhoUp6+`XaVT;o^_y+j4f9A1LL)D{Dg zTKyKC$N|EASA-*x9*xsq6OYw#=tsAS=s!~l?l$Jw!5bD3WSaZS-~zg=#nCK772&gKfW3SH-(5Y48XV_U zP08S{Ke%8Iic+mb8yMr{4Fp}KWgF4%lqn8nl@wtu>gYqd* zxu9Lgequ>4_d5WY=RV>qW6MJ#XH4ZKNa2B6B>~G{>-w7oIuQOi3y~Q#49`y!VuAn_ z03cE^1HgC+yTOSkYt;bB;I1X=`Ee9$ryTqo0AKO60i~VqX2GOQ7u9}ldj>ZXK)h+9 z6i6Td;qt)T+>rYxebE7wH>;!QDN5WqidSfGOKJW=E2wNtoPpqvs0Acs+XZNjqBIcy z9xpHyG-Nq2gp;e@NI2o)*UTbL#tci5OkfWFlM?{xvE--&ww0H6QwI={7=jZTv%|89@1y!i{!k{fE){%Ek{k4JB&*WCc7o zEmuZQmagfIfS*UpO>KBsx0v(rX$N+0sJ1}7I-ru-#2>93$*zXZQc9lk);%;2IEX9Y zYyA9Z?UtT-Z@`Db*6O(Fx2m;J^*!WwNMu?9;;6pyN8&!V(V`O9vbBL)oE(8`0>X>{ zmhNdUpaOOiekI!96SNcDw@m?10=wl7(f3E-9KEFKAB-uU`{*M;akL}bLure=rEYRh zV6dx0ts7A?3QE-bDRPIzk&M16#!&ndj)mQ<5(vXCORQj}xHnR%LJsKHOo6$_ouCXS z%a2&a=H)EgDQy~oSZC6R16m|V6sLqAjS7d$^<2ZK&6b1jz;Z7Ewq(8x5)woh%AG0O zm?mL1?E$TX*;bOIuuP2M<-9;G3f!2&bQEWVA1pu6ZU%1MgL|}+xQ$a8LIj}?u1H-) z9T2gTmskfKWe9VOPC1yTr-WXu7%V|%YCBMrwkrh*hkKMhNnUlNuK)h>hfLmL7=j2Q zNZN%?fG2PYyCM1??X5lF z96OYTbn0O}$8!8kmyo&VB5Tb&Y#i6A_6Vn(Zpjy6ze&{)YO|I>O@0rPTAapb)mcUA z3eH*E!J57oz=$-*|Cw8|fYIi`Am4v`cq-wkYN`)Vn2cgZ;soQ&`nIkE*5Yq}vdS{X zpT4*d$d+}Lv80ehS^pRjhSPqASyU$3$Krf}s34w~Y3}rGElv)f*N3&{t$H7!cb>Cm zyYA1?+R|EuP0C%OrJe*JWjW?o!6z{0&$+9uP^4~!&(r0(S&HWHXHO=^AbAKKo5{FO zZQ`Sh-EOUf01ng<M2>rv}PAm^7cc{w@F3_ABdbA&01z(G8tfxK_0R=gvWvy3w48c8)1KVzkzj} z0OhTAs>2eumSMaogy+SsEBLMV5e?;NV3DlFB52zCYpW~W5|+vupMOGP5Hf02nM6TQ zIURHZLf)xT_~#!*N4Vo0t8iEKpQmYiP2l0+Dh0umyvzbCHI|@8D4s1g{(S*saZIX~ zo3@{;?H|`2gLfD0WE6NAg34lmRVk6fJr@vw_a};Zc4}WR3>y0d$I34b{1}f;m$&@^ zKc@@r{p6_?47aJ8f`_q`ie-W>Xr@fS~CyzjX@UR^v&^v_sK@LwmAgHWttkzZ`O zb_u_XzqBmbvU9eK2o!MNj5mmSbO=AB?70a2%n9nl3G-^E;liX>*gAL?E{Y90ikt5j7;U;H7pMO)!+K47Ai1cl%%9jhTOr6>KC(xt`O zIFkq`f{bU0C4Mp3;B(SWI=lSi3RUAyp66&5WA3x)Dp^oQDO87rd!O*x806&N5VRiN z3}95q_6oyziWqZJTLr*S>!NoovFI7>qw4Bku(PnhVCcrk#RK& z=0B@3c(qzaM4iV4?pyozh+XTlDAhnCnd@-x(})ZUr`_q&412_#&rX;`{)%6Zx)e*- z9u`h!z+!NeS#ZH*Uu%@va*d$mSqQg6zMb9h`UDS?(9`(nmfJ- ziujA#)p^}g_W9U%MCMwT4kiVv{BtXEQ#()@X)92b=^!!I?o@ToEG(328~gcw(UAYZ ziX>QX*p!ZAFp2!wnl0~8D*_5_r2a7wC*uZ_bO-18eu#)HT!XyfM~VX3pSgw~`eA|Q zJ)GCZv@o(^u4cxxO4;ftK^-VvU{J0lwzOW^{DwKdw@+w;qz)LTMpWrIz*3Lwns)00 z#S-o1HdM7m!&)JDHK1z(<(Lg}qO)k&O?r4A$f?tAh51oy5uE-MPEitm@ehmAxe<{- z|A5S_nLBY}jpR(sq2T@9l(MwavGd!4x8xZo(Y<8{k)oP{v!;=nHh6+qMLRn*RNG~h zur=;b0^54VfI`2(sM6D72i+k39_)I+mu#mX0*>FEoHq85lIOJz zXKVuhP2l)k7UM3nz2Rswquao-p!26Y5`ttraVZBiZ*YXRC|G2T=K|G{XCjF zKn;|+$B4p))&%ZUMuSvB!AWKh7@zk)D-$YnDAoacwML2`pc<6^RTM-`J_ZV{T{k9N z1Qj(5NzVvhE+=0b@g@`CjIXuUTu3`^Ph+MBV*M{VoU@s@&&kx)N|#KXJ6Ku~2Dw|y}2tHMD>B3xe;Eh~vGm~v>b6GMlzhsHsUJTFg)5=9I>d#-a{TJ(&SvY=- z!AW@W*NZkJS_vFY*3Y)J7MZ%boWm6ak;>3+hKyF>;EM4R+!(zVdK+n^zvPz%8wHzI zR_w(O$A8567J@Bfc2QgUX#&}G6zW(YjXaCrk(gO(O<`weh?vXllpWlG7wg61*e(;o zO#HI23TgQv0;5r-J&eW0jB%<(6>BY%VM$KCL5E)E zZ~U%pXD@DBNUC^Bf=ijg$}$`|)!4VEAY2Yd0nFO63@H3<@!QGjK#;PFhl6Kbv7;@B z2)z{DVMt{)A`zQ2ah2pH19d4~HaYb}$GbNsxCLm1H>g^31e47ohB-#_BdU+2V;=%Fw`~gZdq8G-3k>6I zcW2C$MNo1Q^>HS^GvWJn2$`OpW_vD?NPgxp0UL9wxAfuoN$KGl%3q1igY`4~z8(B< zuTF~#|8msYUzh1W@l4LeVfG`%`Gc+G+Ku9mOg}rT@Uwz9cC1M+&L(6k8eynHKtT=& z3?fq+s3}8?^CZyqWhQ@wWvJGe*nBacyR>iYoUL3tKRKTrn>T}FDGHwszB>r~YB30* ziNA|G46Cw}BNzX$#u$o%6nr4_tPNm{E`r!dN93-LdwVB3x{ivRX2tTWrh zS#~m$@#jtYZl@xkgc#peO1u%oKq0jdXUcc98BHU0tHnh0eI@ickJT>XnI#p@VLPW_ z0n^eX`e-*yGDo}~tWCtW5cllNlbt0z!|*lDOEZpOxqjI-1Q}^jvi=~aBwQ(CnIN4H zU*HFcq5pPY=9ja!Bii{AUP4NgTj@i{>4Q|WJvxrV#1jQHF%QP66}ym1KFpT)DNf68cqtb)eo2u&l}8@SR_8er;t$sBelX-UeI9$*COM@Z5Z zbN2!Z1k-`6PfMLFR9omgwZxzYwF@gw0*)P^2-pJO!qWo_gT?oPUnzzQUV?5` z_jnOC3=m&tE^Q*~~?B<=8RzJ02TICx430Kg}Kkx0XrC(2}9a0LiT3FmA- zKCn6+`+G0Mmx*K{7W%Z8HS=t1hP}wm#z}+1Oc6}U;eag zPU5G@OTu9j`LjO^TpS5@OBFu9W74$#Yb`kboG4f!ol$cv^Co@lEJbL<6C7R((YMno zg_hlFrvl`U_|7pkv1bs~8X!8jtDbh2))c@JMNcYz`{+Tq5GvcEaW)pxb0~-LVd5lnD%-tB;E}=1D^Ya0!1XdwN=P7<4GboaDs{7 zp_i*IL`b)p;vPbRVL^XtQ--x`IIr(_32f5-+7ge5v4MO!Z7t3e@Ao}B+m+u<{4d}_ z+L=kV!`Lq3RkL`c%=-&rg)vwrx<=ttSa1%j6f1PB>?CX!zd2>5C^&Bs#iC}bA}q1s zYqci*@(?ku>Vv4h5t8Ki9UjhlZ$aU&)GXZ||0~6%0AqKz6FoGd9@#dq$+v*Qomx^zqB$TV}(KW(x_S4)DE-tG4=KS|7NLz z{XQWh@D~LDViAC|fT#eV08m~V))Wa6zV6cec!_{(xG*!@cPdQV8J{se$ah@RDU$p4 zci>VUUvH{giqH0EsW<9RwFZ!92qCr|UGU`Ymw>IRPvlJr@vYJKzqh2(-uQ2NcY3Wk zyYGdp99v&_o=QFe!`04Ttw%5u`1gDRUvVGxFBP9>3s;YPk9toZeLiJAjt}j}XYXRU zIV&+6^uOhvecE&{`C5IN-pgMApN*epUvf=ypK@k$H$Kli0o_SoeP4SQlwb8vRyzf6 zdLKTU2$o-8UvHf~e9!K_K3`{QclhT%dwgBqy`RfoxHnVxJ{uh;=lD&B&}UXp<1br# zJl$U$Usip3tM7sz0UdOJt|qthkLr&vG4B}vO0mhk~ zfzL7DuKT?A)%W@@V8(9~IO%7Q>mct;Ku!Lu$+=2V-#036L}?~Lrt6UavB0u;9B8yDH~N9rHn;7|M~Tz5Y^F( zB$9g`5k&S2Yv_MkV;^!C!J<9}NTn+Ppc35}tw;59v3C4G6n^`F$TXkpjlGnMXh$Lo z<2bbCzxN-{GDKHIpzSW4w1T33(Dmu`r?s)GW!gZ#1aY15t({hLj!X29Q%hEIjHlPP zyZ@jvq+~I9jn15kYz=vq*G5n>@K_ zHf~2XJ@O>$|LZ~A=6I}9=pHo;V|WJszz65lBSsJF5Ana5t*!Pvt(@&$1;y;7Y^+!= zmu>uvpSa9Ys8~rQ@IFcg_*(XBEbRSH5Ta{Pt=J^2nh)c?yRF9X>p6FukYXr##EmpV zgD}?e&>$52kX097Q|qKCW>;qsPX8~6I2SoQOMB1z zI3uS*$Fjan6WM3knt9trNgSjCj^F|4rbma-v5IY-l5?V zgd~Vto1JI>vGDiw?UX1EvDwt7ypM#S@rozSIjRhHn0Ro4((Jn};f*nlabwSmgbe*q zoAkSQL%*5hq?i9qhQvbpm2gY4gxk^8^kMp>A7|R0**x+!Oi#kmt7Go24DWOyL1_lq7)@7?6{vU4t}^ z1U+c~jkvfMM29^P3}C8bFUu1UUBtKm;IuF8RT32i@!n@Xi0w~?h5Z{ykvgHDpxT0@ zP;P(!JBKMSO53zG;YV6HMHZLigR}3SN;-K?#)l}fdVKZx9!6$uD7bJLqF!Vn%bP=@ z%!TxsYUX6=Yg$tGcsN5y%@lr5Rn&~8+!b+p`Yi7L?msD)5(EAmP%!5rikC1WO63JM zlgYSH21TQ_+p^3hD6{f@Qvm{#(t27gg`}p1IzMg}eK8oEGrw$zz>q!3{JaV*cz*b# zxEPZ*Z%nD++YjwtJwe}8Vc@#dCb+M{#Qn%1@}&5;&4|?z`=uY(Z^{1c!2i&zXXRBE zuPDN+Z*c{UQ^l6_cYyOav-u5c*6w}awth!dKPz+yS50cy52`Qi2bwwLNkRp%Sq~=2 z1-CK~ZRp&Oo*!Etlfw1F;zd$hD}w?{7*P->JpJ!ci1AonKn?vEFP`Q)rBL8__k#_PQ$HKAK$vyf#$rkX<2LJTt95xl73Q z$o?-04sI?tQDU4kLdBog5imQ1(GYHhDLJx zM0&8>%yc7n^h9+rBey zS5_Qy0Um#f_bz)K*BK%NB{z}nbqr`w&M&?L%bNqLLX`WV21|EBnYf`!mjxElQF44v z7P)#t<1Qh!wUUBc>qtD&4Yhr_KJX->)Pc`+C2;e`X}((+NYBaHWOz<{cf3HZk?!BM zDlQNCgTuJWyD-px5Y(JKzvPC#ixB?I@>h_B0=5>lLh&35?Hm#GfI8Z$)?GG5IrYtV zbK`Sl8e-jw#~xWaFYTjP%|V~zEC@n-?G!!X4aWM>?kb<{aMD2`xdu%Q=mp^#KMefl zZtE++C(OdFH_>ooHbOWb93=h(WZ1k7U5cRb>u~}56%lzfI(;)rhGqEHS8Xmaq0@9# zyiU{Z*mm&`^rTy@ii_p~;fhdB`T^O$XOz+Zwx`kz?opuHFCSO_@TE1ltE-=1qqD8I zqp3bq&BG&vm^;_3Y3x_g;*G5U0y=u$W!a%Og*X|b61j85FiY@KXA$zIf=Z<72NC>$ z5csC2r3!UP<#wO0KYU%<0U24-Gq9r=)PgyoapP24S6A{#-`*X4?rMQHNL&`Fyweh> zr0pz+0o&ZmJMQBo1iLg!jn|BzsRK6O!8?&eXplyeFq(aUfu#n8()43ef6q=f<-fep zx4%*{49jzEo8mZ~pt9J?;T>N2!8U*v5}J|E;yb%IcWse8;B$PwD)qS*v}DY48xIRC z2*MuOCR+~%jX{C0@Gm-LjYhfyc$WD;@?|W-lb@O;((Y>fm-%dg1Y3(3jk#8=fit;4J<{ItKOz35!LRY3EIV!@y znkuX3V=83oR+E?eTi50n(UL8kz@Hhc?;s+*t8~x~m&Hwy^2&?~zU?6#2HtL=!jGZe zVLKaG(Oo!s`6L}$v5pZN;zt)=o$d@iz01==-oF)^7u`8ZoLYm;^ECs9g(X1rfz?T5 zbkD)ck5-+jGv{tPV#zpqBV~k_Sg2vPdkiFihiCndbL8n>_fCD(y#7%}rAV@Q$vW)F z#uzA+W|zjb)A`(CV2*(T5iy8>iu&+?DtfH^C5Tgvg)rqog?_=uHDK3EPHyl&Y7(!x z)=mux53SZ?CLf8aQjYlFwH=7?TJi-k?_m}cz-o4-tGp!Qb+SUTYhgZruvw$bJ)dZx zxu+0QqvA#{_FnDb{Z@H_(V!zOIMZM7143+ z)Kl1}A)-_?Pf>Sb2T@>8>NX2CWnGzx_U#y5IKn`{V$xN03@Jhe6EI5F{m;q#Dgnb* zNA@6zpsI}S*;8L59laM-_x%oy;gb5kTehM=&&9d-mnbZv<8|~z`>ll{eLWTf|8(!x z?dm#}*suELo}Mm*O@T5v7ggioQuSKe&(N4Ff$tD4WXPuG6h&v7&deklj(hmFU>*D> zDSa|e@AFTW$X3bE_GVu!4He9J#EsGowc5%nnK`|kS#^}@y(TqIz*4r9ilE2Xc701M zt~QDNkzGFmT=L=+wK5_KMn3Y@>gS2^YF7kV69MOi- z2!X7(!U*g&(Fam80mR63=C|WSO$3fq_Uhbyxt-JBCp~ISpkVtvi!=U@s}X|WbeO$T8#>}1 zRGvPv9Vk*pRsJ2-|08K=+aO#N85X>bpu|NvvsYKyKt(WU4QVmw^YtVBTLiDZtVMbk z(n-dAAr&cxf72C6bXnzfhyIs@7Md^%_8%lcE~#@>68Ab(72LK8kEk(ASW&G#qJo`i zSk!%-13B(}r%kLs5NY#kA}uk=xBQQ|wy!Tot@a9xG)bPD4d80ee7h_Sq?GH$?+d8= zr@gW_SGp{wLGX%Wss1Lr#lDpY&7uC5Lk#Ffg2cRV#G%!TY_2A;dh?85l@u zm!yu&|B(?*!B4$v+!TWF?`aGs*AjAHoMWkf!~37SNd%AYA07THlTwoYmk*4Y&{`n< zZ_SKWjt9W2`-K>gSDh?6k@y)L>+Qc0{2KWZ^g9Wl9x@n2n>=Vxlzv)9azqDc?)qTrGYEMcan;^X`{2k*~$7>e!f&Pl*X(_gr1<*2#*v z-TiqX+pCKr_6HXw+-FvnY_4vtU*Dg(B}C$6mY8wM_t@`IGCQ2}EG7#%SuA9Orszo# zv6s2n6^=sE$+ME70rzg{S@wHz@BG&;r8|SHH}``5t_v%(1cE@amz2#h`RLd~l>{iH zdd{DmciL*lvGV zB~oF;!+KAvAuTFltR`?UY&7aqkGj(EdL!%c;n*Zbg#+bZ+Ddq7W%cR9=MTTkJ}y+e zPdOqQM%UIBeV}`F9~lwo-O_$$o7ogp5y$gZ;pj&@pQU*bwPFT3Zaux zS#RB1Y*@%AIFFxRW2lrQJ(4~n46)gKyd~TEfLsGh{V&_r}(epxB8U}3a~w_bz9C)i}55ff8_v;i)GF$Z;jx9iI>CZ>1zT= zEzw_b)tdu4?Wq~I<)Vm7))SNi!~2u<4S8PT*XEmf-o2d#1E|gPxd*jlw}p{UG0M8F z66-lfLzFNugTue#HmiU$=s&Ilp0cz98s2Rb^tjD`ct0N1PFN?DDMBfL!e$<+MD&yz z^Ot(%J}3H$H@IIzI^Th0H?>+CrM(M6JB{1DM`x8;ITKJY^-o%$kGc4Ubge4o%tOO#a2p-;H@4nF?1|7EX-{^2| z>_i$SDF;p=hVJ(8jr&VogaOT&z~d2}N;Z4@&#P$x-;DHHF9V~18MJVq{_GPH2M6aQ z5DN3oq|ny;OtFP%G!;gUC1}h5caI56U*w7gd_^Rm-rVflTs5%PY70{FWL zNL22W9fx<#mMOWXu?~4?3Q%7qP>b3Jj3OawtxeRm)t@=yHnw4n!2}D9TI(6`YVw3? zzD`ljUr?mmj>8{*xhsu%@)8PmI7e3<@RKq%Zjc=Mw{e6w z9zSO4F+tcFkJdtWJ{2D{A3yObQIUVmMb_8$_|qIhpVWxiWT&78i4_E2?_@fK^o0!e z*#_w5hJ@)tAMtr~xYT`S?rp*r5=oumDP#(ArraZMQ36rkkZE|Gq=q5RcPyM~4Hu;CEP)(lc4T*$0BL_p z@NQ7;S8?ccuUF4XC>T+Tp43rhY(ScY?4|OFFgB`w>);7VE+APgmxO)l(mQEU4hcqL z$=Jsv9x@{#)mq_lgq891W#18h$m`|!6V$(L^!nq4+j7-U(XsEz>y8=2fSl9=n(ARS zI7=HLRrnwneu+JA1eSW&lZZh%mp?u^=-fdftRp=Jl`4%kk$p&57`H4PgidmZj)X5m zn4fj@rHV=-MK)gmUVs@-5KJjDAAh$XBdM8C!pyhZNtx+9ROHg_XzQ|}M1Gs{C~~;a z)qI;SK<_QTku9FSi=9-k>wfHSOecnpQI%_vyJu*L?MUu#!zK)N2cUOYVXj8V4IntA zqb%9+6xl({42A+#Ks*4Ga)8U|;*q}G&5V(f;c_%|m1+yob8AkcyTICZd82k>G(~;( zMc0}^#7I%+8jJimHaL(f65|3md&v*=7IO9F;8vE@yK$Wzuf4Dwuw@+<)8c=tPGbV@ zqi$NbPmWe7LZ#8FOC<^COW%*>#;PPIC`BR`5l*Ozd+CzoGkKTeOwe2=T1?!{OOQVM ztAj!R1fQ`FR|_oT(l1VOZ~2WQL5=$2;!-!t%^)oBVqWI2?k$S$?&-E>1$FS*A4ie` zjD^fF%3UWy;(Ai{6tq=ODCgd0LU*1>mT${EqeJR&GpOXL`q~UZ{43lZ2SaJ}}4Bg5EAC z&4aPohT=zkt~)KB`+INF(I zhD0Enid@4NK#9@);cc?zOL zEOoM~uk`mBeu}v{m?3XAN~2ol2_l&?Yj)Zs3NZla=vQRZ?ezo5ur6nlwfD!*8#jk) zv0Q7Vk*>hBkBeJbnzyq+y-`xwJk`I#f+_*~bS~>S^08~4^a$)9$Z;IF?y5g1BhnN^ zLLr(Hmy*elyB&UiVk#sK$16hjW9qwM{n`@W_G~>oykW;*nen$)*b&q7eh$@Y-vM|` zt%+k(0&nE6G*e!|4k#2aehus@{UCSEDuXCNaM7>#vz?C>r z5pD57kWxU`<3XY|VhMjboY^Z6MrTlYME61(XD~lZF8y;#ev!bo_pVu)hY^U;%Tqji zSDAjYa6(Z2<_?D-Flu6r7eN-qG^APe`4)`p`Io?ne2lxp&BD3k`F=EpIU`=vRA)Nat!j5>V?K6qW?eVO}RZ?*e%s zd>5YvF0$@rqX|QxFee|a1Lz9jp_0!s{MHxtnlkzrja*(53d20KQ!V;#oL<#bo3hmd z=nT@p3k!RFMoqrnXY0;_`7x)6;q_?Lw96SO_eP@8X>q1Ji-8YzO;_!(6IjS>n1C-; z(OvSuZ{`jKxPl;ff8}K>n4w;bh<@swY#*`#6}Q+ET@MI*oPUGj%n{s_?mbl(D%LI|*S6 zmLaTG+y3HO4!i#_|G{yr7U@p`51lz@HCSwT?G+LCx-FAe&I;!I=GsAkem8R3J349C{~$M4MzJQ%cIlVw6>I&{v6^||6Ow*4&ll-o{VdQCru4e_c<5@n(i z8Qu;fG#Ssf`>WG<)X;$Ifx-fj4yq1AU_46@Aqbu^VgFQxva4<0D4L;w#{+b70w z007uL(~(lcIeR9rkkVL2+9oJ19%kZcL@mxd?azMZ{>!7mS)*tdd1*2gQ(gJ!9&6Dm z?$l>>h)c|lR=jZau;wbhs3IZRbATB|%*&n{tZ2|474Q;X_mXKkq`cBLaV~$=hu4lk zbVJ4ItcXgv#wK)`eRTKND_%$F@kGE+Nvvv3*a;0E-!yyE0dJ5A+^eoj~!s!o#FZ->rv)K$LR)vs^gMPN%+!5_AWa zpzVf^e$MYF<1^{=8hQyuCu0Y4^#yF1g_`dLZbLWn7yycGK9grdSAmetMui5Xz2Y7?V2B6g2ilszzTw^xf`vuwGKgY(r*p z1*avz@3o4?1le0ZfJ3sou^eSgn0UCuj8_w0gWscYgvRhqI}byLUE7ho*%QgJ9gbNx zUBU;VpNeez7c2mFM>)2^mmm!cBHbF|NpdGIT5Kas#zs^;T9c#{$`_DnQy+#UqkAB1(uIkelt7}yMG|1x5 zio-tc?&9oTry^hi);(ks$r8K7x z6v(UbsJ}Q0F3YM3$r_^O z)j39PP6Rw}{kGdB?QrxK%Gor8{~Y67dqKPh1%5QIe$a=&EAQ(pRhdO zMz5YLk+0At&bH`GXE7&{sA{Be4$&kL*)a<@p9wIyqHjysym%^oRftyXSgE|%0QH_yUH14Dmuu356`k>rl0g~ z6pnjg+dOmTl2|Y!o#rZHI-Dn8kEi-=eC$?82uN0t=ETp=!{3OBQ*9dh%yw>T7EBq3 zo_*7Z%V>yBGG9f~0)7@^JgzaaR4za_3OCR7dPE6btE&DJ0X3um^x6R&aN=eUa%~fa z?Dr8AOx)1w2eQ>0%NOC2K~b^+JmDnR&!##zI*-#+*aB}RVe^c1O+8XOjbN_%q4*BR zdc1ASEV9r)&&0N60RG$Fl!XhCP;oWHQqaegQw?oWz)~lN=TLxaeJ{sv_Q{R~q!UjO z@Vg;Fa$Ctd8Pb=@%Gf-;)hx$XR|H@0&F zv3!M6)^-l;<}*>4=iOe3a)qjmb?9&HKJm4(=oKIcMZ9XR3A4dIBUs3?M?1GvaEB%Z z=W?JaVJt&ShAbIcaSj2hEX0DNjSSc4wm)`R5n3<~wt5S&%+?3OuEu1qGhZa5lq@fZ z@I>%|&4z7qsh|6+3#lvU)5qLY_cQ`Kr(97u+fbMrARp%PbRA6JG{K2jo$KB#O$2=;_~vVH!t9lO)^_ZSrWuTe_XoCO#i`@^X)8p8W*bep%5*n=_7GSB#Xoz%xj5Xc=EpxN`1VT$Fsn6WpN=I| zx0AAddxh1rY3e#xpKnXQI7bw3LiF6CvfZEkmJ3N5K3OL04aaZx#M8hffxc6{p?USU zHq8B!f(C;1X+%+3^6MJm`*5q^W2KOmM~74vHMBhCfAZ#(&P6;WBbIN-n19h0xA`4+ z2|rPbpsQ)_+ztPS|H_;z3YoUe!SR@zjc~_9c5%Y2ndOLwgW6d0w%|p4%y#N5N z#X2R!u$O@{?4+`95>JYhnP^hRg-*Oz=CGJilht}U2{`@0voG^RkU7JUxgbNwY2mQV z8sU2Js?KK(CUA932a>q& z+JoqQ;-IkCOZ_zr4iQb{fG4z{gie1=c?wau{6`7G!pb{t&I z5PXz9t*%`+eky-QHv$skJ~7jcR#Heuc-JOA!{$puIRzJaP(&D59D$*#{BTfP= zh@?-v6SRF{PFFo%VD~2gjhHegOe(B0iFbZSVu!1n)$Naj?Cv9yteeI)nriMp0zawEDVwr= z`Ooo!##HfZLZA_QXW%SV1ndYvxRXicu}{_QFxG0g4#k;IdN~!Cr$qiH8p+x%O3$(w zZM&KcG@s>guZ>G#ORwl(XxBxhLhe>rPVJ%vhKkxHW!p)QcLj&PuL~~ ziS{0(3M{dae`R;2o{T~oX})~Me^ig70wO6>2OkW!##*j{0(*m+F)eNLR^TWg0=Vcs zmZ!qQ)9ZG5s#=?QONSDeGr6qpe<)Z|dyO3>AuAFjLbj0IdoT#jtyum%Z_5c=#+OrK z%y;wwG%-`N7QpGr5Be`aRXo?gNRQJwWuI5Lao{37viVMd-d|-JcuN2y8FMN#kf&z5 zEB78AXy@&ru}lj6#18=)$!a-qxX$ZaO1^J&tio623RG*~zY}Bv`Q1wPd%C+?s?#c; zd)2}vR~QDxSv}2sua!Gk=D{U%)w29A5R#T!KN^Xc=n298FsY6mYu%pjHXX=L5Qzw< zR&f5{S%#=r5tpELy(!=4r-+=zRj5H^I1>A(X(u)239ByYCSG@ZjLD(Q$!5+7y}E@h z5NO=MV2_f^Vxrxe{>Fv>O-=V+*<;Z@M~k00XQ{AJ5(u=Pu>;A2G@5-adYzYirtTE% ze8U$Bq+CXu^S+Vt{Y5HhO`lOQJ7ziC?mMA~MnwU4J|lFs9<%{JSLEG37fnmw79#;4 z#_k_CT`q4P%F!E`b5qwQ!Vi@AQn6qUxef3lQAs7akQ!wK>;j6>wgx&JJAFyu@qCsh z7qf2Hc{IpXjm8pv5y)xfSB5TxMC=y*xZ>qL|5xkg+!qPMM&WFmJ7KHM zcAIV6Ha6Rw+HBjly-lvoTU(oFvW@ro5byoh`3U#PIE$5K5tm zgO~r_1YJCuYKyXw|K>WQ2f;S}^Q6%2vPe$Uwc+kb*JG`>TYQ@Gb)4qU0#lsWLjvIH z<0~hf$yO?dQ*qA$aT5}}wMx%BVwNP%U}MCVg^9vx`JlmN*x!{+s5J7gB@%8T!7tkb zYnKgJFk}yB7J|x7F5Lqw7yH|$u*8E~=3*6jH>GcMQ#`HX0xpWhw&l#hl+nu(Pc?t^ zc+$z1wV;in`BP&yv%^bl5q>3TNG+h4lDp(^AbcG;C8Xp@PPnkOT%Dy4yoi$j*HR)E z{`xS!Sj7wO{IRRVx=t9kll?q0h`Jxji!*2x*>Ux;0{e!>9Nf9p6>E}>Zjxer^S$yG zFyDzOXx7uC=yOwr*P(jTN3gipA+#S*T3;$;O;@o_DS?H1^+BuPu}(Etw74pequ;~m z_c!o;P8Wh1%ZgGIW*%7o5(WgOxv|(jB{jvNXG|+}1BmC$g9*tnXyHtolD~dL;QL>{ z5~%nmDz+G&j2YXfskB8i70rr2i}q@*Z7#)V*%OL#Upf-pbA$u!B7}bqYmHVxsH}4| z6RfFXW(HMP7wqimFSx%$X^cX1ECj4`|CW7{n&p!k47mG^9%~gguo!J6#hC}Npwo&i zK{R?uS{&}8{_#TI;jzr{Qr<{H&=Xv{Oy>)HbQrr+R$%c^3H-D$i{Biu{JY&mRFu-yHc&}!x(=(b`HJg-ftMQ<&G>s8Zne&;Zw zul{Tp7>h+kZ(BM{{g40tp1W`+m?*=4a2E#$m`&6Q|b@zg~w;VT%JWB{3e$$=P%TAPxu{)>Q_Dzb%MZ*~h?N0CIx zb^5pJ!&`Sc1VE4Ytwsz(6CPZLyn~IN%yuwi@^f~V!lhK4qX~+tFzW|nwDEuLkcDY< zHA_Fw8(o-Ghuy2IR;^iYQ2u8SFiiGN!nk8OH~`caI%05G zG+ntw`3S@Y-zPlgaR;gQ@c_4NNBE_PAb<{(CZ{Gyvh~i~wGWLII*pre`K%4Gt6=+5 zYf??hROtK!*x;wKQuBm%~|fqBW*mV4>-npoT4avhY%K8y?#z^9zFM5zNvYLG7U$VAA{M^>(V+s&yZR| z%M${|4$anKCiISxI*@3E&SsT+ic6v_oMhiP4caZ&>ZvnFo&+hSGv*eFGw%W% zj(tQ3k!LY@)#CBhd}i?OO&p3pc!zW*OeD9*L`Rv*&OWXYw!%iCTr{G9@gCoF_@jI% zLHe)Rf32L&bN}6*IS^UW9T4{X&m(?i#Szg~Jrl1(OyZk$Ss$Od`sox>Ig-BO^*8nC zf3)<3S>6e6%rW&<;#@$n}7AabY_B)6ZXJIevmUuC>0 z)`?odzATkb2^-1C9S2*KBz-$ziA;#_TCFrZlocqtJk$n5Z7>Et+#`kBFEVcQ9dkQkX!N!fO)Gy3bRnKonmnO;5 zY$j(~7Ac3_{JT8lI-|ea6Ri3i^~+}w@KFR~XT==(i;nR}rE6maE=g4lg6EubBP&kT z`#IgU!3f!c8ZE=8W*6SygU6lPTq{H1%3n@4urBsVF!UmvLToI=L%MOw$Y#p>dJ~`G^g87kE=aSJ+8ocQ(O+~1Yt^Y9Qh4c_!(x7r6TTa{BS8;G+QG1 z1!NgFxYxmN<0_Y&?I5k)OEli9K2D$;Q>hTer5CjJEO+R}C%}i`V-$qX+Z!m8s+j?m z*}6Z&3iH>i4j*cFP_ctPYjOT#*xmT@t8IUh_r|+RKY^j?VhL{iFVYQs*>eL*dv<)q$d3LT1mgO zDk~+|UK6l61f;iQ<=lB&kAmng2{Egl_LfaQNo$j{xbMXyAy=OUmEzbeoLq%s#A3mV z#I7g6`Dg6a!hc~&QRRAD!SXCn($Sp8FSdW>FEr_~z8-LtE%q@uS8R_{@TQn=Bz|Yr*U3{@Q3E@l z0Dl~p%p)5P#&}@-Epkv7ZCmN!ulA5QIQGd0hWj)Gd;gr#I$)Hrcjb$Z;~wi6D6<7m zY+QtL8kr4*ZFXNZjeF*gXpM&O?qv^@quIqQRmJCXjhFz);Ma9^Zq29W4^nTu6Hpq5SCoK zhLS+pL6n(7XHG@bwja7VLr+y^1?s$n3Cu^p`%YG_KOchHhcq{`$5?-wQ3LG`Y3XMF z?nHQo5R+=$pb+Lv4S2wO&-s(hB1CO%MV{^Pea{cIOT(+xnkbo0ug9ngwzj|(qf-8~ zsL-AN4(ilXEsAWDE2-mW-fsVpcjmINSt6z#LO{_r{l6k^PBsS>UJY-qV8M|p5`*)k z+q)ZWSeIi2-?1foEsP@Bw9o|(=tF97p2^5|*1bH|4_=WQp{*oPP``o_%%3mU-Tk3g zZwV*11P?9+L+4cxV~ia05V@1q_!_rOb!TEj*uI0*rOY}5W-FNc`x0}@i_;app-u&i zO`b!f?Yqr03>uz-Dc~>{LKX#NZXl$amon z?v_>y?ZWt-U8DR3n9rcaOU80BDClPdX0xS}+6A69tD6Ry!cLgr?6EX7WwiXSI*rYt z=Cyndk2r;436w-F*?RSG#zyS6WYSh;6oJJK=_? zz87Jd0bmoDxkhaa5npciM;117oEHUCV9a6Fb00;1vv5R&cTaHk=G-K43mzwr!|XH*uZbFcw1YMReU=ww|o#{A$uR-yFOddWu&1rD90- zXqUmxN`^r58O5LP?6(tqt-GVwsxSbaB@=G&gU4bm>C9h9fNlgiHhjySeno5Ceqs>| zYJye!_6Sx`|JQ+F@|dZR8;1>K7QE`8g3saQ*Yw~{*`TpWIP>2)4b z{mX6bD?Qx6Sf%X_npfZ&XgOE>s1>7A;U&Lryjbst8{Q>lV>C8=P%h*B&e_-u`j#Bh z)mq+NM%|jWw2=e)>Yb)-9K3n1`cNEqt;f49dMIJUE@Iq4PWn2FbKn;1TCoAEi_{(V zbbMvnkC&#Z_s=hgRd)xTd9*?%sl~Pegrl4A zM*7W(Qb(zs)UO7kUgDdi zhswmBylVtagl3m$MlyX`Q2ZYMFnL!69z5f%Xl^=87Ftz&q<+J~@6R_*Uue$_HGkZ3 zdI~vs+(6UEG%IxTMZ)UBr z;WL0FwQ=Nv4Dr=29zeHWPzv1zUR#xKMl$3kWv=|@mG@+f3*BzQNE z*c^4;tbfDVrL6j=25BzHqNTzLn>3M{5~Hh68~%P{p@QhjC&Qt`ZM`me^bw_fFgWCs zG8z5LGrJ!-S6h!R3%SC`$V70GPDZK!9lwo5U@|olqF}<0B?<4?9}-^-K9ll=FnL>1 zB~;3Ua>C2LF|WkGoik&5QCOn#+U7Dcg`1NpL=NFyuWL+vZh2m#tjcNlUzzgKF$36x zfu{Bp>;h!c5n?_8@l-T;K2=cO1m4M4P+GGQNA?#A< zH3>wn;&zBqag+Y8)@w)}irL>8}q8Y%O2m&nw%|wN-pJGdpUO}eHJv$+_?~N zTf=o+f~gkkFD{nU7VU)YTZ9fYY13njw%#H^f%s|m>Bi9^O4|Sqb`yeM!q0SW_~X9A z=0s4XrXr3W4OZw~vqciE$XfTlOuI~f9G#cTT>kw@Zl5GCn6uYicxChVg9egJI?A7w zQ-_)m?v-kVO#(jrBXjoxd-Xej(Sso!pe)9Hj^wN~0FL#?mqkKukquS1fbWo}ty7Zj zk9UN_hplK1!dc!^dm?P1p2z!8lob<|%(4E>N9{cF$+YQT<2%3W1sz$giW>jDDxb*! z3283dOPd=vN+A6!gQidCo@oUh{=qy6R-D|#3_HgZ6{_tjHSM`!j*Ei zeaji)8Gr9Xu(_%fN_U8xg9adpeg(=A9i)7mpt;`YgMPL-sZ(pFvWLJ03T)to66TsR z{Gqcfzg#4BOBQK@6dr|iGH{eh%*OOTBv%oPA|*jFv;MUCqjAX=KscPj{b4l z#?o^;iyG+i@wni<-(VCS&qOUbfy^b{Pp1FhI(uZ9*yE`kKQ)3+9h)fB$!>wJs=AB` zJg1rTa03YqyZ)8l`OYn_AuOfS7()9KCZA24&{cA1tB~I;AUsCmS~h52aL4b`40mfQ z)14~DS*D|2P=W~;{$(8+j`TmFw>AT@B#R;)?yL*lR>Xh{t(PxvtYpJati48(j96_| z#7-%GwV09IHVzjiYVlPUFWI1{_mgV!-nzb{B=|-v9!gS{_no7EL6-J<%v~`ZtUu4< zBkr1u!o%eHP%^s`G$x--nh;3!O(x`Kzyl$a^|qj?hZ&T21!eiVdW(aq@!X%+uOikj z`LNt`(f20%a$f%0K@vUQl+^a$BP=E1F$^PB;K)xyMQAB^qYI1mMG$`>QUY@fLFy?o zOt@=Lg^(f@!TfqYGJDZHK7kPc%iAn49^sXJok-_ zNoHo?t+|iSrmW6q5te`#_S1CT*&LnR3JVL+9a71r)EL8r$e9u#bG(EMudZFs76fj6 zq)8Y~nmz$dEQ|w}uD(%E@?0^#L-`2i4iXeA!W1h)JHK-d5oI!zEZlnPJdW$Gd0c?E zD-Mgh%C|$JZqzxoz2;|~jbC|1|M59+5FA))gzFwgykb~I39#o}kX~JGKotkLjEGyk zTsAUcT`ba&XKEpOMjNjt@tE)8$`x#z+ydD#B>m>5Av0SL@jH2`GgRO9B-YTL9u5k9 z6}qyEKKDE6dqoPGFsI&W@u#XDMkM<>ew{I(4h@cl&!B-^4}#vl#f{uJL{6EGDvhu; zF2ulj(BQ0t+3DnWs~r?V?-oNQW4_wjO^c(QzZAT2{qgVBg2UHpDrgDI{@_NX*Cn1| zwCcOD{lM-zC>VF&aEM$Cmwh2m{M?xuh*3+8VCWYpAw#G!!-tY1g()15ew&u3O&XSZhSsoklc zrLX;A7T!@vL;K>S+NiF(QB+5Z4S|~+{=SG@C~0;8b%tYeV>hg_!$rXY1$D&?Grnnx zog9S@tvr;}tA4FD1+u(AXMpBCIGovdta)?STigo(Zz z-fej2dkf&lJ_)(ux}Gf)r2J`jLkLwP2|H<`RVC;Ut|*|frI&T z2Vn7K%iu&0`3aeWCkvw3>4C({Q&8P^xSKN4}eoY;?zlO*{D4M6olp%XF+dTa$-CYENJ|GFmVSh6g$H;)F$Ns ze$PRUmB4mgIk{gsN@6NyQY( z`abqbEmz)+Qf_9q1u$OP2l#)i7{CK#2*iS1x_4HV^`OF z5+(IQ`t;mlmfp@iQa$+Ak%j>ir|X0^YpekXcYEMzY^gv|J7rR)e!^7<}E_boyFGVAEKaR za_N1vR6)k=#`erM&qucL+2Tme^TLSQ)?GAu%RxcWBC;8_p-2NMW((gjTg|^1qBg(N z%G2}udhoU6a_bsqVw*ILFk;D1aYZQH=d1ewv+Cv7g6&DA>)O5R+oZnRuXPFeRX5sE z7OtbXO>vHgG94bJlq;Wef?i_F$@(yrZEUgnlM1(_*LZm9L3b4q6CvUKRd2gB9p*<( zcmygqlq1W$({P%)J>F=*b3NW(1UG6!tF9bQZD^IVz&Xn+ml*2+lyfgempPLjQ(f>l z6AC#%X41;yuQ_gCwG%^;dXD;?2&0V_#qOj2DcdSTK}8|XU|eyiNExvfAOYTB{!3P zCFp>=!PU|kHQ;xv9Nx>}@JHR-IH58HtF6LGTa9j^;_u^ohhI2EW#7um`b*f(-<7_d z=0FfcWQ#uIN}tnykoY*RgDKmLCkU1%hO?ehm2|0 z+vl7=b83$44pNn%9)tur7SKwcO)!$OO|qvE$&oO%VI+8}pa`dTv9UXJ)!K|e{tLBi z7nD?FqTK}{$2umJud*0Nd8ickOgg7KDzhD8OEG#sCfDoZwHx?O>Y~X&meo{Pv_~Xj z1gt+DB8=5ATyF@(dzyFlDMh00;~ASE*P&2<1;~yWi6No7ybF}`&+ZXIEHW@oUFwVe^pjTP zhK0UEpn#?#zuP54eM#WY%nJ53x_?C6E&vQnr0TI<=s9CSQ^KL~x#YEYu#-F;M>b}s zD-Y|KBE%=xb44#3qURyAP_15q%UX{03VpF$X)5Yku%whmZ(M~PC@j-X6HH)~j#GsJ zkLwHtVrjMe@i85Je=Ii5eK}1ceMZ<7K7oJ*v8OVW(z6FW>*~E#9b4tEw!BvzOf^K& zwo31&*DWC`1c9lLcrjl0&-eAYP{u{lSIj}uGAMEe)v>6_=MgutzUY#HabIhjPp%lQ zgn8c(sP>p;7y(JvgLQS%57A8luRJ}AIunlEpa#B^7Mu;=kNl>&rvHp4Y%f0Oqhk2U(P*vILS`l91#kF@z8hL?DDsz-UC)F(whprh>K5 zqM|GUB8vzLgQ5f!1>As6RInY8DHK5vu^?2WxpT0!eI92!Kfe3E=e+kU-*QeimqXXo zSf>F&kS4;Q@*oHUAO=$Z1bkv^{rM1t-N0q@Jz)w0di(=J$HvBBDueCi3zHeTJ(0TU z=-dmduCDIfuk!o@1w@3|s%VSZBX;vFm9a1Sr5%L>!j{hKN)qOh!DYOb-gMbOZr1#71xmP@o}uvW{lEhX|uUr(Gi7HaK6u zGu(|Cl#)z}jwXBi&`30<-h)Um=Y^`;lll13(NS=;;1Fs@tE0u97FYl6E!SI3;_m0= z7nBvdcz80_H-*A5Od!)Mt7@!;#RNKieaawc@7HCLC=8g)T-z!naM&aY)3_>_N@c?^ z<6eioZn_HW-)!%##-?W3A>s~yPogK=J|JUy)EcI3H@F>6WU;`%c$Q#x!U`y#{(Wmr zZJlXZ5Q)5eqU$6p#*G^a;Kt`eHi^j+{ljzC zX@2HKyLQLyW3afreSJY8;T*1CRtCY*Pm+;!^zp;3wUtf*yF%+OHEb*hq#+Dpc(f;j zvsOCkjs!Gc{^H=F!vuz(k6&QiemF8(q*Oj~@J}5c9(Llzkvuu|)lO1*+PbTu{pyQ# zAUPW2c^n@ULQxo%qsvYB-hO06I;b{kht~gP!Sc&zXleH}+F>iM9%X59(ZB~loArmj z6(GR}MnTmbQ1o*YRb#t`qi70hWP+lN%cC1n)DlHAQIr8l1s~nLWf4W)XVB+>Ml1{d zZ!N|I6rjo zRqFyUZ;a=rrVAG$jjWF+xjPm-y|&=|y!ODfq~}DCkyPzc+K4IC0^wJ{25=@cjvd#5 zf*?a|$;uK66px8lmv514Vft`=|4C#iv)XLi{cdXdwUgYiDWOQWs>Zel!}ZRYj&S%U zFA$pYNOCCr*%H=mIWe)p2NP*THgGt557B71iZ`CE;zFCwO`hzH#0j)oa^FtzAdT-5 z9#nn>tuTQw>M(@Wk^csDwEOHLI^XeV#-c~hykoz{KI-oBiaMgo-I=F1oM+kIwdfX; z;|oQgm6uZpHvWa;r82gYp^9#!Y25#72b+_d{Y+^E?N{+*>il<}B1rMNw|7=fy;i0e zU9a2@nWxQG@@AKs)|j>nU$;bVfrgD(8o z4}GO;gSsS(*3c)wPf_*QK5ammYve}b_%WYzoSZiv3-)J+R5x+$Ch?}qi8Y32;-_-@ zWV-oLnFrs{3LFZEX{llNl_MJ~(%u;`C*FD#l~+ote?0u_=-by%rnSd^2`(xRv(B{m z!}}BL*mZX0TNfQqtL82aDMCeCq%3*GI4{!CL}O+pi0y-1Xq5*DEy-7& zK299F!oUn1uL~pMG~@?Du+eKKh3X6N6SK@UhW1^Y*s#rz(LIuKb6q|Mi8OV%y`ld5`C&rJb!=gP_O^5e=>`Zf!+d*%J%g?g2c7s2VTJ z0Z!C3&R+d{IsaMNV6s zI8M9e#MAtv2h^Z7kH>*Tvl4f_(kaLy7mVj9h$XJWTzc h_tt;E+`3Za&Oh+XZ>&r`u@XEM5aP+9){zCN{{p#G-^>62 literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/settings/menu_layouts/dual_column.png b/admin/src/assets/images/settings/menu_layouts/dual_column.png new file mode 100644 index 0000000000000000000000000000000000000000..9b868cac6915d2784493b0792f18870820f17c5e GIT binary patch literal 514 zcmV+d0{#7oP)k(S^u+pLnXS z1G2mrm-5={`kzSGKnEH|cY%?h`=$GsNxIjWq=SKE0Heb|VvNq%I)hPl#sSZ|>mGRS z9pG7K5y!nlg!r4Ybwh;Ev3uuRXWvkD#sISJf`Q$-7w!VYZru?suhL1kbVN%hoph0N z{LS~L!!nGHf4)OkR|mrqhRV7^U1gmW>P&U2BkI^AnyTYiuxbZk;ix#?ROoDvTLN|q(DUvOk=*HBOFHWGF($AMKDj(5FhxM>J?hO|@ zUI)Wk3>$Pr-3FZzb;jvN-@4JaF0ggv5zW?d7a-5NxTfnUo$3m89&fm;<8^_kJHU;f zbUS~XlukP7PJgQ0y$=6kmlKQy-Np$1?FS@Vd6?;N^O8@`>07*qoM6N<$ Ef{6$HX8-^I literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/settings/menu_layouts/horizontal.png b/admin/src/assets/images/settings/menu_layouts/horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..ca779bc78ad5bf45e4b26e4e75e6481295383369 GIT binary patch literal 409 zcmV;K0cQS*P) zvbn(=z`lWa1n(axjo^q~;4t&bS@^%KH`vFc7`p2Q0I=EJ`>gf&x{iCB)gIPy+H$?# zI&Rh@j<_p{ds>?h{E0Z?i2HPk&-@4Ch&w}E!NafkSzG;R2LRxg$z)yzam_;Ieh_DF zDlv-FwX;E-%i6gQ+qsC>eh{Sfe@zd^TDtroDA!A5hii^ozT^kui2GyQ`9Fk7vFhZSNmiXsv;oD2I1{Wo zId{pb(`jB}R@*LWXIIUn~YTl0Pqhc#kP}ccEGly z+kj$coC&s_oYRhWw4>dQcKbJ=%iasfw_w`=03i1PMtw+J%=}$J00000NkvXXu0mjf DJf+bZ literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/settings/menu_layouts/mixed.png b/admin/src/assets/images/settings/menu_layouts/mixed.png new file mode 100644 index 0000000000000000000000000000000000000000..c82b58038fcbdab260ef4df85cd0bf53fe232ecf GIT binary patch literal 431 zcmV;g0Z{&lP)Yy$(XK&WqF=)bm5 z&;o+?zlB7C)K}fi`)JgA{>Jxlc!KXfgb6|jAx2Z&sveg*c(ShsoP#IVp>AiIo`88@ zr(tz#nx24pU$**en#2z4P8E+W)>TaYs*4?@TZnE@z)$QW={$9Pq+5t?5s4t(vUL8d zk>BD6MzrHvYjymk9q8Cd*CO3GT_5SB%UpMi1j=u9n@Hh+b*4E&dq@{atWMW0ODCOl z4eTM7=ZGc+P_FT6@YfY7)!;zX6{(>H2c9mqfCEieq?Y>qVa~er;Qc|m&m&qoqK^6@ zVBN$3Du2{%NO?+K9&mf!tI@d6TsrBbTaC_bh#7oGv|&;J<@f7bI_ad#=vP1p(n&`P zAWTV(SImLAJYY(!i%iy}>sF+bPPzt{A(q{UW(H7(@yY~TT`{;`%(2Y&+R=+kRYC}n ZKX+7z8kqwQRWJYm002ovPDHLkV1jqY&3ym> literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/settings/menu_layouts/vertical.png b/admin/src/assets/images/settings/menu_layouts/vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..16e942b0e9f3919eb199134692e32004544b280c GIT binary patch literal 439 zcmV;o0Z9IdP)(# zD2kaKe_n4u2iGeYpx=`+HyKd(2SbJL9}%}U{@8`CedQuyi89i?#7jp@$(zBsZj{qeqluEicf z*6Rj#SRgdoBZzCSqT|}D=pbE-bUD@21=xhdeC5x5a>aJJ;D*%S|aFn{?|8j z&_So{5lo1xtA1;g<9us`n+K#rx>Kg(1@+SDdjvb0&iU3z)8W<#y8VnK9qURScO3WW h%8Q3N0002Cig literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/settings/menu_styles/dark.png b/admin/src/assets/images/settings/menu_styles/dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e1653b7a657edd6e2e83b8b479bb1b4af4620e15 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^cYt^j3p0qNwsRej;t%i%asBk^tGc?@&!0c-?cF|q z{-!dsl@Ta)!PCVtB;(%On+JK17;v}*%3nHAmJs>!0B^+Y`oPQsN=8b26YP2SNGmV+ z(<1Wuz(Ft&-I2P^pJ{r#^(-^z!+)>dS^035-TET1?9HMwt^CXP@rda`@oSCvV)pRD9`Ic+V>RN|cZ9-Q}41PonSK>R4CZA96Tw@1diy fo(~{Er;M1%fByXW@#Dw4 zckfEI`&$F0PIK_}Tq&4}k`{J&n3>JYDATM6ZXdH1D1|rwka( a+00#Z%j6q-R9HLER}7x6elF{r5}E*&a(=A< literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/settings/menu_styles/light.png b/admin/src/assets/images/settings/menu_styles/light.png new file mode 100644 index 0000000000000000000000000000000000000000..3007b99ea227f5f203ad309d28358d95aa40aad2 GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^cYt^j3p0qNwsRej5(@AMasBk^>;M1%fByXW@#Dw4 zckhOVhNk`~V+6{c^K@|x$+-9SW+HE^0gvlLR`&CZGg~7o#6IM+GMaerh)K|XF7e~v z*2wU>xm~svV6cO!eAexM#~%n4hWb7dn;9*-?$zE?M-Ku)rRC!eF^lI+S&sX@aO&C)xGbBzUl*1H|~cH`T*7S|L8+xoqd^+pNbuX$T2rdEOtd33n=<}rH(r4Bw?X0 z19j9Zq$jx5yQq&=Rv*u@&uV zZv5wD2VJi=A#|57GsIK59E1=t&(Kn59-)r9I&_O8$G{R>i|A%Y2O}3yS41Z&E$H;i qY~iWsL1-c78CvShBh*n>SoaHFuY$So!<9_{0000$3b*O46wRy83EweL3E-p zTDPTC^?=rG?@;xC)#cYaJXSqGb<2^89-z9fBYo%rs(T&jp$Dk$Kk9+HS#|cPcrJon zbI(i6T+zM{&>wNHId#;{s7qL=Yd{@!)cq1mSEY~blI|(J>{oO}dReaMR_P^Q(MdlO z>i~~A@u&cbFmcTrMB15WsH1KUUF~df!d~+osw?&ewtya>4AqH=XPy0+o=3$lg6Eny zh_o}$P)FSyx+-_rpi9hVN3Lj_1$lsMGW5LKxX|5xOc$eay9h37-XPM>JVPCIbLeVE zkAX{UAEGNAI~aL@@(`VV>$4IoA-;gkJ&1+Cgm8pmlOdPn{^_RrAUFTa z?Nwrlv;qeX95`^`z<~n?4ji~TxhJFJb(>akZP2?200000NkvXXu0mjfdG+rO literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/svg/403.svg b/admin/src/assets/images/svg/403.svg new file mode 100644 index 0000000..68790ad --- /dev/null +++ b/admin/src/assets/images/svg/403.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/src/assets/images/svg/404.svg b/admin/src/assets/images/svg/404.svg new file mode 100644 index 0000000..48e1ca3 --- /dev/null +++ b/admin/src/assets/images/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/src/assets/images/svg/500.svg b/admin/src/assets/images/svg/500.svg new file mode 100644 index 0000000..512429f --- /dev/null +++ b/admin/src/assets/images/svg/500.svg @@ -0,0 +1,5 @@ + diff --git a/admin/src/assets/images/svg/login_icon.svg b/admin/src/assets/images/svg/login_icon.svg new file mode 100644 index 0000000..4beb3ab --- /dev/null +++ b/admin/src/assets/images/svg/login_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/src/assets/images/user/avatar.webp b/admin/src/assets/images/user/avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..6d7234b9eb1d4962efb9d01a4703fe4540d3ec34 GIT binary patch literal 2130 zcmV-Y2(9;0Nk&FW2mkB(|iQh=J6I zyxsF})jWZrx1zAfq`;31uOrtG^aslZ++-*oHbA=Ws=r@BH$nEcT>f}v8c<1MaH|2|TsFVAnjLEj8 z)v3~&1Tg$gPbv>1OlgM(i_=4U(n13#54;Dp%@xYc7c42<_;HI{&blw#e zNxIkz>nHOh3a&_eJ&|2SelyaGrVT5H-&xr3qchs0k*P(yrJZP27PwoGyrzP(HQNq) zdnp%e(8+0Kvd=#P@!j@e@d-;H6ynU*1u2;INSHcM)CHM>4FQx&W>lUOt}HM!x%Fka z%X5GL{_uBtT#(I|h-+J>-VdN=0k`87mNLbrC!e0hz)eum&xzb_(qj;)6C_`RprQp+gHMV1x!s=;2Rb%2PsYfg* zh??Ve&~jKa^pwSDNm#|Es3PLmb7sTuZ(x2P^RA`Y&UPQ||EZaGfbNJ@(YlYt9%HYh zZd6LrWC~-IbvBzuEVN?AAJR!ig~P0-MlXvR7Z7bx7?0-n<*4=SW=S5*A~>8h3`T5! z`I{Rw5T_p0VkFU17Z&8dG9Tw3V-0ioX90$ba-jE=Z$T+Ka@XHj^&Ah5&3^#B4xzV6Zt7h5hW%IMfcreWr(GyA?@Q(v8ZjmLibkv4Aj2n8A)< z=`*~B`m=?dsA{+ox>qZp7zsL(cLHfKRXL9|zDz?~SSeGwu!UZyhdt^PsU~45yW9Oj z|7T$>K9wSeG)6;}llOZ5%}(}z0=hks13|B$4L31VdOF*sd@YU?jiH_PWPK9UZtNa*gq3V&9l+*yS!e&SOs`1NHt-i^1q)x3)% z>It^qg|*?RqvUXDdRScQn|t^Lu)KXY5VsWF%#?Kcx(iSN8x!^6ce+4JmX87y6y?wZZ4&b%iwh!QfdXkYoDFzcx@r36&& zo2APee^J><5DkZ2e=>DFvmAxNih)1iwKOrXu;mfGI%PS5k!rJn1B3wHU(p$^yBEHx zq~i|&7X}r0s20A5acrQR9#hvK2Xx=6c*tFM@J9UuD8ohc`RLDsu}deGD`*`c;m8QS zgCUifRD^>%<4S8#^2TMADqw+gjHZ>sulGW1(}gDKwF2hjvQB>n-jtmz#9@~4F*^x@%R}e{Y2&KCz?duu^aa>y%iPm%1zqnPR{G?Y4 z48C*_3ipwVb2oo`j1WM8AYDT&ioo^DN+XA-8Nq=KolVDJ>^(q9Nykz^AO8@jU-TDL8=@Yp# z`0V^~J;sb-xt;21tD-A@Q!b4`@x|E>D83vGJA5%XgyFaLGshP4j^0BP@nJi;8QPq} zZasCOfyHw%T^l_D156Rxq9l?k^_qF>+c(_`j%kBrqB>?FPa!(O&K%uAjw|3vex zg~6iGGvYe?pO(JYfzZ{%9l2^x{GmywWC-Ereh^T;KWV6K)&<>!={pX|l+g~?h}`RK z0g*I#Xa`GBWma{trr5d?fk)++ZmoO}fx6-Yk^Ncr>2Umkn-MFU(O?_Y9peO6vtnRF z$QVFE%8MY|Xohq7+4+t3Y-1=b`+7f0p6}a*tiScRyO=V+;Q3*I3IM{IKduC`j8lKO I28@6J0QOxWo&W#< literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/user/bg.webp b/admin/src/assets/images/user/bg.webp new file mode 100644 index 0000000000000000000000000000000000000000..762b22dbbe34305886a78b1d103dddcc61d106ec GIT binary patch literal 12352 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 literal 0 HcmV?d00001 diff --git a/admin/src/assets/styles/core/app.scss b/admin/src/assets/styles/core/app.scss new file mode 100644 index 0000000..c0efeed --- /dev/null +++ b/admin/src/assets/styles/core/app.scss @@ -0,0 +1,292 @@ +// 全局样式 +// 顶部进度条颜色 +#nprogress .bar { + z-index: 2400; + background-color: color-mix(in srgb, var(--theme-color) 70%, white); +} + +#nprogress .peg { + box-shadow: + 0 0 10px var(--theme-color), + 0 0 5px var(--theme-color) !important; +} + +#nprogress .spinner-icon { + border-top-color: var(--theme-color) !important; + border-left-color: var(--theme-color) !important; +} + +// 处理移动端组件兼容性 +@media screen and (max-width: 640px) { + * { + cursor: default !important; + } +} + +// 背景滤镜 +*, +::before, +::after { + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +// 色弱模式 +.color-weak { + filter: invert(80%); + -webkit-filter: invert(80%); +} + +#noop { + display: none; +} + +// 语言切换选中样式 +.langDropDownStyle { + // 选中项背景颜色 + .is-selected { + background-color: var(--art-el-active-color) !important; + } + + // 语言切换按钮菜单样式优化 + .lang-btn-item { + .el-dropdown-menu__item { + padding-left: 13px !important; + padding-right: 6px !important; + margin-bottom: 3px !important; + } + + &:last-child { + .el-dropdown-menu__item { + margin-bottom: 0 !important; + } + } + + .menu-txt { + min-width: 60px; + display: block; + } + + i { + font-size: 10px; + margin-left: 10px; + } + } +} + +// 盒子默认边框 +.page-content { + border: 1px solid var(--art-card-border) !important; +} + +@mixin art-card-base($border-color, $shadow: none, $radius-diff: 4px) { + background: var(--default-box-color); + border: 1px solid #{$border-color} !important; + border-radius: calc(var(--custom-radius) + #{$radius-diff}) !important; + box-shadow: #{$shadow} !important; + + --el-card-border-color: var(--default-border) !important; +} + +.art-card, +.art-card-sm, +.art-card-xs { + border: 1px solid var(--art-card-border); +} + +// 盒子边框 +[data-box-mode='border-mode'] { + .page-content, + .art-table-card { + border: 1px solid var(--art-card-border) !important; + } + + .art-card { + @include art-card-base(var(--art-card-border), none, 4px); + } + + .art-card-sm { + @include art-card-base(var(--art-card-border), none, 0px); + } + + .art-card-xs { + @include art-card-base(var(--art-card-border), none, -4px); + } +} + +// 盒子阴影 +[data-box-mode='shadow-mode'] { + .page-content, + .art-table-card { + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.04) !important; + border: 1px solid var(--art-gray-200) !important; + } + + .layout-sidebar { + border-right: 1px solid var(--art-card-border) !important; + } + + .art-card { + @include art-card-base( + var(--art-gray-200), + (0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)), + 4px + ); + } + + .art-card-sm { + @include art-card-base( + var(--art-gray-200), + (0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)), + 2px + ); + } + + .art-card-xs { + @include art-card-base( + var(--art-gray-200), + (0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 1px -1px rgba(0, 0, 0, 0.08)), + -4px + ); + } +} + +// 元素全屏 +.el-full-screen { + position: fixed; + top: 0; + left: 0; + right: 0; + width: 100vw !important; + height: 100% !important; + z-index: 2300; + margin-top: 0; + padding: 15px; + box-sizing: border-box; + background-color: var(--default-box-color); + display: flex; + flex-direction: column; +} + +// 表格卡片 +.art-table-card { + flex: 1; + display: flex; + flex-direction: column; + margin-top: 12px; + border-radius: calc(var(--custom-radius) / 2 + 2px) !important; + + .el-card__body { + height: 100%; + overflow: hidden; + } +} + +// 容器全高 +.art-full-height { + height: var(--art-full-height); + display: flex; + flex-direction: column; + + @media (max-width: 640px) { + height: auto; + } +} + +// 徽章样式 +.art-badge { + position: absolute; + top: 0; + right: 20px; + bottom: 0; + width: 6px; + height: 6px; + margin: auto; + background: #ff3860; + border-radius: 50%; + animation: breathe 1.5s ease-in-out infinite; + + &.art-badge-horizontal { + right: 0; + } + + &.art-badge-mixed { + right: 0; + } + + &.art-badge-dual { + right: 5px; + top: 5px; + bottom: auto; + } +} + +// 文字徽章样式 +.art-text-badge { + position: absolute; + top: 0; + right: 12px; + bottom: 0; + min-width: 20px; + height: 18px; + line-height: 17px; + padding: 0 5px; + margin: auto; + font-size: 10px; + color: #fff; + text-align: center; + background: #fd4e4e; + border-radius: 4px; +} + +@keyframes breathe { + 0% { + opacity: 0.7; + transform: scale(1); + } + + 50% { + opacity: 1; + transform: scale(1.1); + } + + 100% { + opacity: 0.7; + transform: scale(1); + } +} + +// 修复老机型 loading 定位问题 +.art-loading-fix { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +.art-loading-fix .el-loading-spinner { + position: static !important; + top: auto !important; + left: auto !important; + transform: none !important; +} + +// 去除移动端点击背景色 +@media screen and (max-width: 1180px) { + * { + -webkit-tap-highlight-color: transparent; + } +} diff --git a/admin/src/assets/styles/core/dark.scss b/admin/src/assets/styles/core/dark.scss new file mode 100644 index 0000000..c52abc3 --- /dev/null +++ b/admin/src/assets/styles/core/dark.scss @@ -0,0 +1,93 @@ +/* +* 深色主题 +* 单页面移除深色主题 document.getElementsByTagName("html")[0].removeAttribute('class') +*/ + +$font-color: rgba(#ffffff, 0.85); + +/* 覆盖element-plus默认深色背景色 */ +html.dark { + // element-plus + --el-bg-color: var(--default-box-color); + --el-text-color-regular: #{$font-color}; + + // 富文本编辑器 + // 工具栏背景颜色 + --w-e-toolbar-bg-color: #18191c; + // 输入区域背景颜色 + --w-e-textarea-bg-color: #090909; + // 工具栏文字颜色 + --w-e-toolbar-color: var(--art-gray-600); + // 选中菜单颜色 + --w-e-toolbar-active-bg-color: #25262b; + // 弹窗边框颜色 + --w-e-toolbar-border-color: var(--default-border-dashed); + // 分割线颜色 + --w-e-textarea-border-color: var(--default-border-dashed); + // 链接输入框边框颜色 + --w-e-modal-button-border-color: var(--default-border-dashed); + // 表格头颜色 + --w-e-textarea-slight-bg-color: #090909; + // 按钮背景颜色 + --w-e-modal-button-bg-color: #090909; + // hover toolbar 背景颜色 + --w-e-toolbar-active-color: var(--art-gray-800); +} + +.dark { + .page-content .article-list .item .left .outer > div { + border-right-color: var(--dark-border-color) !important; + } + + // 富文本编辑器 + .editor-wrapper { + *:not(pre code *) { + color: inherit !important; + } + } + // 分隔线 + .w-e-bar-divider { + background-color: var(--art-gray-300) !important; + } + + .w-e-select-list, + .w-e-drop-panel, + .w-e-bar-item-group .w-e-bar-item-menus-container, + .w-e-text-container [data-slate-editor] pre > code { + border: 1px solid var(--default-border) !important; + } + + // 下拉选择框 + .w-e-select-list { + background-color: var(--default-box-color) !important; + } + + /* 下拉选择框 hover 样式调整 */ + .w-e-select-list ul li:hover, + /* 工具栏 hover 按钮背景颜色 */ + .w-e-bar-item button:hover { + background-color: #090909 !important; + } + + /* 代码块 */ + .w-e-text-container [data-slate-editor] pre > code { + background-color: #25262b !important; + text-shadow: none !important; + } + + /* 引用 */ + .w-e-text-container [data-slate-editor] blockquote { + border-left: 4px solid var(--default-border-dashed) !important; + background-color: var(--art-color); + } + + .editor-wrapper { + .w-e-text-container [data-slate-editor] .table-container th:last-of-type { + border-right: 1px solid var(--default-border-dashed) !important; + } + + .w-e-modal { + background-color: var(--art-color); + } + } +} diff --git a/admin/src/assets/styles/core/el-dark.scss b/admin/src/assets/styles/core/el-dark.scss new file mode 100644 index 0000000..8f81cdf --- /dev/null +++ b/admin/src/assets/styles/core/el-dark.scss @@ -0,0 +1,2 @@ +// 导入暗黑主题 +@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *; diff --git a/admin/src/assets/styles/core/el-light.scss b/admin/src/assets/styles/core/el-light.scss new file mode 100644 index 0000000..ddf2bc5 --- /dev/null +++ b/admin/src/assets/styles/core/el-light.scss @@ -0,0 +1,34 @@ +// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss +// 自定义Element 亮色主题 + +@forward 'element-plus/theme-chalk/src/common/var.scss' with ( + $colors: ( + 'white': #ffffff, + 'black': #000000, + 'success': ( + 'base': #13deb9 + ), + 'warning': ( + 'base': #ffae1f + ), + 'danger': ( + 'base': #ff4d4f + ), + 'error': ( + 'base': #fa896b + ) + ), + $button: ( + 'hover-bg-color': var(--el-color-primary-light-9), + 'hover-border-color': var(--el-color-primary), + 'border-color': var(--el-color-primary), + 'text-color': var(--el-color-primary) + ), + $messagebox: ( + 'border-radius': '12px' + ), + $popover: ( + 'padding': '14px', + 'border-radius': '10px' + ) +); diff --git a/admin/src/assets/styles/core/el-ui.scss b/admin/src/assets/styles/core/el-ui.scss new file mode 100644 index 0000000..44429a1 --- /dev/null +++ b/admin/src/assets/styles/core/el-ui.scss @@ -0,0 +1,519 @@ +// 优化 Element Plus 组件库默认样式 + +:root { + // 系统主色 + --main-color: var(--el-color-primary); + --el-color-white: white !important; + --el-color-black: white !important; + // 输入框边框颜色 + // --el-border-color: #E4E4E7 !important; // DCDFE6 + // 按钮粗度 + --el-font-weight-primary: 400 !important; + + --el-component-custom-height: 36px !important; + + --el-component-size: var(--el-component-custom-height) !important; + + // 边框、按钮圆角... + --el-border-radius-base: calc(var(--custom-radius) / 3 + 2px) !important; + + --el-border-radius-small: calc(var(--custom-radius) / 3 + 4px) !important; + --el-messagebox-border-radius: calc(var(--custom-radius) / 3 + 4px) !important; + --el-popover-border-radius: calc(var(--custom-radius) / 3 + 4px) !important; + + .region .el-radio-button__original-radio:checked + .el-radio-button__inner { + color: var(--theme-color); + } +} + +// 优化 el-form-item 标签高度 +.el-form-item__label { + height: var(--el-component-custom-height) !important; + line-height: var(--el-component-custom-height) !important; +} + +// 日期选择器 +.el-date-range-picker { + --el-datepicker-inrange-bg-color: var(--art-gray-200) !important; +} + +// el-card 背景色跟系统背景色保持一致 +html.dark .el-card { + --el-card-bg-color: var(--default-box-color) !important; +} + +// 修改 el-pagination 大小 +.el-pagination--default { + & { + --el-pagination-button-width: 32px !important; + --el-pagination-button-height: var(--el-pagination-button-width) !important; + } + + @media (max-width: 1180px) { + & { + --el-pagination-button-width: 28px !important; + } + } + + .el-select--default .el-select__wrapper { + min-height: var(--el-pagination-button-width) !important; + } + + .el-pagination__jump .el-input { + height: var(--el-pagination-button-width) !important; + } +} + +.el-pager li { + padding: 0 10px !important; + // border: 1px solid red !important; +} + +// 优化菜单折叠展开动画(提升动画流畅度) +.el-menu.el-menu--inline { + transition: max-height 0.26s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +// 优化菜单 item hover 动画(提升鼠标跟手感) +.el-sub-menu__title, +.el-menu-item { + transition: background-color 0s !important; +} + +// -------------------------------- 修改 el-size=default 组件默认高度 start -------------------------------- +// 修改 el-button 高度 +.el-button--default { + height: var(--el-component-custom-height) !important; +} + +// circle 按钮宽度优化 +.el-button--default.is-circle { + width: var(--el-component-custom-height) !important; +} + +// 修改 el-select 高度 +.el-select--default { + .el-select__wrapper { + min-height: var(--el-component-custom-height) !important; + } +} + +// 修改 el-checkbox-button 高度 +.el-checkbox-button--default .el-checkbox-button__inner, +// 修改 el-radio-button 高度 +.el-radio-button--default .el-radio-button__inner { + padding: 10px 15px !important; +} +// -------------------------------- 修改 el-size=default 组件默认高度 end -------------------------------- + +.el-pagination.is-background .btn-next, +.el-pagination.is-background .btn-prev, +.el-pagination.is-background .el-pager li { + border-radius: 6px; +} + +.el-popover { + min-width: 80px; + border-radius: var(--el-border-radius-small) !important; +} + +.el-dialog { + border-radius: 100px !important; + border-radius: calc(var(--custom-radius) / 1.2 + 2px) !important; + overflow: hidden; +} + +.el-dialog__header { + .el-dialog__title { + font-size: 16px; + } +} + +.el-dialog__body { + padding: 25px 0 !important; + position: relative; // 为了兼容 el-pagination 样式,需要设置 relative,不然会影响 el-pagination 的样式,比如 el-pagination__jump--small 会被影响,导致 el-pagination__jump--small 按钮无法点击,详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275; +} + +.el-dialog.el-dialog-border { + .el-dialog__body { + // 上边框 + &::before, + // 下边框 + &::after { + content: ''; + position: absolute; + left: -16px; + width: calc(100% + 32px); + height: 1px; + background-color: var(--art-gray-300); + } + + &::before { + top: 0; + } + + &::after { + bottom: 0; + } + } +} + +// el-message 样式优化 +.el-message { + background-color: var(--default-box-color) !important; + border: 0 !important; + box-shadow: + 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 9px 28px 8px rgba(0, 0, 0, 0.05) !important; + + p { + font-size: 13px; + } +} + +// 修改 el-dropdown 样式 +.el-dropdown-menu { + padding: 6px !important; + border-radius: 10px !important; + border: none !important; + + .el-dropdown-menu__item { + padding: 6px 16px !important; + border-radius: 6px !important; + + &:hover:not(.is-disabled) { + color: var(--art-gray-900) !important; + background-color: var(--art-el-active-color) !important; + } + + &:focus:not(.is-disabled) { + color: var(--art-gray-900) !important; + background-color: var(--art-gray-200) !important; + } + } +} + +// 隐藏 select、dropdown 的三角 +.el-select__popper, +.el-dropdown__popper { + margin-top: -6px !important; + + .el-popper__arrow { + display: none; + } +} + +.el-dropdown-selfdefine:focus { + outline: none !important; +} + +// 处理移动端组件兼容性 +@media screen and (max-width: 640px) { + .el-message-box, + .el-dialog { + width: calc(100% - 24px) !important; + } + + .el-date-picker.has-sidebar.has-time { + width: calc(100% - 24px); + left: 12px !important; + } + + .el-picker-panel *[slot='sidebar'], + .el-picker-panel__sidebar { + display: none; + } + + .el-picker-panel *[slot='sidebar'] + .el-picker-panel__body, + .el-picker-panel__sidebar + .el-picker-panel__body { + margin-left: 0; + } +} + +// 修改el-button样式 +.el-button { + &.el-button--text { + background-color: transparent !important; + padding: 0 !important; + + span { + margin-left: 0 !important; + } + } +} + +// 修改el-tag样式 +.el-tag { + font-weight: 500; + transition: all 0s !important; + + &.el-tag--default { + height: 26px !important; + } +} + +.el-checkbox-group { + &.el-table-filter__checkbox-group label.el-checkbox { + height: 17px !important; + + .el-checkbox__label { + font-weight: 400 !important; + } + } +} + +.el-radio--default { + // 优化单选按钮大小 + .el-radio__input { + .el-radio__inner { + width: 16px; + height: 16px; + + &::after { + width: 6px; + height: 6px; + } + } + } +} + +.el-checkbox { + .el-checkbox__inner { + border-radius: 2px !important; + } +} + +// 优化复选框样式 +.el-checkbox--default { + .el-checkbox__inner { + width: 16px !important; + height: 16px !important; + border-radius: 4px !important; + + &::before { + content: ''; + height: 4px !important; + top: 5px !important; + background-color: #fff !important; + transform: scale(0.6) !important; + } + } + + .is-checked { + .el-checkbox__inner { + &::after { + width: 3px; + height: 8px; + margin: auto; + border: 2px solid var(--el-checkbox-checked-icon-color); + border-left: 0; + border-top: 0; + transform: translate(-45%, -60%) rotate(45deg) scale(0.86) !important; + transform-origin: center; + } + } + } +} + +.el-notification .el-notification__icon { + font-size: 22px !important; +} + +// 修改 el-message-box 样式 +.el-message-box__headerbtn .el-message-box__close, +.el-dialog__headerbtn .el-dialog__close { + top: 7px; + right: 7px; + width: 30px; + height: 30px; + border-radius: 5px; + transition: all 0.3s; + + &:hover { + background-color: var(--art-hover-color) !important; + color: var(--art-gray-900) !important; + } +} + +.el-message-box { + padding: 25px 20px !important; +} + +.el-message-box__title { + font-weight: 500 !important; +} + +.el-table__column-filter-trigger i { + color: var(--theme-color) !important; + margin: -3px 0 0 2px; +} + +// 去除 el-dropdown 鼠标放上去出现的边框 +.el-tooltip__trigger:focus-visible { + outline: unset; +} + +// ipad 表单右侧按钮优化 +@media screen and (max-width: 1180px) { + .el-table-fixed-column--right { + padding-right: 0 !important; + } +} + +.login-out-dialog { + padding: 30px 20px !important; + border-radius: 10px !important; +} + +// 修改 dialog 动画 +.dialog-fade-enter-active { + .el-dialog:not(.is-draggable) { + animation: dialog-open 0.3s cubic-bezier(0.32, 0.14, 0.15, 0.86); + + // 修复 el-dialog 动画后宽度不自适应问题 + .el-select__selected-item { + display: inline-block; + } + } +} + +.dialog-fade-leave-active { + animation: fade-out 0.2s linear; + + .el-dialog:not(.is-draggable) { + animation: dialog-close 0.5s; + } +} + +@keyframes dialog-open { + 0% { + opacity: 0; + transform: scale(0.2); + } + + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes dialog-close { + 0% { + opacity: 1; + transform: scale(1); + } + + 100% { + opacity: 0; + transform: scale(0.2); + } +} + +// 遮罩层动画 +@keyframes fade-out { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +// 修改 el-select 样式 +.el-select__popper:not(.el-tree-select__popper) { + .el-select-dropdown__list { + padding: 5px !important; + + .el-select-dropdown__item { + height: 34px !important; + line-height: 34px !important; + border-radius: 6px !important; + + &.is-selected { + color: var(--art-gray-900) !important; + font-weight: 400 !important; + background-color: var(--art-el-active-color) !important; + margin-bottom: 4px !important; + } + + &:hover { + background-color: var(--art-hover-color) !important; + } + } + + .el-select-dropdown__item:hover ~ .is-selected, + .el-select-dropdown__item.is-selected:has(~ .el-select-dropdown__item:hover) { + background-color: transparent !important; + } + } +} + +// 修改 el-tree-select 样式 +.el-tree-select__popper { + .el-select-dropdown__list { + padding: 5px !important; + + .el-tree-node { + .el-tree-node__content { + height: 36px !important; + border-radius: 6px !important; + + &:hover { + background-color: var(--art-gray-200) !important; + } + } + } + } +} + +// 实现水波纹在文字下面效果 +.el-button > span { + position: relative; + z-index: 10; +} + +// 优化颜色选择器圆角 +.el-color-picker__color { + border-radius: 2px !important; +} + +// 优化日期时间选择器底部圆角 +.el-picker-panel { + .el-picker-panel__footer { + border-radius: 0 0 var(--el-border-radius-base) var(--el-border-radius-base); + } +} + +// 优化树型菜单样式 +.el-tree-node__content { + border-radius: 4px; + margin-bottom: 4px; + padding: 1px 0; + + &:hover { + background-color: var(--art-hover-color) !important; + } +} + +.dark { + .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content { + background-color: var(--art-gray-300) !important; + } +} + +// 隐藏折叠菜单弹窗 hover 出现的边框 +.menu-left-popper:focus-within, +.horizontal-menu-popper:focus-within { + box-shadow: none !important; + outline: none !important; +} + +// 数字输入组件右侧按钮高度跟随自定义组件高度 +.el-input-number--default.is-controls-right { + .el-input-number__decrease, + .el-input-number__increase { + height: calc((var(--el-component-size) / 2)) !important; + } +} diff --git a/admin/src/assets/styles/core/md.scss b/admin/src/assets/styles/core/md.scss new file mode 100644 index 0000000..b22fdc2 --- /dev/null +++ b/admin/src/assets/styles/core/md.scss @@ -0,0 +1,1036 @@ +/* 文章标题设置(h1-h6)*/ +/* ------------------------------------------------ */ +$font-color: #24292e; + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + color: var(--art-gray-800) !important; + margin: 30px 0 10px 0; + font-weight: 600; +} + +.markdown-body h1 { + font-size: 30px; +} + +@media only screen and (max-width: 550px) { + .markdown-body h1 { + font-size: 26px; + } + + .markdown-body h2 { + font-size: 22px; + } + + .markdown-body h3 { + font-size: 18px; + } +} + +/* 块引用 */ +/* ------------------------------------------------ */ +.markdown-body blockquote { + color: rgba(60, 60, 67, 0.7); + font-size: 15px !important; + border-left: 0.18em solid #e7e7e8; + background: #f8f8f8; + padding: 15px 1em; + font-weight: 400 !important; +} + +/* 详情页文章字体颜色 */ +/* ------------------------------------------------ */ +.markdown-body p { + line-height: 28px; + margin-bottom: 10px; +} + +.markdown-body li, +.markdown-body p { + color: var(--art-gray-800) !important; + font-size: 16px !important; +} + +.dark .markdown-body li span { + color: var(--art-gray-800) !important; + background-color: transparent !important; +} + +.dark .markdown-body p span { + color: var(--art-gray-800) !important; + background-color: transparent !important; +} + +.line-numbers-mode { + background-color: var(--art-code-bg); + border-radius: 8px; + position: relative; + padding-left: 32px; + box-sizing: border-box; +} + +.line-numbers-mode pre { + flex: 1; + border-radius: 0 8px 8px 0; + background-color: var(--art-code-bg); +} + +.line-numbers-mode .line-numbers-wrapper { + width: 32px; + height: 100%; + text-align: center; + padding: 16px 0; + box-sizing: border-box; + border-right: 1px solid #000000; + position: absolute; + left: 0; + top: 0; +} + +.line-numbers-mode .line-numbers-wrapper span { + height: 23.6px; + line-height: 23.6px; + display: block; + color: #72747b; + font-size: 13px; + box-sizing: border-box; +} + +.line-numbers-mode .copy-btn { + display: inline-block; + display: flex; + position: absolute; + right: 10px; + top: 10px; + cursor: pointer; + opacity: 0; + background-color: #000; + border-radius: 5px; + text-align: center; + color: rgba(255, 255, 255, 0.6); + transition: opacity 0.3s; +} + +.line-numbers-mode .copy-btn div { + width: 34px; + height: 34px; + line-height: 34px; + cursor: pointer; + text-align: center; + font-size: 20px; +} + +.line-numbers-mode:hover .copy-btn { + opacity: 1; +} + +.line-numbers-mode .copy-btn span { + height: 34px; + line-height: 34px; + font-size: 13px; + padding-left: 10px; + display: none; +} + +.line-numbers-mode .copy-btn .show-copy { + opacity: 1; + display: block; +} + +.line-numbers-mode ::-webkit-scrollbar-track { + background-color: #292b30 !important; +} + +.markdown-body .anchor { + float: left; + line-height: 1; + margin-left: -20px; + padding-right: 4px; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #1b1f23; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; +} + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + line-height: 1.5; + color: $font-color; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body details { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body a { + background-color: initial; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline-width: 0; +} + +.markdown-body strong { + font-weight: inherit; + font-weight: bolder; +} + +.markdown-body p br { + display: inline; + line-height: 11px; +} + +.markdown-body img { + border-style: none; +} + +.markdown-body hr { + box-sizing: initial; + height: 0; + overflow: visible; +} + +.markdown-body input { + font: inherit; + margin: 0; +} + +.markdown-body input { + overflow: visible; +} + +.markdown-body [type='checkbox'] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body * { + box-sizing: border-box; +} + +.markdown-body input { + font-size: inherit; + line-height: inherit; +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body strong { + font-weight: 600; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #dfe2e5; +} + +.markdown-body hr:after, +.markdown-body hr:before { + display: table; + content: ''; +} + +.markdown-body hr:after { + clear: both; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: + 11px SFMono-Regular, + Consolas, + Liberation Mono, + Menlo, + monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: 1px solid #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ol, +.markdown-body ul { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ol ol ol, +.markdown-body ol ul ol, +.markdown-body ul ol ol, +.markdown-body ul ul ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code, +.markdown-body pre, +.markdown-body .line-number { + font-size: 14px !important; + border-radius: 8px; + background-color: #282c34; +} + +.dark { + .markdown-body code, + .markdown-body pre, + .markdown-body .line-number { + background-color: #252525; + } +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body input::-webkit-inner-spin-button, +.markdown-body input::-webkit-outer-spin-button { + margin: 0; + -webkit-appearance: none; + appearance: none; +} + +.markdown-body :checked + .radio-label { + position: relative; + z-index: 1; + border-color: #0366d6; +} + +.markdown-body .border { + border: 1px solid #e1e4e8 !important; +} + +.markdown-body .border-0 { + border: 0 !important; +} + +.markdown-body .border-bottom { + border-bottom: 1px solid #e1e4e8 !important; +} + +.markdown-body .rounded-1 { + border-radius: 3px !important; +} + +.markdown-body .bg-white { + background-color: #fff !important; +} + +.markdown-body .bg-gray-light { + background-color: #fafbfc !important; +} + +.markdown-body .text-gray-light { + color: #6a737d !important; +} + +.markdown-body .mb-0 { + margin-bottom: 0 !important; +} + +.markdown-body .my-2 { + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .py-2 { + padding-top: 8px !important; + padding-bottom: 8px !important; +} + +.markdown-body .pl-3, +.markdown-body .px-3 { + padding-left: 16px !important; +} + +.markdown-body .px-3 { + padding-right: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body .f6 { + font-size: 12px !important; +} + +.markdown-body .lh-condensed { + line-height: 1.25 !important; +} + +.markdown-body .text-bold { + font-weight: 600 !important; +} + +.markdown-body .pl-c { + color: #6a737d; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #005cc5; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #6f42c1; +} + +.markdown-body .pl-s .pl-s1, +.markdown-body .pl-smi { + color: $font-color; +} + +.markdown-body .pl-ent { + color: #22863a; +} + +.markdown-body .pl-k { + color: #d73a49; +} + +.markdown-body .pl-pds, +.markdown-body .pl-s, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sra, +.markdown-body .pl-sr .pl-sre { + color: #032f62; +} + +.markdown-body .pl-smw, +.markdown-body .pl-v { + color: #e36209; +} + +.markdown-body .pl-bu { + color: #b31d28; +} + +.markdown-body .pl-ii { + color: #fafbfc; + background-color: #b31d28; +} + +.markdown-body .pl-c2 { + color: #fafbfc; + background-color: #d73a49; +} + +.markdown-body .pl-c2:before { + content: '^M'; +} + +.markdown-body .pl-sr .pl-cce { + font-weight: 700; + color: #22863a; +} + +.markdown-body .pl-ml { + color: #735c0f; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: 700; + color: #005cc5; +} + +.markdown-body .pl-mi { + font-style: italic; + color: $font-color; +} + +.markdown-body .pl-mb { + font-weight: 700; + color: $font-color; +} + +.markdown-body .pl-md { + color: #b31d28; + background-color: #ffeef0; +} + +.markdown-body .pl-mi1 { + color: #22863a; + background-color: #f0fff4; +} + +.markdown-body .pl-mc { + color: #e36209; + background-color: #ffebda; +} + +.markdown-body .pl-mi2 { + color: #f6f8fa; + background-color: #005cc5; +} + +.markdown-body .pl-mdr { + font-weight: 700; + color: #6f42c1; +} + +.markdown-body .pl-ba { + color: #586069; +} + +.markdown-body .pl-sg { + color: #959da5; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #032f62; +} + +.markdown-body .mb-0 { + margin-bottom: 0 !important; +} + +.markdown-body .my-2 { + margin-bottom: 8px !important; +} + +.markdown-body .my-2 { + margin-top: 8px !important; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .py-2 { + padding-top: 8px !important; + padding-bottom: 8px !important; +} + +.markdown-body .pl-3 { + padding-left: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body .pl-7 { + padding-left: 48px !important; +} + +.markdown-body .pl-8 { + padding-left: 64px !important; +} + +.markdown-body .pl-9 { + padding-left: 80px !important; +} + +.markdown-body .pl-10 { + padding-left: 96px !important; +} + +.markdown-body .pl-11 { + padding-left: 112px !important; +} + +.markdown-body .pl-12 { + padding-left: 128px !important; +} + +.markdown-body hr { + border-bottom-color: #eee; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: + 11px SFMono-Regular, + Consolas, + Liberation Mono, + Menlo, + monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: 1px solid #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; +} + +.markdown-body:after, +.markdown-body:before { + display: table; + content: ''; +} + +.markdown-body:after { + clear: both; +} + +.markdown-body > :first-child { + margin-top: 0 !important; +} + +.markdown-body > :last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body blockquote, +.markdown-body details, +.markdown-body dl, +.markdown-body ol, +.markdown-body pre, +.markdown-body table, +.markdown-body ul { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; +} + +.markdown-body blockquote > :first-child { + margin-top: 0; +} + +.markdown-body blockquote > :last-child { + margin-bottom: 0; +} + +.markdown-body ol, +.markdown-body ul { + padding-left: 1em; +} + +.markdown-body ol ol, +.markdown-body ol ul, +.markdown-body ul ol, +.markdown-body ul ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li { + line-height: 28px; + font-size: 14px; + word-wrap: break-all; + list-style: disc; + margin-left: 10px; +} + +.markdown-body li > p { + margin-top: 16px; +} + +.markdown-body li + li { + margin-top: 0.25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; +} + +.markdown-body table th { + font-weight: 600; +} + +.markdown-body table td, +.markdown-body table th { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body img { + max-width: 100%; + box-sizing: initial; + background-color: #fff; + border: 1px solid #eee; + border: 1px solid var(--art-c-border-2); + cursor: zoom-in; +} + +.markdown-body img[align='right'] { + padding-left: 20px; +} + +.markdown-body img[align='left'] { + padding-right: 20px; +} + +.markdown-body code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27, 31, 35, 0.05); + border-radius: 3px; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre > code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 15px 20px 15px 0; + overflow: auto; + font-size: 92%; + line-height: 1.6; +} + +.markdown-body pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: initial; + border: 0; +} + +.markdown-body .commit-tease-sha { + display: inline-block; + font-size: 90%; + color: #444d56; +} + +.markdown-body .full-commit .btn-outline:not(:disabled):hover { + color: #005cc5; + border-color: #005cc5; +} + +.markdown-body .blob-wrapper { + overflow-x: auto; + overflow-y: hidden; +} + +.markdown-body .blob-wrapper-embedded { + max-height: 240px; + overflow-y: auto; +} + +.markdown-body .blob-num { + width: 1%; + min-width: 50px; + padding-right: 10px; + padding-left: 10px; + font-size: 12px; + line-height: 20px; + color: rgba(27, 31, 35, 0.3); + text-align: right; + white-space: nowrap; + vertical-align: top; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.markdown-body .blob-num:hover { + color: rgba(27, 31, 35, 0.6); +} + +.markdown-body .blob-num:before { + content: attr(data-line-number); +} + +.markdown-body .blob-code { + position: relative; + padding-right: 10px; + padding-left: 10px; + line-height: 20px; + vertical-align: top; +} + +.markdown-body .blob-code-inner { + overflow: visible; + font-size: 12px; + color: $font-color; + word-wrap: normal; + white-space: pre; +} + +.markdown-body .pl-token.active, +.markdown-body .pl-token:hover { + cursor: pointer; + background: #ffea7f; +} + +.markdown-body .tab-size[data-tab-size='1'] { + -moz-tab-size: 1; + tab-size: 1; +} + +.markdown-body .tab-size[data-tab-size='2'] { + -moz-tab-size: 2; + tab-size: 2; +} + +.markdown-body .tab-size[data-tab-size='3'] { + -moz-tab-size: 3; + tab-size: 3; +} + +.markdown-body .tab-size[data-tab-size='4'] { + -moz-tab-size: 4; + tab-size: 4; +} + +.markdown-body .tab-size[data-tab-size='5'] { + -moz-tab-size: 5; + tab-size: 5; +} + +.markdown-body .tab-size[data-tab-size='6'] { + -moz-tab-size: 6; + tab-size: 6; +} + +.markdown-body .tab-size[data-tab-size='7'] { + -moz-tab-size: 7; + tab-size: 7; +} + +.markdown-body .tab-size[data-tab-size='8'] { + -moz-tab-size: 8; + tab-size: 8; +} + +.markdown-body .tab-size[data-tab-size='9'] { + -moz-tab-size: 9; + tab-size: 9; +} + +.markdown-body .tab-size[data-tab-size='10'] { + -moz-tab-size: 10; + tab-size: 10; +} + +.markdown-body .tab-size[data-tab-size='11'] { + -moz-tab-size: 11; + tab-size: 11; +} + +.markdown-body .tab-size[data-tab-size='12'] { + -moz-tab-size: 12; + tab-size: 12; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item + .task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + margin: 0 0.2em 0.25em -1.6em; + vertical-align: middle; +} diff --git a/admin/src/assets/styles/core/mixin.scss b/admin/src/assets/styles/core/mixin.scss new file mode 100644 index 0000000..db36888 --- /dev/null +++ b/admin/src/assets/styles/core/mixin.scss @@ -0,0 +1,157 @@ +// sass 混合宏(函数) + +/** +* 溢出省略号 +* @param {Number} 行数 +*/ +@mixin ellipsis($rowCount: 1) { + @if $rowCount <=1 { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } @else { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: $rowCount; + -webkit-box-orient: vertical; + } +} + +/** +* 控制用户能否选中文本 +* @param {String} 类型 +*/ +@mixin userSelect($value: none) { + user-select: $value; + -moz-user-select: $value; + -ms-user-select: $value; + -webkit-user-select: $value; +} + +// 绝对定位居中 +@mixin absoluteCenter() { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; +} + +/** +* css3动画 +* +*/ +@mixin animation( + $from: ( + width: 0px + ), + $to: ( + width: 100px + ), + $name: mymove, + $animate: mymove 2s 1 linear infinite +) { + -webkit-animation: $animate; + -o-animation: $animate; + animation: $animate; + + @keyframes #{$name} { + from { + @each $key, $value in $from { + #{$key}: #{$value}; + } + } + + to { + @each $key, $value in $to { + #{$key}: #{$value}; + } + } + } + + @-webkit-keyframes #{$name} { + from { + @each $key, $value in $from { + $key: $value; + } + } + + to { + @each $key, $value in $to { + $key: $value; + } + } + } +} + +// 圆形盒子 +@mixin circle($size: 11px, $bg: #fff) { + border-radius: 50%; + width: $size; + height: $size; + line-height: $size; + text-align: center; + background: $bg; +} + +// placeholder +@mixin placeholder($color: #bbb) { + // Firefox + &::-moz-placeholder { + color: $color; + opacity: 1; + } + + // Internet Explorer 10+ + &:-ms-input-placeholder { + color: $color; + } + + // Safari and Chrome + &::-webkit-input-placeholder { + color: $color; + } + + &:placeholder-shown { + text-overflow: ellipsis; + } +} + +//背景透明,文字不透明。兼容IE8 +@mixin betterTransparentize($color, $alpha) { + $c: rgba($color, $alpha); + $ie_c: ie_hex_str($c); + background: rgba($color, 1); + background: $c; + background: transparent \9; + zoom: 1; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c}); + -ms-filter: 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c})'; +} + +//添加浏览器前缀 +@mixin browserPrefix($propertyName, $value) { + @each $prefix in -webkit-, -moz-, -ms-, -o-, '' { + #{$prefix}#{$propertyName}: $value; + } +} + +// 边框 +@mixin border($color: red) { + border: 1px solid $color; +} + +// 背景滤镜 +@mixin backdropBlur() { + --tw-backdrop-blur: blur(30px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) + var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) + var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) + var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) + var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) + var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} diff --git a/admin/src/assets/styles/core/reset.scss b/admin/src/assets/styles/core/reset.scss new file mode 100644 index 0000000..17a3bcf --- /dev/null +++ b/admin/src/assets/styles/core/reset.scss @@ -0,0 +1,41 @@ +@charset "UTF-8"; + +/*滚动条*/ +/*滚动条整体部分,必须要设置*/ +::-webkit-scrollbar { + width: 8px !important; + height: 0 !important; +} + +/*滚动条的轨道*/ +::-webkit-scrollbar-track { + background-color: var(--art-gray-200); +} + +/*滚动条的滑块按钮*/ +::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: #cccccc !important; + transition: all 0.2s; + -webkit-transition: all 0.2s; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #b0abab !important; +} + +/*滚动条的上下两端的按钮*/ +::-webkit-scrollbar-button { + height: 0px; + width: 0; +} + +.dark { + ::-webkit-scrollbar-track { + background-color: var(--default-bg-color); + } + + ::-webkit-scrollbar-thumb { + background-color: var(--art-gray-300) !important; + } +} diff --git a/admin/src/assets/styles/core/router-transition.scss b/admin/src/assets/styles/core/router-transition.scss new file mode 100644 index 0000000..f47c741 --- /dev/null +++ b/admin/src/assets/styles/core/router-transition.scss @@ -0,0 +1,104 @@ +@use 'sass:map'; + +// === 变量区域 === +$transition: ( + // 动画持续时间 + duration: 0.25s, + // 滑动动画的移动距离 + distance: 15px, + // 默认缓动函数 + easing: cubic-bezier(0.25, 0.1, 0.25, 1), + // 淡入淡出专用的缓动函数 + fade-easing: cubic-bezier(0.4, 0, 0.6, 1) +); + +// 抽取配置值函数,提高可复用性 +@function transition-config($key) { + @return map.get($transition, $key); +} + +// 变量简写 +$duration: transition-config('duration'); +$distance: transition-config('distance'); +$easing: transition-config('easing'); +$fade-easing: transition-config('fade-easing'); + +// === 动画类 === + +// 淡入淡出动画 +.fade { + &-enter-active, + &-leave-active { + transition: opacity $duration $fade-easing; + will-change: opacity; + } + + &-enter-from, + &-leave-to { + opacity: 0; + } + + &-enter-to, + &-leave-from { + opacity: 1; + } +} + +// 滑动动画通用样式 +@mixin slide-transition($direction) { + $distance-x: 0; + $distance-y: 0; + + @if $direction == 'left' { + $distance-x: -$distance; + } @else if $direction == 'right' { + $distance-x: $distance; + } @else if $direction == 'top' { + $distance-y: -$distance; + } @else if $direction == 'bottom' { + $distance-y: $distance; + } + + &-enter-active { + transition: + opacity $duration $easing, + transform $duration $easing; + will-change: opacity, transform; + } + + &-leave-active { + transition: + opacity calc($duration * 0.7) $easing, + transform calc($duration * 0.7) $easing; + will-change: opacity, transform; + } + + &-enter-from { + opacity: 0; + transform: translate3d($distance-x, $distance-y, 0); + } + + &-enter-to { + opacity: 1; + transform: translate3d(0, 0, 0); + } + + &-leave-to { + opacity: 0; + transform: translate3d(-$distance-x, -$distance-y, 0); + } +} + +// 滑动动画方向类 +.slide-left { + @include slide-transition('left'); +} +.slide-right { + @include slide-transition('right'); +} +.slide-top { + @include slide-transition('top'); +} +.slide-bottom { + @include slide-transition('bottom'); +} diff --git a/admin/src/assets/styles/core/tailwind.css b/admin/src/assets/styles/core/tailwind.css new file mode 100644 index 0000000..1a9e22c --- /dev/null +++ b/admin/src/assets/styles/core/tailwind.css @@ -0,0 +1,208 @@ +@import 'tailwindcss'; +@custom-variant dark (&:where(.dark, .dark *)); + +/* ==================== Light Mode Variables ==================== */ +:root { + /* Base Colors */ + --art-color: #ffffff; + --theme-color: var(--main-color); + + /* Theme Colors - OKLCH Format */ + --art-primary: oklch(0.7 0.23 260); + --art-secondary: oklch(0.72 0.19 231.6); + --art-error: oklch(0.73 0.15 25.3); + --art-info: oklch(0.58 0.03 254.1); + --art-success: oklch(0.78 0.17 166.1); + --art-warning: oklch(0.78 0.14 75.5); + --art-danger: oklch(0.68 0.22 25.3); + + /* Gray Scale - Light Mode */ + --art-gray-100: #f9fafb; + --art-gray-200: #f2f4f5; + --art-gray-300: #e6eaeb; + --art-gray-400: #dbdfe1; + --art-gray-500: #949eb7; + --art-gray-600: #7987a1; + --art-gray-700: #4d5875; + --art-gray-800: #383853; + --art-gray-900: #323251; + + /* Border Colors */ + --art-card-border: rgba(0, 0, 0, 0.08); + + --default-border: #e2e8ee; + --default-border-dashed: #dbdfe9; + + /* Background Colors */ + --default-bg-color: #fafbfc; + --default-box-color: #ffffff; + + /* Hover Color */ + --art-hover-color: #edeff0; + + /* Active Color */ + --art-active-color: #f2f4f5; + + /* Element Component Active Color */ + --art-el-active-color: #f2f4f5; +} + +/* ==================== Dark Mode Variables ==================== */ +.dark { + /* Base Colors */ + --art-color: #000000; + + /* Gray Scale - Dark Mode */ + --art-gray-100: #110f0f; + --art-gray-200: #17171c; + --art-gray-300: #393946; + --art-gray-400: #505062; + --art-gray-500: #73738c; + --art-gray-600: #8f8fa3; + --art-gray-700: #ababba; + --art-gray-800: #c7c7d1; + --art-gray-900: #e3e3e8; + + /* Border Colors */ + --art-card-border: rgba(255, 255, 255, 0.08); + + --default-border: rgba(255, 255, 255, 0.1); + --default-border-dashed: #363843; + + /* Background Colors */ + --default-bg-color: #070707; + --default-box-color: #161618; + + /* Hover Color */ + --art-hover-color: #252530; + + /* Active Color */ + --art-active-color: #202226; + + /* Element Component Active Color */ + --art-el-active-color: #2e2e38; +} + +/* ==================== Tailwind Theme Configuration ==================== */ +@theme { + /* Box Color (Light: white / Dark: black) */ + --color-box: var(--default-box-color); + + /* System Theme Color */ + --color-theme: var(--theme-color); + + /* Hover Color */ + --color-hover-color: var(--art-hover-color); + + /* Active Color */ + --color-active-color: var(--art-active-color); + + /* Active Color */ + --color-el-active-color: var(--art-active-color); + + /* ElementPlus Theme Colors */ + --color-primary: var(--art-primary); + --color-secondary: var(--art-secondary); + --color-error: var(--art-error); + --color-info: var(--art-info); + --color-success: var(--art-success); + --color-warning: var(--art-warning); + --color-danger: var(--art-danger); + + /* Gray Scale Colors (Auto-adapts to dark mode) */ + --color-g-100: var(--art-gray-100); + --color-g-200: var(--art-gray-200); + --color-g-300: var(--art-gray-300); + --color-g-400: var(--art-gray-400); + --color-g-500: var(--art-gray-500); + --color-g-600: var(--art-gray-600); + --color-g-700: var(--art-gray-700); + --color-g-800: var(--art-gray-800); + --color-g-900: var(--art-gray-900); +} + +/* ==================== Custom Border Radius Utilities ==================== */ +@utility rounded-custom-xs { + border-radius: calc(var(--custom-radius) / 2); +} + +@utility rounded-custom-sm { + border-radius: calc(var(--custom-radius) / 2 + 2px); +} + +/* ==================== Custom Utility Classes ==================== */ +@layer utilities { + /* Flexbox Layout Utilities */ + .flex-c { + @apply flex items-center; + } + + .flex-b { + @apply flex justify-between; + } + + .flex-cc { + @apply flex items-center justify-center; + } + + .flex-cb { + @apply flex items-center justify-between; + } + + /* Transition Utilities */ + .tad-200 { + @apply transition-all duration-200; + } + + .tad-300 { + @apply transition-all duration-300; + } + + /* Border Utilities */ + .border-full-d { + @apply border border-[var(--default-border)]; + } + + .border-b-d { + @apply border-b border-[var(--default-border)]; + } + + .border-t-d { + @apply border-t border-[var(--default-border)]; + } + + .border-l-d { + @apply border-l border-[var(--default-border)]; + } + + .border-r-d { + @apply border-r border-[var(--default-border)]; + } + + /* Cursor Utilities */ + .c-p { + @apply cursor-pointer; + } +} + +/* ==================== Custom Component Classes ==================== */ +@layer components { + /* Art Card Header Component */ + .art-card-header { + @apply flex justify-between pr-6 pb-1; + + .title { + h4 { + @apply text-lg font-medium text-g-900; + } + + p { + @apply mt-1 text-sm text-g-600; + + span { + @apply ml-2 font-medium; + } + } + } + } +} diff --git a/admin/src/assets/styles/core/theme-animation.scss b/admin/src/assets/styles/core/theme-animation.scss new file mode 100644 index 0000000..377b945 --- /dev/null +++ b/admin/src/assets/styles/core/theme-animation.scss @@ -0,0 +1,63 @@ +// 定义基础变量 +$bg-animation-color-light: #000; +$bg-animation-color-dark: #fff; +$bg-animation-duration: 0.5s; + +html { + --bg-animation-color: $bg-animation-color-light; + + &.dark { + --bg-animation-color: $bg-animation-color-dark; + } + + // View transition styles + &::view-transition-old(*) { + animation: none; + } + + &::view-transition-new(*) { + animation: clip $bg-animation-duration ease-in both; + } + + &::view-transition-old(root) { + z-index: 1; + } + + &::view-transition-new(root) { + z-index: 9999; + } + + &.dark { + &::view-transition-old(*) { + animation: clip $bg-animation-duration ease-in reverse both; + } + + &::view-transition-new(*) { + animation: none; + } + + &::view-transition-old(root) { + z-index: 9999; + } + + &::view-transition-new(root) { + z-index: 1; + } + } +} + +// 定义动画 +@keyframes clip { + from { + clip-path: circle(0% at var(--x) var(--y)); + } + + to { + clip-path: circle(var(--r) at var(--x) var(--y)); + } +} + +// body 相关样式 +body { + background-color: var(--bg-animation-color); +} diff --git a/admin/src/assets/styles/core/theme-change.scss b/admin/src/assets/styles/core/theme-change.scss new file mode 100644 index 0000000..5b640d2 --- /dev/null +++ b/admin/src/assets/styles/core/theme-change.scss @@ -0,0 +1,11 @@ +// 主题切换过渡优化,优化除视觉上的不适感 +.theme-change { + * { + transition: 0s !important; + } + + .el-switch__core, + .el-switch__action { + transition: all 0.3s !important; + } +} diff --git a/admin/src/assets/styles/custom/one-dark-pro.scss b/admin/src/assets/styles/custom/one-dark-pro.scss new file mode 100644 index 0000000..36bdf63 --- /dev/null +++ b/admin/src/assets/styles/custom/one-dark-pro.scss @@ -0,0 +1,98 @@ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + + color: #a6accd; +} + +.hljs-string, +.hljs-section, +.hljs-selector-class, +.hljs-template-variable, +.hljs-deletion { + color: #aed07e !important; +} + +.hljs-comment, +.hljs-quote { + color: #6f747d; +} + +.hljs-doctag, +.hljs-keyword, +.hljs-formula { + color: #c792ea; +} + +.hljs-section, +.hljs-name, +.hljs-selector-tag, +.hljs-deletion, +.hljs-subst { + color: #c86068; +} + +.hljs-literal { + color: #56b6c2; +} + +.hljs-string, +.hljs-regexp, +.hljs-addition, +.hljs-attribute, +.hljs-meta-string { + color: #abb2bf; +} + +.hljs-attribute { + color: #c792ea; +} + +.hljs-function { + color: #c792ea; +} + +.hljs-type { + color: #f07178; +} + +.hljs-title { + color: #82aaff !important; +} + +.hljs-built_in, +.hljs-class { + color: #82aaff; +} + +// 括号 +.hljs-params { + color: #a6accd; +} + +.hljs-attr, +.hljs-variable, +.hljs-template-variable, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-number { + color: #de7e61; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-meta, +.hljs-selector-id { + color: #61aeee; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/admin/src/assets/styles/index.scss b/admin/src/assets/styles/index.scss new file mode 100644 index 0000000..cdc2ddc --- /dev/null +++ b/admin/src/assets/styles/index.scss @@ -0,0 +1,23 @@ +// 重置默认样式 +@use './core/reset.scss'; + +// 应用全局样式 +@use './core/app.scss'; + +// Element Plus 样式优化 +@use './core/el-ui.scss'; + +// Element Plus 暗黑主题 +@use './core/el-dark.scss'; + +// 暗黑主题样式优化 +@use './core/dark.scss'; + +// 路由切换动画 +@use './core/router-transition'; + +// 主题切换过渡优化 +@use './core/theme-change.scss'; + +// 主题切换圆形扩散动画 +@use './core/theme-animation.scss'; diff --git a/admin/src/assets/svg/loading.ts b/admin/src/assets/svg/loading.ts new file mode 100644 index 0000000..fdfb078 --- /dev/null +++ b/admin/src/assets/svg/loading.ts @@ -0,0 +1,32 @@ +// 自定义四点旋转SVG +export const fourDotsSpinnerSvg = ` + + + + + + + + + +` diff --git a/admin/src/components/core/banners/art-basic-banner/index.vue b/admin/src/components/core/banners/art-basic-banner/index.vue new file mode 100644 index 0000000..65b47e4 --- /dev/null +++ b/admin/src/components/core/banners/art-basic-banner/index.vue @@ -0,0 +1,343 @@ + + + + + + diff --git a/admin/src/components/core/banners/art-card-banner/index.vue b/admin/src/components/core/banners/art-card-banner/index.vue new file mode 100644 index 0000000..8a5f9d4 --- /dev/null +++ b/admin/src/components/core/banners/art-card-banner/index.vue @@ -0,0 +1,114 @@ + + + + diff --git a/admin/src/components/core/base/art-back-to-top/index.vue b/admin/src/components/core/base/art-back-to-top/index.vue new file mode 100644 index 0000000..6f8da61 --- /dev/null +++ b/admin/src/components/core/base/art-back-to-top/index.vue @@ -0,0 +1,40 @@ + + + + diff --git a/admin/src/components/core/base/art-logo/index.vue b/admin/src/components/core/base/art-logo/index.vue new file mode 100644 index 0000000..d5a4375 --- /dev/null +++ b/admin/src/components/core/base/art-logo/index.vue @@ -0,0 +1,52 @@ + + + + + + diff --git a/admin/src/components/core/base/art-svg-icon/index.vue b/admin/src/components/core/base/art-svg-icon/index.vue new file mode 100644 index 0000000..0bfcd0c --- /dev/null +++ b/admin/src/components/core/base/art-svg-icon/index.vue @@ -0,0 +1,24 @@ + + + + diff --git a/admin/src/components/core/cards/art-bar-chart-card/index.vue b/admin/src/components/core/cards/art-bar-chart-card/index.vue new file mode 100644 index 0000000..6815c2b --- /dev/null +++ b/admin/src/components/core/cards/art-bar-chart-card/index.vue @@ -0,0 +1,103 @@ + + + + diff --git a/admin/src/components/core/cards/art-data-list-card/index.vue b/admin/src/components/core/cards/art-data-list-card/index.vue new file mode 100644 index 0000000..fc43323 --- /dev/null +++ b/admin/src/components/core/cards/art-data-list-card/index.vue @@ -0,0 +1,74 @@ + + + + diff --git a/admin/src/components/core/cards/art-donut-chart-card/index.vue b/admin/src/components/core/cards/art-donut-chart-card/index.vue new file mode 100644 index 0000000..df2dcbb --- /dev/null +++ b/admin/src/components/core/cards/art-donut-chart-card/index.vue @@ -0,0 +1,124 @@ + + + + diff --git a/admin/src/components/core/cards/art-image-card/index.vue b/admin/src/components/core/cards/art-image-card/index.vue new file mode 100644 index 0000000..d27fe00 --- /dev/null +++ b/admin/src/components/core/cards/art-image-card/index.vue @@ -0,0 +1,89 @@ + + + + diff --git a/admin/src/components/core/cards/art-line-chart-card/index.vue b/admin/src/components/core/cards/art-line-chart-card/index.vue new file mode 100644 index 0000000..e58c9b2 --- /dev/null +++ b/admin/src/components/core/cards/art-line-chart-card/index.vue @@ -0,0 +1,126 @@ + + + + diff --git a/admin/src/components/core/cards/art-progress-card/index.vue b/admin/src/components/core/cards/art-progress-card/index.vue new file mode 100644 index 0000000..048a836 --- /dev/null +++ b/admin/src/components/core/cards/art-progress-card/index.vue @@ -0,0 +1,86 @@ + + + + diff --git a/admin/src/components/core/cards/art-stats-card/index.vue b/admin/src/components/core/cards/art-stats-card/index.vue new file mode 100644 index 0000000..8e0341b --- /dev/null +++ b/admin/src/components/core/cards/art-stats-card/index.vue @@ -0,0 +1,67 @@ + + + + diff --git a/admin/src/components/core/cards/art-timeline-list-card/index.vue b/admin/src/components/core/cards/art-timeline-list-card/index.vue new file mode 100644 index 0000000..fbb2c78 --- /dev/null +++ b/admin/src/components/core/cards/art-timeline-list-card/index.vue @@ -0,0 +1,69 @@ + + + + diff --git a/admin/src/components/core/charts/art-bar-chart/index.vue b/admin/src/components/core/charts/art-bar-chart/index.vue new file mode 100644 index 0000000..d677196 --- /dev/null +++ b/admin/src/components/core/charts/art-bar-chart/index.vue @@ -0,0 +1,203 @@ + + + + diff --git a/admin/src/components/core/charts/art-dual-bar-compare-chart/index.vue b/admin/src/components/core/charts/art-dual-bar-compare-chart/index.vue new file mode 100644 index 0000000..32aa60f --- /dev/null +++ b/admin/src/components/core/charts/art-dual-bar-compare-chart/index.vue @@ -0,0 +1,195 @@ + + + + diff --git a/admin/src/components/core/charts/art-h-bar-chart/index.vue b/admin/src/components/core/charts/art-h-bar-chart/index.vue new file mode 100644 index 0000000..2e34759 --- /dev/null +++ b/admin/src/components/core/charts/art-h-bar-chart/index.vue @@ -0,0 +1,208 @@ + + + + diff --git a/admin/src/components/core/charts/art-k-line-chart/index.vue b/admin/src/components/core/charts/art-k-line-chart/index.vue new file mode 100644 index 0000000..0061b51 --- /dev/null +++ b/admin/src/components/core/charts/art-k-line-chart/index.vue @@ -0,0 +1,152 @@ + + + + diff --git a/admin/src/components/core/charts/art-line-chart/index.vue b/admin/src/components/core/charts/art-line-chart/index.vue new file mode 100644 index 0000000..b70c2c3 --- /dev/null +++ b/admin/src/components/core/charts/art-line-chart/index.vue @@ -0,0 +1,371 @@ + + + + diff --git a/admin/src/components/core/charts/art-radar-chart/index.vue b/admin/src/components/core/charts/art-radar-chart/index.vue new file mode 100644 index 0000000..e99fff6 --- /dev/null +++ b/admin/src/components/core/charts/art-radar-chart/index.vue @@ -0,0 +1,105 @@ + + + + diff --git a/admin/src/components/core/charts/art-ring-chart/index.vue b/admin/src/components/core/charts/art-ring-chart/index.vue new file mode 100644 index 0000000..79115f7 --- /dev/null +++ b/admin/src/components/core/charts/art-ring-chart/index.vue @@ -0,0 +1,133 @@ + + + + diff --git a/admin/src/components/core/charts/art-scatter-chart/index.vue b/admin/src/components/core/charts/art-scatter-chart/index.vue new file mode 100644 index 0000000..995b56a --- /dev/null +++ b/admin/src/components/core/charts/art-scatter-chart/index.vue @@ -0,0 +1,115 @@ + + + + diff --git a/admin/src/components/core/forms/art-button-more/index.vue b/admin/src/components/core/forms/art-button-more/index.vue new file mode 100644 index 0000000..858d305 --- /dev/null +++ b/admin/src/components/core/forms/art-button-more/index.vue @@ -0,0 +1,71 @@ + + + + diff --git a/admin/src/components/core/forms/art-button-table/index.vue b/admin/src/components/core/forms/art-button-table/index.vue new file mode 100644 index 0000000..c849901 --- /dev/null +++ b/admin/src/components/core/forms/art-button-table/index.vue @@ -0,0 +1,59 @@ + + + + diff --git a/admin/src/components/core/forms/art-drag-verify/index.vue b/admin/src/components/core/forms/art-drag-verify/index.vue new file mode 100644 index 0000000..5306e04 --- /dev/null +++ b/admin/src/components/core/forms/art-drag-verify/index.vue @@ -0,0 +1,430 @@ + + + + + + + + diff --git a/admin/src/components/core/forms/art-form/index.vue b/admin/src/components/core/forms/art-form/index.vue new file mode 100644 index 0000000..561e878 --- /dev/null +++ b/admin/src/components/core/forms/art-form/index.vue @@ -0,0 +1,507 @@ + + + + + + diff --git a/admin/src/components/core/forms/art-search-bar/index.vue b/admin/src/components/core/forms/art-search-bar/index.vue new file mode 100644 index 0000000..6786dca --- /dev/null +++ b/admin/src/components/core/forms/art-search-bar/index.vue @@ -0,0 +1,581 @@ + + + + + + + + diff --git a/admin/src/components/core/layouts/art-breadcrumb/index.vue b/admin/src/components/core/layouts/art-breadcrumb/index.vue new file mode 100644 index 0000000..4b54859 --- /dev/null +++ b/admin/src/components/core/layouts/art-breadcrumb/index.vue @@ -0,0 +1,142 @@ + + + + diff --git a/admin/src/components/core/layouts/art-chat-window/index.vue b/admin/src/components/core/layouts/art-chat-window/index.vue new file mode 100644 index 0000000..f3d9471 --- /dev/null +++ b/admin/src/components/core/layouts/art-chat-window/index.vue @@ -0,0 +1,262 @@ + + + + diff --git a/admin/src/components/core/layouts/art-fast-enter/index.vue b/admin/src/components/core/layouts/art-fast-enter/index.vue new file mode 100644 index 0000000..fdde222 --- /dev/null +++ b/admin/src/components/core/layouts/art-fast-enter/index.vue @@ -0,0 +1,113 @@ + + + + diff --git a/admin/src/components/core/layouts/art-fireworks-effect/index.vue b/admin/src/components/core/layouts/art-fireworks-effect/index.vue new file mode 100644 index 0000000..be85274 --- /dev/null +++ b/admin/src/components/core/layouts/art-fireworks-effect/index.vue @@ -0,0 +1,633 @@ + + + + diff --git a/admin/src/components/core/layouts/art-global-component/index.vue b/admin/src/components/core/layouts/art-global-component/index.vue new file mode 100644 index 0000000..6908f94 --- /dev/null +++ b/admin/src/components/core/layouts/art-global-component/index.vue @@ -0,0 +1,14 @@ + + + + diff --git a/admin/src/components/core/layouts/art-global-search/index.vue b/admin/src/components/core/layouts/art-global-search/index.vue new file mode 100644 index 0000000..f2c0b88 --- /dev/null +++ b/admin/src/components/core/layouts/art-global-search/index.vue @@ -0,0 +1,431 @@ + + + + + + + diff --git a/admin/src/components/core/layouts/art-header-bar/index.vue b/admin/src/components/core/layouts/art-header-bar/index.vue new file mode 100644 index 0000000..4e3c8f9 --- /dev/null +++ b/admin/src/components/core/layouts/art-header-bar/index.vue @@ -0,0 +1,485 @@ + + + + + + diff --git a/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue b/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue new file mode 100644 index 0000000..7b9a370 --- /dev/null +++ b/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue @@ -0,0 +1,144 @@ + + + + + + diff --git a/admin/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue b/admin/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue new file mode 100644 index 0000000..edd1473 --- /dev/null +++ b/admin/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/admin/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue b/admin/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue new file mode 100644 index 0000000..0f3a817 --- /dev/null +++ b/admin/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/admin/src/components/core/layouts/art-menus/art-mixed-menu/index.vue b/admin/src/components/core/layouts/art-menus/art-mixed-menu/index.vue new file mode 100644 index 0000000..4e98246 --- /dev/null +++ b/admin/src/components/core/layouts/art-menus/art-mixed-menu/index.vue @@ -0,0 +1,279 @@ + + + + + + + + diff --git a/admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue new file mode 100644 index 0000000..c9eb1fa --- /dev/null +++ b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue @@ -0,0 +1,369 @@ + + + + + + + + diff --git a/admin/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss new file mode 100644 index 0000000..8cccdd9 --- /dev/null +++ b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss @@ -0,0 +1,251 @@ +.layout-sidebar { + display: flex; + height: 100vh; + user-select: none; + scrollbar-width: none; + border-right: 1px solid var(--art-card-border); + + &.no-border { + border-right: none !important; + } + + // 自定义滚动条宽度 + :deep(.el-scrollbar__bar.is-vertical) { + width: 4px; + } + + :deep(.el-scrollbar__thumb) { + right: -2px; + background-color: #ccc; + border-radius: 2px; + } + + .dual-menu-left { + position: relative; + width: 80px; + height: 100%; + border-right: 1px solid var(--art-card-border) !important; + transition: width 0.25s; + + .logo { + margin: auto; + margin-top: 12px; + margin-bottom: 3px; + cursor: pointer; + } + + ul { + li { + > div { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 8px; + overflow: hidden; + text-align: center; + cursor: pointer; + border-radius: 5px; + + .art-svg-icon { + display: block; + margin: 0 auto; + font-size: 20px; + } + + span { + display: -webkit-box; + width: 100%; + overflow: hidden; + font-size: 12px; + text-overflow: ellipsis; + -webkit-line-clamp: 1; + line-clamp: 1; + -webkit-box-orient: vertical; + } + + &.is-active { + background: var(--el-color-primary-light-9); + + .art-svg-icon, + span { + color: var(--theme-color) !important; + } + } + } + } + } + + .switch-btn { + position: absolute; + right: 0; + bottom: 15px; + left: 0; + margin: auto; + } + } + + .menu-left { + position: relative; + box-sizing: border-box; + height: 100vh; + + @media only screen and (width <= 640px) { + height: 100dvh; + } + + .el-menu { + height: 100%; + } + + &:hover { + .dual-menu-collapse-btn { + opacity: 1 !important; + } + } + + .dual-menu-collapse-btn { + position: absolute; + top: 50%; + right: -11px; + z-index: 10; + width: 11px; + height: 50px; + cursor: pointer; + background-color: var(--default-box-color); + border: 1px solid var(--art-card-border); + border-radius: 0 15px 15px 0; + opacity: 0; + transition: opacity 0.2s; + transform: translateY(-50%); + + &:hover { + .art-svg-icon { + color: var(--art-gray-800) !important; + } + } + + .art-svg-icon { + position: absolute; + top: 0; + bottom: 0; + left: -4px; + margin: auto; + transition: all 0.3s; + } + } + } + + .header { + position: relative; + box-sizing: border-box; + display: flex; + align-items: center; + width: 100%; + height: 60px; + overflow: hidden; + line-height: 60px; + cursor: pointer; + + .logo { + margin-left: 22px; + } + + p { + position: absolute; + top: 0; + bottom: 0; + left: 58px; + box-sizing: border-box; + margin-left: 10px; + font-size: 18px; + + &.is-dual-menu-name { + left: 25px; + margin: auto; + } + } + } + + .el-menu { + box-sizing: border-box; + // 防止菜单内的滚动影响整个页面滚动 + overscroll-behavior: contain; + border-right: 0; + scrollbar-width: none; + -ms-scroll-chaining: contain; + + &::-webkit-scrollbar { + width: 0 !important; + } + } + + .menu-model { + display: none; + } +} + +@media only screen and (width <= 800px) { + .layout-sidebar { + width: 0; + + .header { + height: 50px; + line-height: 50px; + } + + .el-menu { + height: calc(100vh - 60px); + } + + .el-menu--collapse { + width: 0; + } + + // 折叠状态下的header样式 + .menu-left-close .header { + .logo { + display: none; + } + + p { + left: 16px; + font-size: 0; + opacity: 0 !important; + } + } + + .menu-model { + position: fixed; + top: 0; + left: 0; + z-index: -1; + display: block; + width: 100%; + height: 100vh; + background: rgba($color: #000, $alpha: 50%); + transition: opacity 0.2s ease-in-out; + } + } +} + +@media only screen and (width <= 640px) { + .layout-sidebar { + border-right: 0 !important; + } +} + +.dark { + .layout-sidebar { + border-right: 1px solid rgb(255 255 255 / 13%); + + :deep(.el-scrollbar__thumb) { + background-color: #777; + } + + .dual-menu-left { + border-right: 1px solid rgb(255 255 255 / 9%) !important; + } + } +} diff --git a/admin/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss new file mode 100644 index 0000000..7626c42 --- /dev/null +++ b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss @@ -0,0 +1,258 @@ +@use '@styles/core/mixin.scss' as *; + +// 菜单样式变量 +$menu-height: 42px; +$menu-icon-size: 20px; +$menu-font-size: 14px; +$hover-bg-color: var(--art-gray-200); +$popup-menu-height: 40px; +$popup-menu-padding: 8px; +$popup-menu-margin: 5px; +$popup-menu-radius: 6px; + +// 通用菜单项样式 +@mixin menu-item-base { + width: calc(100% - 16px); + margin-left: 8px; + border-radius: 6px; + + .menu-icon { + margin-left: -7px; + } +} + +// 通用 hover 样式 +@mixin menu-hover($bg-color) { + .el-sub-menu__title:hover, + .el-menu-item:not(.is-active):hover { + background: $bg-color !important; + } +} + +// 通用选中样式 +@mixin menu-active($color, $bg-color, $icon-color: var(--theme-color)) { + .el-menu-item.is-active { + color: $color !important; + background-color: $bg-color; + + .menu-icon { + .art-svg-icon { + color: $icon-color !important; + } + } + } +} + +// 弹窗菜单项样式 +@mixin popup-menu-item { + height: $popup-menu-height; + margin-bottom: $popup-menu-margin; + border-radius: $popup-menu-radius; + + .menu-icon { + margin-right: 5px; + } + + &:last-of-type { + margin-bottom: 0; + } +} + +// 主题菜单通用样式(合并 design 和 dark 主题的共同逻辑) +@mixin theme-menu-base { + .el-sub-menu__title, + .el-menu-item { + @include menu-item-base; + } +} + +// 弹窗菜单通用样式 +@mixin popup-menu-base($hover-bg, $active-color, $active-bg) { + .el-menu--popup { + padding: $popup-menu-padding; + + .el-sub-menu__title:hover, + .el-menu-item:hover { + background-color: $hover-bg !important; + border-radius: $popup-menu-radius; + } + + .el-menu-item { + @include popup-menu-item; + + &.is-active { + color: $active-color !important; + background-color: $active-bg !important; + } + } + + .el-sub-menu { + @include popup-menu-item; + + height: $popup-menu-height !important; + + .el-sub-menu__title { + height: $popup-menu-height !important; + border-radius: $popup-menu-radius; + } + } + } +} + +.layout-sidebar { + // ---------------------- Modify default style ---------------------- + + // 菜单折叠样式 + .menu-left-close { + .header { + .logo { + margin: 0 auto; + } + } + } + + // 菜单图标 + .menu-icon { + margin-right: 8px; + font-size: $menu-icon-size; + } + + // 菜单高度 + .el-sub-menu__title, + .el-menu-item { + height: $menu-height !important; + margin-bottom: 4px; + line-height: $menu-height !important; + + span { + font-size: $menu-font-size !important; + + @include ellipsis(); + } + } + + // 右侧箭头 + .el-sub-menu__icon-arrow { + width: 13px !important; + font-size: 13px !important; + } + + // 菜单折叠 + .el-menu--collapse { + .el-sub-menu.is-active { + .el-sub-menu__title { + .menu-icon { + .art-svg-icon { + // 选中菜单图标颜色 + color: var(--theme-color) !important; + } + } + } + } + } + + // ---------------------- Design theme menu ---------------------- + .el-menu-design { + @include theme-menu-base; + @include menu-active(var(--theme-color), var(--el-color-primary-light-9)); + @include menu-hover($hover-bg-color); + + .el-sub-menu__icon-arrow { + color: var(--art-gray-600); + } + } + + // ---------------------- Dark theme menu ---------------------- + .el-menu-dark { + @include theme-menu-base; + @include menu-active(#fff, #27282d, #fff); + @include menu-hover(#0f1015); + + .el-sub-menu__icon-arrow { + color: var(--art-gray-400); + } + } + + // ---------------------- Light theme menu ---------------------- + .el-menu-light { + .el-sub-menu__title, + .el-menu-item { + .menu-icon { + margin-left: 1px; + } + } + + .el-menu-item.is-active { + background-color: var(--el-color-primary-light-9); + + .art-svg-icon { + color: var(--theme-color) !important; + } + + &::before { + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + content: ''; + background: var(--theme-color); + } + } + + @include menu-hover($hover-bg-color); + + .el-sub-menu__icon-arrow { + color: var(--art-gray-600); + } + } +} + +@media only screen and (width <= 640px) { + .layout-sidebar { + .el-menu-design { + > .el-sub-menu { + margin-left: 0; + } + + .el-sub-menu { + width: 100% !important; + } + } + } +} + +// 菜单折叠 hover 弹窗样式(浅色主题) +.el-menu--vertical, +.el-menu--popup-container { + @include popup-menu-base(var(--art-gray-200), var(--art-gray-900), var(--art-gray-200)); +} + +// 暗黑模式菜单样式 +.dark { + .el-menu--vertical, + .el-menu--popup-container { + @include popup-menu-base(var(--art-gray-200), var(--art-gray-900), #292a2e); + } + + .layout-sidebar { + // 图标颜色、文字颜色 + .menu-icon .art-svg-icon, + .menu-name { + color: var(--art-gray-800) !important; + } + + // 选中的文字颜色跟图标颜色 + .el-menu-item.is-active { + span, + .menu-icon .art-svg-icon { + color: var(--theme-color) !important; + } + } + + // 右侧箭头颜色 + .el-sub-menu__icon-arrow { + color: #fff; + } + } +} diff --git a/admin/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue new file mode 100644 index 0000000..9afa262 --- /dev/null +++ b/admin/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue @@ -0,0 +1,199 @@ + + + diff --git a/admin/src/components/core/layouts/art-notification/index.vue b/admin/src/components/core/layouts/art-notification/index.vue new file mode 100644 index 0000000..a58853c --- /dev/null +++ b/admin/src/components/core/layouts/art-notification/index.vue @@ -0,0 +1,456 @@ + + + + + + diff --git a/admin/src/components/core/layouts/art-page-content/index.vue b/admin/src/components/core/layouts/art-page-content/index.vue new file mode 100644 index 0000000..a862df1 --- /dev/null +++ b/admin/src/components/core/layouts/art-page-content/index.vue @@ -0,0 +1,136 @@ + + + diff --git a/admin/src/components/core/layouts/art-screen-lock/index.vue b/admin/src/components/core/layouts/art-screen-lock/index.vue new file mode 100644 index 0000000..5bf5248 --- /dev/null +++ b/admin/src/components/core/layouts/art-screen-lock/index.vue @@ -0,0 +1,519 @@ + + + + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts new file mode 100644 index 0000000..35e8066 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts @@ -0,0 +1,248 @@ +import { computed } from 'vue' +import { useI18n } from 'vue-i18n' +import { ContainerWidthEnum } from '@/enums/appEnum' +import AppConfig from '@/config' +import { headerBarConfig } from '@/config/modules/headerBar' + +/** + * 设置项配置选项管理 + */ +export function useSettingsConfig() { + const { t } = useI18n() + + // 标签页风格选项 + const tabStyleOptions = computed(() => [ + { + value: 'tab-default', + label: t('setting.tabStyle.default') + }, + { + value: 'tab-card', + label: t('setting.tabStyle.card') + }, + { + value: 'tab-google', + label: t('setting.tabStyle.google') + } + ]) + + // 页面切换动画选项 + const pageTransitionOptions = computed(() => [ + { + value: '', + label: t('setting.transition.list.none') + }, + { + value: 'fade', + label: t('setting.transition.list.fade') + }, + { + value: 'slide-left', + label: t('setting.transition.list.slideLeft') + }, + { + value: 'slide-bottom', + label: t('setting.transition.list.slideBottom') + }, + { + value: 'slide-top', + label: t('setting.transition.list.slideTop') + } + ]) + + // 圆角大小选项 + const customRadiusOptions = [ + { value: '0', label: '0' }, + { value: '0.25', label: '0.25' }, + { value: '0.5', label: '0.5' }, + { value: '0.75', label: '0.75' }, + { value: '1', label: '1' } + ] + + // 容器宽度选项 + const containerWidthOptions = computed(() => [ + { + value: ContainerWidthEnum.FULL, + label: t('setting.container.list[0]'), + icon: 'icon-park-outline:auto-width' + }, + { + value: ContainerWidthEnum.BOXED, + label: t('setting.container.list[1]'), + icon: 'ix:width' + } + ]) + + // 盒子样式选项 + const boxStyleOptions = computed(() => [ + { + value: 'border-mode', + label: t('setting.box.list[0]'), + type: 'border-mode' as const + }, + { + value: 'shadow-mode', + label: t('setting.box.list[1]'), + type: 'shadow-mode' as const + } + ]) + + // 从配置文件获取的选项 + const configOptions = { + // 主题色彩选项 + mainColors: AppConfig.systemMainColor, + + // 主题风格选项 + themeList: AppConfig.settingThemeList, + + // 菜单布局选项 + menuLayoutList: AppConfig.menuLayoutList + } + + // 基础设置项配置 + const basicSettingsConfig = computed(() => { + // 定义所有基础设置项 + const allSettings = [ + { + key: 'showWorkTab', + label: t('setting.basics.list.multiTab'), + type: 'switch' as const, + handler: 'workTab', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'uniqueOpened', + label: t('setting.basics.list.accordion'), + type: 'switch' as const, + handler: 'uniqueOpened', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'showMenuButton', + label: t('setting.basics.list.collapseSidebar'), + type: 'switch' as const, + handler: 'menuButton', + headerBarKey: 'menuButton' as const + }, + { + key: 'showFastEnter', + label: t('setting.basics.list.fastEnter'), + type: 'switch' as const, + handler: 'fastEnter', + headerBarKey: 'fastEnter' as const + }, + { + key: 'showRefreshButton', + label: t('setting.basics.list.reloadPage'), + type: 'switch' as const, + handler: 'refreshButton', + headerBarKey: 'refreshButton' as const + }, + { + key: 'showCrumbs', + label: t('setting.basics.list.breadcrumb'), + type: 'switch' as const, + handler: 'crumbs', + mobileHide: true, + headerBarKey: 'breadcrumb' as const + }, + { + key: 'showLanguage', + label: t('setting.basics.list.language'), + type: 'switch' as const, + handler: 'language', + headerBarKey: 'language' as const + }, + { + key: 'showNprogress', + label: t('setting.basics.list.progressBar'), + type: 'switch' as const, + handler: 'nprogress', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'colorWeak', + label: t('setting.basics.list.weakMode'), + type: 'switch' as const, + handler: 'colorWeak', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'watermarkVisible', + label: t('setting.basics.list.watermark'), + type: 'switch' as const, + handler: 'watermark', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'menuOpenWidth', + label: t('setting.basics.list.menuWidth'), + type: 'input-number' as const, + handler: 'menuOpenWidth', + min: 180, + max: 320, + step: 10, + style: { width: '120px' }, + controlsPosition: 'right' as const, + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'tabStyle', + label: t('setting.basics.list.tabStyle'), + type: 'select' as const, + handler: 'tabStyle', + options: tabStyleOptions.value, + style: { width: '120px' }, + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'pageTransition', + label: t('setting.basics.list.pageTransition'), + type: 'select' as const, + handler: 'pageTransition', + options: pageTransitionOptions.value, + style: { width: '120px' }, + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'customRadius', + label: t('setting.basics.list.borderRadius'), + type: 'select' as const, + handler: 'customRadius', + options: customRadiusOptions, + style: { width: '120px' }, + headerBarKey: null // 不依赖headerBar配置 + } + ] + + // 根据 headerBarConfig 过滤设置项 + return ( + allSettings + .filter((setting) => { + // 如果设置项不依赖headerBar配置,则始终显示 + if (setting.headerBarKey === null) { + return true + } + + // 如果依赖headerBar配置,检查对应的功能是否启用 + const headerBarFeature = headerBarConfig[setting.headerBarKey] + return headerBarFeature?.enabled !== false + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(({ headerBarKey: _headerBarKey, ...setting }) => setting) + ) + }) + + return { + // 选项配置 + tabStyleOptions, + pageTransitionOptions, + customRadiusOptions, + containerWidthOptions, + boxStyleOptions, + configOptions, + + // 设置项配置 + basicSettingsConfig + } +} diff --git a/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts new file mode 100644 index 0000000..392c690 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts @@ -0,0 +1,167 @@ +import { useSettingStore } from '@/store/modules/setting' +import { storeToRefs } from 'pinia' +import type { ContainerWidthEnum } from '@/enums/appEnum' + +/** + * 设置项通用处理逻辑 + */ +export function useSettingsHandlers() { + const settingStore = useSettingStore() + + // DOM 操作相关 + const domOperations = { + // 设置HTML类名 + setHtmlClass: (className: string, add: boolean) => { + const el = document.getElementsByTagName('html')[0] + if (add) { + el.classList.add(className) + } else { + el.classList.remove(className) + } + }, + + // 设置根元素属性 + setRootAttribute: (attribute: string, value: string) => { + const el = document.documentElement + el.setAttribute(attribute, value) + }, + + // 设置body类名 + setBodyClass: (className: string, add: boolean) => { + const el = document.getElementsByTagName('body')[0] + if (add) { + el.classList.add(className) + } else { + el.classList.remove(className) + } + } + } + + // 通用切换处理器 + const createToggleHandler = (storeMethod: () => void, callback?: () => void) => { + return () => { + storeMethod() + callback?.() + } + } + + // 通用值变更处理器 + const createValueHandler = ( + storeMethod: (value: T) => void, + callback?: (value: T) => void + ) => { + return (value: T) => { + if (value !== undefined && value !== null) { + storeMethod(value) + callback?.(value) + } + } + } + + // 基础设置处理器 + const basicHandlers = { + // 工作台标签页 + workTab: createToggleHandler(() => settingStore.setWorkTab(!settingStore.showWorkTab)), + + // 菜单手风琴 + uniqueOpened: createToggleHandler(() => settingStore.setUniqueOpened()), + + // 显示菜单按钮 + menuButton: createToggleHandler(() => settingStore.setButton()), + + // 显示快速入口 + fastEnter: createToggleHandler(() => settingStore.setFastEnter()), + + // 显示刷新按钮 + refreshButton: createToggleHandler(() => settingStore.setShowRefreshButton()), + + // 显示面包屑 + crumbs: createToggleHandler(() => settingStore.setCrumbs()), + + // 显示语言切换 + language: createToggleHandler(() => settingStore.setLanguage()), + + // 显示进度条 + nprogress: createToggleHandler(() => settingStore.setNprogress()), + + // 色弱模式 + colorWeak: createToggleHandler( + () => settingStore.setColorWeak(), + () => { + domOperations.setHtmlClass('color-weak', settingStore.colorWeak) + } + ), + + // 水印显示 + watermark: createToggleHandler(() => + settingStore.setWatermarkVisible(!settingStore.watermarkVisible) + ), + + // 菜单展开宽度 + menuOpenWidth: createValueHandler((width: number) => + settingStore.setMenuOpenWidth(width) + ), + + // 标签页风格 + tabStyle: createValueHandler((style: string) => settingStore.setTabStyle(style)), + + // 页面切换动画 + pageTransition: createValueHandler((transition: string) => + settingStore.setPageTransition(transition) + ), + + // 圆角大小 + customRadius: createValueHandler((radius: string) => + settingStore.setCustomRadius(radius) + ) + } + + // 盒子样式处理器 + const boxStyleHandlers = { + // 设置盒子模式 + setBoxMode: (type: 'border-mode' | 'shadow-mode') => { + const { boxBorderMode } = storeToRefs(settingStore) + + // 防止重复设置 + if ( + (type === 'shadow-mode' && boxBorderMode.value === false) || + (type === 'border-mode' && boxBorderMode.value === true) + ) { + return + } + + setTimeout(() => { + domOperations.setRootAttribute('data-box-mode', type) + settingStore.setBorderMode() + }, 50) + } + } + + // 颜色设置处理器 + const colorHandlers = { + // 选择主题色 + selectColor: (theme: string) => { + settingStore.setElementTheme(theme) + settingStore.reload() + } + } + + // 容器设置处理器 + const containerHandlers = { + // 设置容器宽度 + setWidth: (type: ContainerWidthEnum) => { + settingStore.setContainerWidth(type) + settingStore.reload() + } + } + + return { + domOperations, + basicHandlers, + boxStyleHandlers, + colorHandlers, + containerHandlers, + createToggleHandler, + createValueHandler + } +} diff --git a/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts new file mode 100644 index 0000000..f5e4e56 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts @@ -0,0 +1,233 @@ +import { ref, computed, watch } from 'vue' +import { useSettingStore } from '@/store/modules/setting' +import { storeToRefs } from 'pinia' +import { useBreakpoints } from '@vueuse/core' +import AppConfig from '@/config' +import { SystemThemeEnum, MenuTypeEnum } from '@/enums/appEnum' +import { mittBus } from '@/utils/sys' +import { StorageConfig } from '@/utils' +import { useTheme } from '@/hooks/core/useTheme' +import { useCeremony } from '@/hooks/core/useCeremony' +import { useSettingsState } from './useSettingsState' +import { useSettingsHandlers } from './useSettingsHandlers' + +/** + * 设置面板核心逻辑管理 + */ +export function useSettingsPanel() { + const settingStore = useSettingStore() + const { systemThemeType, systemThemeMode, menuType } = storeToRefs(settingStore) + + // Composables + const { openFestival, cleanup } = useCeremony() + const { setSystemTheme, setSystemAutoTheme } = useTheme() + const { initColorWeak } = useSettingsState() + const { domOperations } = useSettingsHandlers() + + // 响应式状态 + const showDrawer = ref(false) + + // 使用 VueUse breakpoints 优化性能 + const breakpoints = useBreakpoints({ tablet: 1000 }) + const isMobile = breakpoints.smaller('tablet') + + // 记录窗口宽度变化前的菜单类型 + const getStoredDesktopMenuType = (): MenuTypeEnum | undefined => { + const storedMenuType = localStorage.getItem(StorageConfig.RESPONSIVE_MENU_TYPE_KEY) + return Object.values(MenuTypeEnum).includes(storedMenuType as MenuTypeEnum) + ? (storedMenuType as MenuTypeEnum) + : undefined + } + + const setStoredDesktopMenuType = (type: MenuTypeEnum) => { + localStorage.setItem(StorageConfig.RESPONSIVE_MENU_TYPE_KEY, type) + } + + const clearStoredDesktopMenuType = () => { + localStorage.removeItem(StorageConfig.RESPONSIVE_MENU_TYPE_KEY) + } + + const storedDesktopMenuType = getStoredDesktopMenuType() + const beforeMenuType = ref(storedDesktopMenuType) + const hasChangedMenu = ref(Boolean(storedDesktopMenuType)) + + // 计算属性 + const systemThemeColor = computed(() => settingStore.systemThemeColor as string) + + // 主题相关处理 + const useThemeHandlers = () => { + // 初始化系统颜色 + const initSystemColor = () => { + if (!AppConfig.systemMainColor.includes(systemThemeColor.value)) { + settingStore.setElementTheme(AppConfig.systemMainColor[0]) + settingStore.reload() + } + } + + // 初始化系统主题 + const initSystemTheme = () => { + if (systemThemeMode.value === SystemThemeEnum.AUTO) { + setSystemAutoTheme() + } else { + setSystemTheme(systemThemeType.value) + } + } + + // 监听系统主题变化 + const listenerSystemTheme = () => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + mediaQuery.addEventListener('change', initSystemTheme) + return () => { + mediaQuery.removeEventListener('change', initSystemTheme) + } + } + + return { + initSystemColor, + initSystemTheme, + listenerSystemTheme + } + } + + // 响应式布局处理 + const useResponsiveLayout = () => { + // 使用 watch 监听断点变化,性能更优 + const stopWatch = watch( + isMobile, + (mobile: boolean) => { + if (mobile) { + // 切换到移动端布局 + if (!hasChangedMenu.value) { + beforeMenuType.value = menuType.value + if (menuType.value !== MenuTypeEnum.LEFT) { + setStoredDesktopMenuType(menuType.value) + useSettingsState().switchMenuLayouts(MenuTypeEnum.LEFT) + hasChangedMenu.value = true + } + } + + settingStore.setMenuOpen(false) + } else { + // 恢复桌面端布局 + if (hasChangedMenu.value && beforeMenuType.value) { + if (menuType.value === MenuTypeEnum.LEFT) { + useSettingsState().switchMenuLayouts(beforeMenuType.value) + } + + clearStoredDesktopMenuType() + hasChangedMenu.value = false + } + + settingStore.setMenuOpen(true) + } + }, + { immediate: true } + ) + + return { stopWatch } + } + + // 抽屉控制 + const useDrawerControl = () => { + // 用于存储 setTimeout 的 ID,以便在需要时清除 + let themeChangeTimer: ReturnType | null = null + + // 打开抽屉 + const handleOpen = () => { + // 清除可能存在的旧定时器 + if (themeChangeTimer) { + clearTimeout(themeChangeTimer) + } + // 延迟添加 theme-change class,避免抽屉打开动画受影响 + themeChangeTimer = setTimeout(() => { + domOperations.setBodyClass('theme-change', true) + themeChangeTimer = null + }, 500) + } + + // 关闭抽屉 + const handleClose = () => { + // 清除未执行的定时器,防止关闭后才添加 class + if (themeChangeTimer) { + clearTimeout(themeChangeTimer) + themeChangeTimer = null + } + // 立即移除 theme-change class + domOperations.setBodyClass('theme-change', false) + } + + // 打开设置 + const openSetting = () => { + showDrawer.value = true + } + + // 关闭设置 + const closeDrawer = () => { + showDrawer.value = false + } + + return { + handleOpen, + handleClose, + openSetting, + closeDrawer + } + } + + // Props 变化监听 + const usePropsWatcher = (props: { open?: boolean }) => { + watch( + () => props.open, + (val: boolean | undefined) => { + if (val !== undefined) { + showDrawer.value = val + } + } + ) + } + + // 初始化设置 + const useSettingsInitializer = () => { + const themeHandlers = useThemeHandlers() + const { openSetting } = useDrawerControl() + const { stopWatch } = useResponsiveLayout() + let themeCleanup: (() => void) | null = null + + const initializeSettings = () => { + mittBus.on('openSetting', openSetting) + themeHandlers.initSystemColor() + themeCleanup = themeHandlers.listenerSystemTheme() + initColorWeak() + + // 设置盒子模式 + const boxMode = settingStore.boxBorderMode ? 'border-mode' : 'shadow-mode' + domOperations.setRootAttribute('data-box-mode', boxMode) + + themeHandlers.initSystemTheme() + openFestival() + } + + const cleanupSettings = () => { + stopWatch() + themeCleanup?.() + cleanup() + } + + return { + initializeSettings, + cleanupSettings + } + } + + return { + // 状态 + showDrawer, + + // 方法组合 + useThemeHandlers, + useResponsiveLayout, + useDrawerControl, + usePropsWatcher, + useSettingsInitializer + } +} diff --git a/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts new file mode 100644 index 0000000..65352d2 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts @@ -0,0 +1,37 @@ +import { useSettingStore } from '@/store/modules/setting' +import { MenuThemeEnum, MenuTypeEnum } from '@/enums/appEnum' + +/** + * 设置状态管理 + */ +export function useSettingsState() { + const settingStore = useSettingStore() + + // 色弱模式初始化 + const initColorWeak = () => { + if (settingStore.colorWeak) { + const el = document.getElementsByTagName('html')[0] + setTimeout(() => { + el.classList.add('color-weak') + }, 100) + } + } + + // 菜单布局切换 + const switchMenuLayouts = (type: MenuTypeEnum) => { + if (type === MenuTypeEnum.LEFT || type === MenuTypeEnum.TOP_LEFT) { + settingStore.setMenuOpen(true) + } + settingStore.switchMenuLayouts(type) + if (type === MenuTypeEnum.DUAL_MENU) { + settingStore.switchMenuStyles(MenuThemeEnum.DESIGN) + settingStore.setMenuOpen(true) + } + } + + return { + // 方法 + initColorWeak, + switchMenuLayouts + } +} diff --git a/admin/src/components/core/layouts/art-settings-panel/index.vue b/admin/src/components/core/layouts/art-settings-panel/index.vue new file mode 100644 index 0000000..0cbf344 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/index.vue @@ -0,0 +1,72 @@ + + + + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/style.scss b/admin/src/components/core/layouts/art-settings-panel/style.scss new file mode 100644 index 0000000..e863074 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/style.scss @@ -0,0 +1,92 @@ +@use '@styles/core/mixin.scss' as *; + +// 设置抽屉模态框样式 +.setting-modal { + background: transparent !important; + + .el-drawer { + // 背景滤镜效果 + background: rgba($color: #fff, $alpha: 50%) !important; + box-shadow: 0 0 30px rgb(0 0 0 / 10%) !important; + + @include backdropBlur(); + + .setting-box-wrap { + display: flex; + flex-wrap: wrap; + align-items: center; + width: calc(100% + 15px); + margin-bottom: 10px; + + .setting-item { + box-sizing: border-box; + width: calc(33.333% - 15px); + margin-right: 15px; + text-align: center; + + .box { + position: relative; + box-sizing: border-box; + display: flex; + height: 52px; + overflow: hidden; + cursor: pointer; + border: 2px solid var(--default-border); + border-radius: 8px; + box-shadow: 0 0 8px 0 rgb(0 0 0 / 10%); + transition: box-shadow 0.1s; + + &.mt-16 { + margin-top: 16px; + } + + &.is-active { + border: 2px solid var(--theme-color); + } + + img { + width: 100%; + height: 100%; + } + } + + .name { + margin-top: 6px; + font-size: 14px; + text-align: center; + } + } + } + } + + // 去除滚动条 + .el-drawer__body::-webkit-scrollbar { + width: 0 !important; + } +} + +.dark { + .setting-modal { + .el-drawer { + background: rgba($color: #000, $alpha: 50%) !important; + + .setting-item { + .box { + border: 2px solid transparent; + } + } + } + } +} + +// 去除火狐浏览器滚动条 +:deep(.el-drawer__body) { + scrollbar-width: none; +} + +// 移动端隐藏 +@media screen and (width <= 800px) { + .mobile-hide { + display: none !important; + } +} diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue new file mode 100644 index 0000000..b6dc9d3 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue @@ -0,0 +1,77 @@ + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue new file mode 100644 index 0000000..86c7a9e --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue @@ -0,0 +1,38 @@ + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue new file mode 100644 index 0000000..05a4b41 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue @@ -0,0 +1,35 @@ + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue new file mode 100644 index 0000000..1f5be72 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue @@ -0,0 +1,33 @@ + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue new file mode 100644 index 0000000..dbcae46 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue @@ -0,0 +1,31 @@ + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue new file mode 100644 index 0000000..61237eb --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue @@ -0,0 +1,44 @@ + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue new file mode 100644 index 0000000..31ef00c --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue new file mode 100644 index 0000000..7b47d1a --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue @@ -0,0 +1,235 @@ + + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue new file mode 100644 index 0000000..85372be --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue @@ -0,0 +1,51 @@ + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue new file mode 100644 index 0000000..e3ead9e --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue @@ -0,0 +1,18 @@ + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue b/admin/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue new file mode 100644 index 0000000..5721027 --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/admin/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue b/admin/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue new file mode 100644 index 0000000..4b46fcd --- /dev/null +++ b/admin/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue @@ -0,0 +1,28 @@ + + + diff --git a/admin/src/components/core/layouts/art-work-tab/index.vue b/admin/src/components/core/layouts/art-work-tab/index.vue new file mode 100644 index 0000000..152ff63 --- /dev/null +++ b/admin/src/components/core/layouts/art-work-tab/index.vue @@ -0,0 +1,584 @@ + + + + + + diff --git a/admin/src/components/core/others/art-menu-right/index.vue b/admin/src/components/core/others/art-menu-right/index.vue new file mode 100644 index 0000000..1cc92ab --- /dev/null +++ b/admin/src/components/core/others/art-menu-right/index.vue @@ -0,0 +1,415 @@ + + + + + + diff --git a/admin/src/components/core/others/art-watermark/index.vue b/admin/src/components/core/others/art-watermark/index.vue new file mode 100644 index 0000000..1d7f06b --- /dev/null +++ b/admin/src/components/core/others/art-watermark/index.vue @@ -0,0 +1,64 @@ + + + + diff --git a/admin/src/components/core/tables/art-table-header/index.vue b/admin/src/components/core/tables/art-table-header/index.vue new file mode 100644 index 0000000..788c2b7 --- /dev/null +++ b/admin/src/components/core/tables/art-table-header/index.vue @@ -0,0 +1,339 @@ + + + + + + diff --git a/admin/src/components/core/tables/art-table/index.vue b/admin/src/components/core/tables/art-table/index.vue new file mode 100644 index 0000000..be99e70 --- /dev/null +++ b/admin/src/components/core/tables/art-table/index.vue @@ -0,0 +1,368 @@ + + + + + + + + + diff --git a/admin/src/components/core/tables/art-table/style.scss b/admin/src/components/core/tables/art-table/style.scss new file mode 100644 index 0000000..67459e8 --- /dev/null +++ b/admin/src/components/core/tables/art-table/style.scss @@ -0,0 +1,99 @@ +.art-table { + position: relative; + height: 100%; + + .el-table { + height: 100%; + margin-top: 10px; + } + + :deep(.el-loading-mask) { + z-index: 100; + background-color: var(--default-box-color) !important; + } + + // Loading 过渡动画 - 消失时淡出 + .loading-fade-leave-active { + transition: opacity 0.3s ease-out; + } + + .loading-fade-leave-to { + opacity: 0; + } + + // 空状态垂直居中 + &.is-empty { + :deep(.el-scrollbar__wrap) { + display: flex; + } + } + + .pagination { + display: flex; + margin-top: 13px; + + :deep(.el-select) { + width: 102px !important; + } + + // 分页对齐方式 + &.left { + justify-content: flex-start; + } + + &.center { + justify-content: center; + } + + &.right { + justify-content: flex-end; + } + + // 自定义分页组件样式 + &.custom-pagination { + :deep(.el-pagination) { + .btn-prev, + .btn-next { + background-color: transparent; + border: 1px solid var(--art-gray-300); + transition: border-color 0.15s; + + &:hover:not(.is-disabled) { + color: var(--theme-color); + border-color: var(--theme-color); + } + } + + li { + box-sizing: border-box; + font-weight: 400 !important; + background-color: transparent; + border: 1px solid var(--art-gray-300); + transition: border-color 0.15s; + + &.is-active { + font-weight: 400; + color: #fff; + background-color: var(--theme-color); + border: 1px solid var(--theme-color); + } + + &:hover:not(.is-disabled) { + border-color: var(--theme-color); + } + } + } + } + } +} + +// 移动端分页 +@media (width <= 640px) { + :deep(.el-pagination) { + display: flex; + flex-wrap: wrap; + gap: 15px 0; + align-items: center; + justify-content: center; + } +} diff --git a/admin/src/components/core/text-effect/art-count-to/index.vue b/admin/src/components/core/text-effect/art-count-to/index.vue new file mode 100644 index 0000000..7fb104b --- /dev/null +++ b/admin/src/components/core/text-effect/art-count-to/index.vue @@ -0,0 +1,310 @@ + + + + diff --git a/admin/src/components/core/text-effect/art-festival-text-scroll/index.vue b/admin/src/components/core/text-effect/art-festival-text-scroll/index.vue new file mode 100644 index 0000000..770b457 --- /dev/null +++ b/admin/src/components/core/text-effect/art-festival-text-scroll/index.vue @@ -0,0 +1,32 @@ + + + + diff --git a/admin/src/components/core/text-effect/art-text-scroll/index.vue b/admin/src/components/core/text-effect/art-text-scroll/index.vue new file mode 100644 index 0000000..90be30f --- /dev/null +++ b/admin/src/components/core/text-effect/art-text-scroll/index.vue @@ -0,0 +1,285 @@ + + + + diff --git a/admin/src/components/core/theme/theme-svg/index.vue b/admin/src/components/core/theme/theme-svg/index.vue new file mode 100644 index 0000000..0b565a9 --- /dev/null +++ b/admin/src/components/core/theme/theme-svg/index.vue @@ -0,0 +1,100 @@ + + + + + + + diff --git a/admin/src/components/core/views/exception/ArtException.vue b/admin/src/components/core/views/exception/ArtException.vue new file mode 100644 index 0000000..4be59ae --- /dev/null +++ b/admin/src/components/core/views/exception/ArtException.vue @@ -0,0 +1,55 @@ + + + diff --git a/admin/src/components/core/views/login/AuthTopBar.vue b/admin/src/components/core/views/login/AuthTopBar.vue new file mode 100644 index 0000000..9455253 --- /dev/null +++ b/admin/src/components/core/views/login/AuthTopBar.vue @@ -0,0 +1,149 @@ + + + + + + diff --git a/admin/src/components/core/views/login/LoginLeftView.vue b/admin/src/components/core/views/login/LoginLeftView.vue new file mode 100644 index 0000000..af6a904 --- /dev/null +++ b/admin/src/components/core/views/login/LoginLeftView.vue @@ -0,0 +1,602 @@ + + + + + + diff --git a/admin/src/components/core/views/result/ArtResultPage.vue b/admin/src/components/core/views/result/ArtResultPage.vue new file mode 100644 index 0000000..b2eca48 --- /dev/null +++ b/admin/src/components/core/views/result/ArtResultPage.vue @@ -0,0 +1,43 @@ + + + diff --git a/admin/src/components/core/widget/art-icon-button/index.vue b/admin/src/components/core/widget/art-icon-button/index.vue new file mode 100644 index 0000000..760888b --- /dev/null +++ b/admin/src/components/core/widget/art-icon-button/index.vue @@ -0,0 +1,23 @@ + + + + diff --git a/admin/src/config/assets/images.ts b/admin/src/config/assets/images.ts new file mode 100644 index 0000000..f3e89dd --- /dev/null +++ b/admin/src/config/assets/images.ts @@ -0,0 +1,61 @@ +/** + * 配置图片资源 + * + * 统一管理设置中心使用的预览图片资源。 + * 包含主题样式、菜单布局、菜单风格的预览图。 + * + * ## 图片分类 + * + * - themeStyles: 系统主题预览图(亮色/暗色/自动) + * - menuLayouts: 菜单布局预览图(左侧/顶部/混合/双栏) + * - menuStyles: 菜单风格预览图(设计/暗色/亮色) + * + * @module config/assets/images + * @author Art Design Pro Team + */ + +import lightTheme from '@imgs/settings/theme_styles/light.png' +import darkTheme from '@imgs/settings/theme_styles/dark.png' +import systemTheme from '@imgs/settings/theme_styles/system.png' +import verticalLayout from '@imgs/settings/menu_layouts/vertical.png' +import horizontalLayout from '@imgs/settings/menu_layouts/horizontal.png' +import mixedLayout from '@imgs/settings/menu_layouts/mixed.png' +import dualColumnLayout from '@imgs/settings/menu_layouts/dual_column.png' +import designStyle from '@imgs/settings/menu_styles/design.png' +import darkStyle from '@imgs/settings/menu_styles/dark.png' +import lightStyle from '@imgs/settings/menu_styles/light.png' + +/** + * 配置中心图片资源对象 + */ +export const configImages = { + /** 系统主题预览图 */ + themeStyles: { + /** 亮色主题 */ + light: lightTheme, + /** 暗色主题 */ + dark: darkTheme, + /** 自动主题(跟随系统) */ + system: systemTheme + }, + /** 菜单布局预览图 */ + menuLayouts: { + /** 左侧菜单 */ + vertical: verticalLayout, + /** 顶部菜单 */ + horizontal: horizontalLayout, + /** 混合菜单 */ + mixed: mixedLayout, + /** 双栏菜单 */ + dualColumn: dualColumnLayout + }, + /** 菜单风格预览图 */ + menuStyles: { + /** 设计风格 */ + design: designStyle, + /** 暗色风格 */ + dark: darkStyle, + /** 亮色风格 */ + light: lightStyle + } +} diff --git a/admin/src/config/index.ts b/admin/src/config/index.ts new file mode 100644 index 0000000..ddbc958 --- /dev/null +++ b/admin/src/config/index.ts @@ -0,0 +1,135 @@ +/** + * 系统全局配置 + * + * 这是系统的核心配置文件,集中管理所有全局配置项。 + * 包含系统信息、主题样式、菜单布局、颜色方案等所有可配置项。 + * + * ## 主要功能 + * + * - 系统信息 - 系统名称等基础信息 + * - 主题配置 - 亮色/暗色/自动主题的样式配置 + * - 菜单配置 - 菜单布局、主题、宽度等配置 + * - 颜色方案 - 系统主色和预设颜色列表 + * - 快速入口 - 快速入口应用和链接配置 + * - 顶部栏配置 - 顶部栏功能模块配置 + * + * ## 配置项说明 + * + * - systemInfo: 系统基础信息(名称等) + * - systemThemeStyles: 系统主题样式映射 + * - settingThemeList: 可选的系统主题列表 + * - menuLayoutList: 可选的菜单布局列表 + * - themeList: 菜单主题样式列表 + * - darkMenuStyles: 暗黑模式下的菜单样式 + * - systemMainColor: 预设的系统主色列表 + * - fastEnter: 快速入口配置 + * - headerBar: 顶部栏功能配置 + * + * @module config + * @author Art Design Pro Team + */ + +import { MenuThemeEnum, MenuTypeEnum, SystemThemeEnum } from '@/enums/appEnum' +import { SystemConfig } from '@/types/config' +import { configImages } from './assets/images' +import fastEnterConfig from './modules/fastEnter' +import { headerBarConfig } from './modules/headerBar' + +const appConfig: SystemConfig = { + // 系统信息 + systemInfo: { + name: 'all-kefu 管理后台' // 系统名称 + }, + // 系统主题 + systemThemeStyles: { + [SystemThemeEnum.LIGHT]: { className: '' }, + [SystemThemeEnum.DARK]: { className: SystemThemeEnum.DARK } + }, + // 系统主题列表 + settingThemeList: [ + { + name: 'Light', + theme: SystemThemeEnum.LIGHT, + color: ['#fff', '#fff'], + leftLineColor: '#EDEEF0', + rightLineColor: '#EDEEF0', + img: configImages.themeStyles.light + }, + { + name: 'Dark', + theme: SystemThemeEnum.DARK, + color: ['#22252A'], + leftLineColor: '#3F4257', + rightLineColor: '#3F4257', + img: configImages.themeStyles.dark + }, + { + name: 'System', + theme: SystemThemeEnum.AUTO, + color: ['#fff', '#22252A'], + leftLineColor: '#EDEEF0', + rightLineColor: '#3F4257', + img: configImages.themeStyles.system + } + ], + // 菜单布局列表 + menuLayoutList: [ + { name: 'Left', value: MenuTypeEnum.LEFT, img: configImages.menuLayouts.vertical }, + { name: 'Top', value: MenuTypeEnum.TOP, img: configImages.menuLayouts.horizontal }, + { name: 'Mixed', value: MenuTypeEnum.TOP_LEFT, img: configImages.menuLayouts.mixed }, + { name: 'Dual Column', value: MenuTypeEnum.DUAL_MENU, img: configImages.menuLayouts.dualColumn } + ], + // 菜单主题列表 + themeList: [ + { + theme: MenuThemeEnum.DESIGN, + background: '#FFFFFF', + systemNameColor: 'var(--art-gray-800)', + iconColor: '#6B6B6B', + textColor: '#29343D', + img: configImages.menuStyles.design + }, + { + theme: MenuThemeEnum.DARK, + background: '#191A23', + systemNameColor: '#D9DADB', + iconColor: '#BABBBD', + textColor: '#BABBBD', + img: configImages.menuStyles.dark + }, + { + theme: MenuThemeEnum.LIGHT, + background: '#ffffff', + systemNameColor: 'var(--art-gray-800)', + iconColor: '#6B6B6B', + textColor: '#29343D', + img: configImages.menuStyles.light + } + ], + // 暗黑模式菜单样式 + darkMenuStyles: [ + { + theme: MenuThemeEnum.DARK, + background: 'var(--default-box-color)', + systemNameColor: '#DDDDDD', + iconColor: '#BABBBD', + textColor: 'rgba(#FFFFFF, 0.7)' + } + ], + // 系统主色 + systemMainColor: [ + '#5D87FF', + '#B48DF3', + '#1D84FF', + '#60C041', + '#38C0FC', + '#F9901F', + '#FF80C8' + ] as const, + // 快速入口配置 + fastEnter: fastEnterConfig, + // 顶部栏功能配置 + headerBar: headerBarConfig +} + +export default Object.freeze(appConfig) diff --git a/admin/src/config/modules/component.ts b/admin/src/config/modules/component.ts new file mode 100644 index 0000000..bc709e0 --- /dev/null +++ b/admin/src/config/modules/component.ts @@ -0,0 +1,105 @@ +/** + * 全局组件配置 + * + * 统一管理系统级全局组件的注册。 + * 这些组件会在应用启动时全局注册,可在任何地方使用。 + * + * ## 主要功能 + * + * - 组件配置 - 集中管理全局组件的配置信息 + * - 异步加载 - 使用 defineAsyncComponent 实现按需加载 + * - 开关控制 - 支持通过 enabled 字段启用/禁用组件 + * - 配置查询 - 提供工具函数快速查询组件配置 + * + * @module config/component + * @author Art Design Pro Team + */ + +import { defineAsyncComponent } from 'vue' + +/** + * 全局组件配置列表 + */ +export const globalComponentsConfig: GlobalComponentConfig[] = [ + { + name: '设置面板', + key: 'settings-panel', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-settings-panel/index.vue') + ), + enabled: true + }, + { + name: '全局搜索', + key: 'global-search', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-global-search/index.vue') + ), + enabled: true + }, + { + name: '锁屏', + key: 'screen-lock', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-screen-lock/index.vue') + ), + enabled: true + }, + { + name: '聊天窗口', + key: 'chat-window', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-chat-window/index.vue') + ), + enabled: true + }, + { + name: '礼花效果', + key: 'fireworks-effect', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-fireworks-effect/index.vue') + ), + enabled: true + }, + { + name: '水印效果', + key: 'watermark', + component: defineAsyncComponent( + () => import('@/components/core/others/art-watermark/index.vue') + ), + enabled: true + } +] + +/** + * 全局组件配置接口 + */ +export interface GlobalComponentConfig { + /** 组件名称 */ + name: string + /** 组件标识 */ + key: string + /** 组件 */ + component: any + /** 是否启用 */ + enabled?: boolean + /** 组件描述 */ + description?: string +} + +/** + * 获取启用的全局组件 + * @returns 已启用的组件配置列表 + */ +export const getEnabledGlobalComponents = () => { + return globalComponentsConfig.filter((config) => config.enabled !== false) +} + +/** + * 根据 key 获取组件配置 + * @param key 组件标识 + * @returns 组件配置对象 + */ +export const getGlobalComponentByKey = (key: string) => { + return globalComponentsConfig.find((config) => config.key === key) +} diff --git a/admin/src/config/modules/fastEnter.ts b/admin/src/config/modules/fastEnter.ts new file mode 100644 index 0000000..4117136 --- /dev/null +++ b/admin/src/config/modules/fastEnter.ts @@ -0,0 +1,66 @@ +/** + * 快速入口配置 + * 包含:应用列表、快速链接等 + */ +import type { FastEnterConfig } from '@/types/config' + +const fastEnterConfig: FastEnterConfig = { + // 显示条件(屏幕宽度) + minWidth: 1200, + // 应用列表 + applications: [ + { + name: '仪表盘', + description: '平台概览与数据统计', + icon: 'ri:dashboard-line', + iconColor: '#377dff', + enabled: true, + order: 1, + routeName: 'Console' + }, + { + name: '会话与线索', + description: '会话列表与询盘推送', + icon: 'ri:chat-3-line', + iconColor: '#13DEB9', + enabled: true, + order: 2, + routeName: 'ConversationList' + }, + { + name: '配置中心', + description: '租户配置与品牌管理', + icon: 'ri:settings-4-line', + iconColor: '#ffb100', + enabled: true, + order: 3, + routeName: 'ConfigCenter' + }, + { + name: '统计分析', + description: '卡片/漏斗/趋势/分布', + icon: 'ri:bar-chart-2-line', + iconColor: '#ff6b6b', + enabled: true, + order: 4, + routeName: 'StatsOverview' + } + ], + // 快速链接 + quickLinks: [ + { + name: '登录', + enabled: true, + order: 1, + routeName: 'Login' + }, + { + name: '修改密码', + enabled: true, + order: 2, + routeName: 'ForceChangePassword' + } + ] +} + +export default Object.freeze(fastEnterConfig) diff --git a/admin/src/config/modules/festival.ts b/admin/src/config/modules/festival.ts new file mode 100644 index 0000000..39cd790 --- /dev/null +++ b/admin/src/config/modules/festival.ts @@ -0,0 +1,51 @@ +/** + * 节日庆祝配置 + * + * 配置系统的节日烟花效果和祝福文本。 + * 支持单日节日和跨日期节日,可自定义烟花播放次数。 + * + * ## 配置说明 + * + * - name: 节日名称 + * - date: 节日开始日期(格式:YYYY-MM-DD) + * - endDate: 节日结束日期(可选,用于跨日期节日) + * - image: 烟花图片(需要预先导入) + * - scrollText: 滚动显示的祝福文本 + * - count: 烟花播放次数(可选,默认为 3 次) + * + * ## 注意事项 + * + * - 图片需要预先导入并在配置中引用 + * - 跨日期节日会在整个日期范围内生效 + * - 每个用户每天只会播放一次烟花效果 + * + * @module config/modules/festival + * @author Art Design Pro Team + */ + +import { FestivalConfig } from '@/types/config' + +// 导入烟花图片(根据需要取消注释) +// import sd from '@imgs/ceremony/sd.png' +// import yd from '@imgs/ceremony/yd.png' + +export const festivalConfigList: FestivalConfig[] = [ + // 跨日期示例 + // { + // name: 'v3.0 Sass 升级至 TailwindCSS', + // date: '2025-11-03', + // endDate: '2025-11-09', + // image: '', + // count: 3, + // scrollText: + // '🚀 系统 v3.0 测试阶段正式开启!测试周期为 11 月 3 日 - 11 月 16 日,通过 TailwindCSS 重构样式体系、统一 Iconify 图标方案,带来更高效现代的开发体验,正式发布敬请期待~' + // } + // 单日示例:圣诞节 + // { + // name: '圣诞节', + // date: '2024-12-25', + // image: sd, + // count: 3 // 可选,不设置则使用默认值 3 次 + // scrollText: 'Merry Christmas!Art Design Pro 祝您圣诞快乐,愿节日的欢乐与祝福如雪花般纷至沓来!', + // } +] diff --git a/admin/src/config/modules/headerBar.ts b/admin/src/config/modules/headerBar.ts new file mode 100644 index 0000000..a420e82 --- /dev/null +++ b/admin/src/config/modules/headerBar.ts @@ -0,0 +1,63 @@ +/** + * 顶部栏功能配置 + * + * 统一管理顶部栏各个功能模块的启用状态。 + * 通过修改此配置文件可以快速启用或禁用顶部栏的功能按钮。 + * + * @module config/headerBar + * @author Art Design Pro Team + */ + +import { HeaderBarFeatureConfig } from '@/types' + +/** + * 顶部栏功能配置对象 + */ +export const headerBarConfig: HeaderBarFeatureConfig = { + menuButton: { + enabled: true, + description: '控制左侧菜单的展开/收起按钮' + }, + refreshButton: { + enabled: true, + description: '页面刷新按钮' + }, + fastEnter: { + enabled: true, + description: '快速入口功能,提供常用应用和链接的快速访问' + }, + breadcrumb: { + enabled: true, + description: '面包屑导航,显示当前页面路径' + }, + globalSearch: { + enabled: true, + description: '全局搜索功能,支持快捷键 Ctrl+K 或 Cmd+K' + }, + fullscreen: { + enabled: true, + description: '全屏切换功能' + }, + notification: { + enabled: true, + description: '通知中心,显示系统通知和消息' + }, + chat: { + enabled: true, + description: '聊天功能,提供实时沟通' + }, + language: { + enabled: true, + description: '多语言切换功能' + }, + settings: { + enabled: true, + description: '系统设置面板' + }, + themeToggle: { + enabled: true, + description: '主题切换功能(明暗主题)' + } +} + +export default headerBarConfig diff --git a/admin/src/config/setting.ts b/admin/src/config/setting.ts new file mode 100644 index 0000000..94f2d2c --- /dev/null +++ b/admin/src/config/setting.ts @@ -0,0 +1,109 @@ +/** + * 系统设置默认值配置 + * + * 统一管理系统设置的所有默认值 + * + * ## 主要功能 + * + * - 菜单相关默认配置 + * - 主题相关默认配置 + * - 界面显示默认配置 + * - 功能开关默认配置 + * - 样式相关默认配置 + * + * ## 注意事项 + * + * 1. 修改此文件的配置项时,需要同步更新以下文件: + * - src/components/core/layouts/art-settings-panel/widget/SettingActions.vue(复制配置和重置配置逻辑) + * - src/store/modules/setting.ts(Store 状态定义) + * 2. 可以通过设置面板的"复制配置"按钮快速生成配置代码 + * 3. 枚举类型的值需要与 src/enums/appEnum.ts 中的定义保持一致 + */ + +import AppConfig from '@/config' +import { SystemThemeEnum, MenuThemeEnum, MenuTypeEnum, ContainerWidthEnum } from '@/enums/appEnum' + +/** + * 系统设置默认值配置 + */ +export const SETTING_DEFAULT_CONFIG = { + /** 菜单类型 */ + menuType: MenuTypeEnum.LEFT, + /** 菜单展开宽度 */ + menuOpenWidth: 230, + /** 菜单是否展开 */ + menuOpen: true, + /** 双菜单是否显示文本 */ + dualMenuShowText: false, + /** 系统主题类型 */ + systemThemeType: SystemThemeEnum.AUTO, + /** 系统主题模式 */ + systemThemeMode: SystemThemeEnum.AUTO, + /** 菜单风格 */ + menuThemeType: MenuThemeEnum.DESIGN, + /** 系统主题颜色 */ + systemThemeColor: AppConfig.systemMainColor[0], + /** 是否显示菜单按钮 */ + showMenuButton: true, + /** 是否显示快速入口 */ + showFastEnter: true, + /** 是否显示刷新按钮 */ + showRefreshButton: true, + /** 是否显示面包屑 */ + showCrumbs: true, + /** 是否显示工作台标签 */ + showWorkTab: true, + /** 是否显示语言切换 */ + showLanguage: true, + /** 是否显示进度条 */ + showNprogress: false, + /** 是否显示设置引导 */ + showSettingGuide: true, + /** 是否显示节日文本 */ + showFestivalText: false, + /** 是否显示水印 */ + watermarkVisible: false, + /** 是否自动关闭 */ + autoClose: false, + /** 是否唯一展开 */ + uniqueOpened: true, + /** 是否色弱模式 */ + colorWeak: false, + /** 是否刷新 */ + refresh: false, + /** 是否加载节日烟花 */ + holidayFireworksLoaded: false, + /** 边框模式 */ + boxBorderMode: true, + /** 页面过渡效果 */ + pageTransition: 'slide-left', + /** 标签页样式 */ + tabStyle: 'tab-default', + /** 自定义圆角 */ + customRadius: '0.75', + /** 容器宽度 */ + containerWidth: ContainerWidthEnum.FULL, + /** 节日日期 */ + festivalDate: '' +} + +/** + * 获取设置默认值 + * @returns 设置默认值对象 + */ +export function getSettingDefaults() { + return { ...SETTING_DEFAULT_CONFIG } +} + +/** + * 重置为默认设置 + * @param currentSettings 当前设置对象 + */ +export function resetToDefaults(currentSettings: Record) { + const defaults = getSettingDefaults() + Object.keys(defaults).forEach((key) => { + if (key in currentSettings) { + currentSettings[key] = defaults[key as keyof typeof defaults] + } + }) +} diff --git a/admin/src/directives/business/ripple.ts b/admin/src/directives/business/ripple.ts new file mode 100644 index 0000000..e65470e --- /dev/null +++ b/admin/src/directives/business/ripple.ts @@ -0,0 +1,116 @@ +/** + * v-ripple 水波纹效果指令 + * + * 为元素添加 Material Design 风格的水波纹点击效果。 + * 点击时从点击位置扩散出圆形水波纹动画,提升交互体验。 + * + * ## 主要功能 + * + * - 水波纹动画 - 点击时从点击位置扩散圆形波纹 + * - 自适应大小 - 根据元素尺寸自动调整波纹大小和动画时长 + * - 智能配色 - 自动识别按钮类型,使用合适的波纹颜色 + * - 自定义颜色 - 支持通过参数自定义波纹颜色 + * - 性能优化 - 使用 requestAnimationFrame 和自动清理机制 + * + * ## 使用示例 + * + * ```vue + * + * ``` + * + * ## 颜色规则 + * + * - 有色按钮(primary、success、warning 等):使用白色半透明波纹 + * - 默认按钮:使用主题色半透明波纹 + * - 自定义:通过 color 参数指定任意颜色 + * + * @module directives/ripple + * @author Art Design Pro Team + */ + +import type { App, Directive, DirectiveBinding } from 'vue' + +export interface RippleOptions { + /** 水波纹颜色 */ + color?: string +} + +export type RippleDirective = Directive + +export const vRipple: RippleDirective = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + // 获取指令的配置参数 + const options: RippleOptions = binding.value || {} + + // 设置元素为相对定位,并隐藏溢出部分 + el.style.position = 'relative' + el.style.overflow = 'hidden' + + // 点击事件处理 + el.addEventListener('mousedown', (e: MouseEvent) => { + const rect = el.getBoundingClientRect() + const left = e.clientX - rect.left + const top = e.clientY - rect.top + + // 创建水波纹元素 + const ripple = document.createElement('div') + const diameter = Math.max(el.clientWidth, el.clientHeight) + const radius = diameter / 2 + + // 根据直径计算动画时间(直径越大,动画时间越长) + const baseTime = 600 // 基础动画时间(毫秒) + const scaleFactor = 0.5 // 缩放因子 + const animationDuration = baseTime + diameter * scaleFactor + + // 设置水波纹的尺寸和位置 + ripple.style.width = ripple.style.height = `${diameter}px` + ripple.style.left = `${left - radius}px` + ripple.style.top = `${top - radius}px` + ripple.style.position = 'absolute' + ripple.style.borderRadius = '50%' + ripple.style.pointerEvents = 'none' + + // 判断是否为有色按钮(Element Plus 按钮类型) + const buttonTypes = ['primary', 'info', 'warning', 'danger', 'success'].map( + (type) => `el-button--${type}` + ) + const isColoredButton = buttonTypes.some((type) => el.classList.contains(type)) + const defaultColor = isColoredButton + ? 'rgba(255, 255, 255, 0.25)' // 有色按钮使用白色水波纹 + : 'var(--el-color-primary-light-7)' // 默认按钮使用主题色水波纹 + + // 设置水波纹颜色、初始状态和过渡效果 + ripple.style.backgroundColor = options.color || defaultColor + ripple.style.transform = 'scale(0)' + ripple.style.transition = `transform ${animationDuration}ms cubic-bezier(0.3, 0, 0.2, 1), opacity ${animationDuration}ms cubic-bezier(0.3, 0, 0.5, 1)` + ripple.style.zIndex = '1' + + // 添加水波纹元素到DOM中 + el.appendChild(ripple) + + // 触发动画 + requestAnimationFrame(() => { + ripple.style.transform = 'scale(2)' + ripple.style.opacity = '0' + }) + + // 动画结束后移除水波纹元素 + setTimeout(() => { + ripple.remove() + }, animationDuration + 500) // 增加500ms缓冲时间 + }) + } +} + +export function setupRippleDirective(app: App) { + app.directive('ripple', vRipple) +} diff --git a/admin/src/directives/core/auth.ts b/admin/src/directives/core/auth.ts new file mode 100644 index 0000000..3d244c7 --- /dev/null +++ b/admin/src/directives/core/auth.ts @@ -0,0 +1,66 @@ +/** + * v-auth 权限指令 + * + * 适用于后端权限控制模式,基于权限标识控制 DOM 元素的显示和隐藏。 + * 如果用户没有对应权限,元素将从 DOM 中移除。 + * + * ## 主要功能 + * + * - 权限验证 - 根据路由 meta 中的权限列表验证用户权限 + * - DOM 控制 - 无权限时自动移除元素,而非隐藏 + * - 响应式更新 - 权限变化时自动更新元素状态 + * + * ## 使用示例 + * + * ```vue + * + * 新增 + * + * + * 编辑 + * + * + * 删除 + * ``` + * + * ## 注意事项 + * + * - 该指令会直接移除 DOM 元素,而不是使用 v-if 隐藏 + * - 权限列表从当前路由的 meta.authList 中获取 + * + * @module directives/auth + * @author Art Design Pro Team + */ + +import { router } from '@/router' +import { App, Directive, DirectiveBinding } from 'vue' + +export type AuthDirective = Directive + +function checkAuthPermission(el: HTMLElement, binding: DirectiveBinding): void { + // 获取当前路由的权限列表 + const authList = (router.currentRoute.value.meta.authList as Array<{ authMark: string }>) || [] + + // 检查是否有对应的权限标识 + const hasPermission = authList.some((item) => item.authMark === binding.value) + + // 如果没有权限,移除元素 + if (!hasPermission) { + removeElement(el) + } +} + +function removeElement(el: HTMLElement): void { + if (el.parentNode) { + el.parentNode.removeChild(el) + } +} + +const authDirective: AuthDirective = { + mounted: checkAuthPermission, + updated: checkAuthPermission +} + +export function setupAuthDirective(app: App): void { + app.directive('auth', authDirective) +} diff --git a/admin/src/directives/core/roles.ts b/admin/src/directives/core/roles.ts new file mode 100644 index 0000000..f1db35b --- /dev/null +++ b/admin/src/directives/core/roles.ts @@ -0,0 +1,87 @@ +/** + * v-roles 角色权限指令 + * + * 基于用户角色控制 DOM 元素的显示和隐藏。 + * 只要用户拥有指定角色中的任意一个,元素就会显示,否则从 DOM 中移除。 + * + * ## 主要功能 + * + * - 角色验证 - 检查用户是否拥有指定角色 + * - 多角色支持 - 支持单个角色或多个角色(满足其一即可) + * - DOM 控制 - 无权限时自动移除元素,而非隐藏 + * - 响应式更新 - 角色变化时自动更新元素状态 + * + * ## 使用示例 + * + * ```vue + * + * ``` + * + * ## 权限逻辑 + * + * - 用户角色从 userStore.getUserInfo.roles 获取 + * - 只要用户拥有指定角色中的任意一个,元素就会显示 + * - 如果用户没有任何角色或不满足条件,元素将被移除 + * + * ## 注意事项 + * + * - 该指令会直接移除 DOM 元素,而不是使用 v-if 隐藏 + * - 适用于基于角色的粗粒度权限控制 + * - 如需基于具体操作的细粒度权限控制,请使用 v-auth 指令 + * + * @module directives/roles + * @author Art Design Pro Team + */ + +import { useUserStore } from '@/store/modules/user' +import { App, Directive, DirectiveBinding } from 'vue' + +export type RolesDirective = Directive + +function checkRolePermission(el: HTMLElement, binding: DirectiveBinding): void { + const userStore = useUserStore() + const userRoles = userStore.getUserInfo.roles + + // 如果用户角色为空或未定义,移除元素 + if (!userRoles?.length) { + removeElement(el) + return + } + + // 确保指令值为数组格式 + const requiredRoles = Array.isArray(binding.value) ? binding.value : [binding.value] + + // 检查用户是否具有所需角色之一 + const hasPermission = requiredRoles.some((role: string) => userRoles.includes(role)) + + // 如果没有权限,安全地移除元素 + if (!hasPermission) { + removeElement(el) + } +} + +function removeElement(el: HTMLElement): void { + if (el.parentNode) { + el.parentNode.removeChild(el) + } +} + +const rolesDirective: RolesDirective = { + mounted: checkRolePermission, + updated: checkRolePermission +} + +export function setupRolesDirective(app: App): void { + app.directive('roles', rolesDirective) +} diff --git a/admin/src/directives/index.ts b/admin/src/directives/index.ts new file mode 100644 index 0000000..b396be0 --- /dev/null +++ b/admin/src/directives/index.ts @@ -0,0 +1,12 @@ +import type { App } from 'vue' +import { setupAuthDirective, type AuthDirective } from './core/auth' +import { setupRippleDirective, type RippleDirective } from './business/ripple' +import { setupRolesDirective, type RolesDirective } from './core/roles' + +export function setupGlobDirectives(app: App) { + setupAuthDirective(app) // 权限指令 + setupRolesDirective(app) // 角色权限指令 + setupRippleDirective(app) // 水波纹指令 +} + +export type { AuthDirective, RippleDirective, RolesDirective } diff --git a/admin/src/enums/appEnum.ts b/admin/src/enums/appEnum.ts new file mode 100644 index 0000000..a39c278 --- /dev/null +++ b/admin/src/enums/appEnum.ts @@ -0,0 +1,81 @@ +/** + * 系统级别枚举定义模块 + * + * ## 主要功能 + * + * - 菜单类型枚举(左侧、顶部、混合、双栏) + * - 主题类型枚举(亮色、暗色、自动) + * - 菜单主题枚举(设计、亮色、暗色) + * - 语言类型枚举(中文、英文) + * - 容器宽度枚举(全屏、固定) + * - 菜单宽度枚举(收起宽度) + * + * @module enums/appEnum + * @author Art Design Pro Team + */ + +/** + * 菜单类型 + */ +export enum MenuTypeEnum { + /** 左侧菜单 */ + LEFT = 'left', + /** 顶部菜单 */ + TOP = 'top', + /** 顶部+左侧菜单 */ + TOP_LEFT = 'top-left', + /** 双栏菜单 */ + DUAL_MENU = 'dual-menu' +} + +/** + * 系统主题 + */ +export enum SystemThemeEnum { + /** 暗色主题 */ + DARK = 'dark', + /** 亮色主题 */ + LIGHT = 'light', + /** 自动主题(跟随系统) */ + AUTO = 'auto' +} + +/** + * 菜单主题 + */ +export enum MenuThemeEnum { + /** 暗色主题 */ + DARK = 'dark', + /** 亮色主题 */ + LIGHT = 'light', + /** 设计主题 */ + DESIGN = 'design' +} + +/** + * 菜单宽度 + */ +export enum MenuWidth { + /** 收起宽度 */ + CLOSE = '64px' +} + +/** + * 语言类型 + */ +export enum LanguageEnum { + /** 中文 */ + ZH = 'zh', + /** 英文 */ + EN = 'en' +} + +/** + * 容器宽度 + */ +export enum ContainerWidthEnum { + /** 全屏宽度 */ + FULL = '100%', + /** 固定宽度 */ + BOXED = '1200px' +} diff --git a/admin/src/enums/formEnum.ts b/admin/src/enums/formEnum.ts new file mode 100644 index 0000000..8e9b3b4 --- /dev/null +++ b/admin/src/enums/formEnum.ts @@ -0,0 +1,24 @@ +/** + * 表单相关枚举定义模块 + * + * ## 主要功能 + * + * - 页面模式枚举(新增、编辑) + * - 表格尺寸枚举(默认、紧凑、宽松) + * + * @module enums/formEnum + * @author Art Design Pro Team + */ + +// 页面类型 +export enum PageModeEnum { + Add, // 新增 + Edit // 编辑 +} + +// 表格大小 +export enum TableSizeEnum { + DEFAULT = 'default', + SMALL = 'small', + LARGE = 'large' +} diff --git a/admin/src/env.d.ts b/admin/src/env.d.ts new file mode 100644 index 0000000..73846e2 --- /dev/null +++ b/admin/src/env.d.ts @@ -0,0 +1,8 @@ +/// + +declare module 'nprogress' + +declare module 'crypto-js' + +// 全局变量声明 +declare const __APP_VERSION__: string // 版本号 diff --git a/admin/src/hooks/core/useAppMode.ts b/admin/src/hooks/core/useAppMode.ts new file mode 100644 index 0000000..c39cd9e --- /dev/null +++ b/admin/src/hooks/core/useAppMode.ts @@ -0,0 +1,45 @@ +/** + * useAppMode - 应用模式管理 + * + * 提供应用访问模式的判断和管理功能,支持前端和后端两种权限控制模式。 + * 根据环境变量 VITE_ACCESS_MODE 自动识别当前运行模式。 + * + * ## 主要功能 + * + * 1. 模式识别 - 自动识别前端模式或后端模式 + * 2. 前端模式 - 权限由前端路由配置控制,适合小型项目或演示环境 + * 3. 后端模式 - 权限由后端接口返回的菜单数据控制,适合企业级应用 + * 4. 响应式状态 - 提供响应式的模式判断,方便在组件中使用 + * + * @module useAppMode + * @author Art Design Pro Team + */ + +import { computed } from 'vue' + +export function useAppMode() { + // 获取访问模式配置 + const accessMode = import.meta.env.VITE_ACCESS_MODE + + /** + * 是否为前端控制模式 + * 前端模式:权限由前端路由配置控制 + */ + const isFrontendMode = computed(() => accessMode === 'frontend') + /** + * 是否为后端控制模式 + * 后端模式:权限由后端接口返回的菜单数据控制 + */ + const isBackendMode = computed(() => accessMode === 'backend') + + /** + * 当前应用模式 + */ + const currentMode = computed(() => accessMode) + + return { + isFrontendMode, + isBackendMode, + currentMode + } +} diff --git a/admin/src/hooks/core/useAuth.ts b/admin/src/hooks/core/useAuth.ts new file mode 100644 index 0000000..283b859 --- /dev/null +++ b/admin/src/hooks/core/useAuth.ts @@ -0,0 +1,74 @@ +/** + * useAuth - 权限验证管理 + * + * 提供统一的权限验证功能,支持前端和后端两种权限模式。 + * 用于控制页面按钮、操作等功能的显示和访问权限。 + * + * ## 主要功能 + * + * 1. 权限检查 - 检查用户是否拥有指定的权限标识 + * 2. 双模式支持 - 自动适配前端模式和后端模式的权限验证 + * 3. 前端模式 - 从用户信息中获取按钮权限列表(如 ['add', 'edit', 'delete']) + * 4. 后端模式 - 从路由 meta 配置中获取权限列表(如 [{ authMark: 'add' }]) + * + * ## 使用示例 + * + * ```typescript + * const { hasAuth } = useAuth() + * + * // 检查是否有新增权限 + * if (hasAuth('add')) { + * // 显示新增按钮 + * } + * + * // 在模板中使用 + * 编辑 + * 删除 + * ``` + * + * @module useAuth + * @author Art Design Pro Team + */ + +import { useRoute } from 'vue-router' +import { storeToRefs } from 'pinia' +import { useUserStore } from '@/store/modules/user' +import { useAppMode } from '@/hooks/core/useAppMode' +import type { AppRouteRecord } from '@/types/router' + +type AuthItem = NonNullable[number] + +const userStore = useUserStore() + +export const useAuth = () => { + const route = useRoute() + const { isFrontendMode } = useAppMode() + const { info } = storeToRefs(userStore) + + // 前端按钮权限(例如:['add', 'edit']) + const frontendAuthList = info.value?.buttons ?? [] + + // 后端路由 meta 配置的权限列表(例如:[{ authMark: 'add' }]) + const backendAuthList: AuthItem[] = Array.isArray(route.meta.authList) + ? (route.meta.authList as AuthItem[]) + : [] + + /** + * 检查是否拥有某权限标识(前后端模式通用) + * @param auth 权限标识 + * @returns 是否有权限 + */ + const hasAuth = (auth: string): boolean => { + // 前端模式 + if (isFrontendMode.value) { + return frontendAuthList.includes(auth) + } + + // 后端模式 + return backendAuthList.some((item) => item?.authMark === auth) + } + + return { + hasAuth + } +} diff --git a/admin/src/hooks/core/useCeremony.ts b/admin/src/hooks/core/useCeremony.ts new file mode 100644 index 0000000..ead2630 --- /dev/null +++ b/admin/src/hooks/core/useCeremony.ts @@ -0,0 +1,184 @@ +/** + * useCeremony - 节日庆祝管理 + * + * 提供节日烟花效果和祝福文本展示功能,为系统增添节日氛围。 + * 自动检测当前日期是否为节日,并在首次进入时播放烟花动画和显示祝福语。 + * + * ## 主要功能 + * + * 1. 节日检测 - 自动匹配当前日期与节日配置列表,支持单日和跨日期节日 + * 2. 烟花动画 - 播放节日烟花特效,支持自定义图片和触发次数 + * 3. 祝福文本 - 烟花结束后显示节日祝福文本 + * 4. 状态管理 - 记录烟花播放状态,避免重复播放 + * 5. 清理机制 - 提供清理方法,支持手动停止和重置 + * + * ## 使用示例 + * + * ```typescript + * // 在配置文件中定义节日 + * // 单日节日 + * { + * date: '2024-12-25', + * name: '圣诞节', + * image: christmasImage, + * count: 3 // 可选,不设置则使用默认值 3 次 + * scrollText: 'Merry Christmas!', + * } + * + * // 跨日期节日 + * { + * date: '2025-11-07', + * endDate: '2025-11-10', + * name: 'v3.0 测试阶段', + * image: '', + * count: 5 // 自定义烟花播放次数 + * scrollText: '系统 v3.0 测试阶段正式开启!', + * } + * ``` + * + * @module useCeremony + * @author Art Design Pro Team + */ + +import { useTimeoutFn, useIntervalFn, useDateFormat } from '@vueuse/core' +import { storeToRefs } from 'pinia' +import { computed } from 'vue' +import { useSettingStore } from '@/store/modules/setting' +import { mittBus } from '@/utils/sys' +import { festivalConfigList } from '@/config/modules/festival' + +/** + * 节日庆祝配置常量 + */ +const FESTIVAL_CONFIG = { + /** 初始延迟(毫秒) */ + INITIAL_DELAY: 300, + /** 烟花播放间隔(毫秒) */ + FIREWORK_INTERVAL: 1000, + /** 文本显示延迟(毫秒) */ + TEXT_DELAY: 2000, + /** 默认烟花播放次数 */ + DEFAULT_FIREWORKS_COUNT: 3 +} as const + +/** + * 节日庆祝功能 + * 提供节日烟花效果和祝福文本展示 + */ +export function useCeremony() { + const settingStore = useSettingStore() + const { holidayFireworksLoaded, isShowFireworks } = storeToRefs(settingStore) + + let fireworksInterval: { pause: () => void } | null = null + + /** + * 检查日期是否在节日范围内 + * @param currentDate 当前日期 + * @param festivalDate 节日开始日期 + * @param festivalEndDate 节日结束日期(可选) + */ + const isDateInRange = ( + currentDate: string, + festivalDate: string, + festivalEndDate?: string + ): boolean => { + if (!festivalEndDate) { + // 单日节日 + return currentDate === festivalDate + } + + // 跨日期节日 + const current = new Date(currentDate) + const start = new Date(festivalDate) + const end = new Date(festivalEndDate) + + return current >= start && current <= end + } + + /** + * 获取当前日期对应的节日数据 + */ + const currentFestivalData = computed(() => { + const currentDate = useDateFormat(new Date(), 'YYYY-MM-DD').value + return festivalConfigList.find((item) => isDateInRange(currentDate, item.date, item.endDate)) + }) + + /** + * 更新节日日期到 store + */ + const updateFestivalDate = () => { + settingStore.setFestivalDate(currentFestivalData.value?.date || '') + } + + /** + * 触发烟花效果 + */ + const triggerFirework = () => { + mittBus.emit('triggerFireworks', currentFestivalData.value?.image) + } + + /** + * 完成烟花效果后显示文本 + */ + const showFestivalText = () => { + settingStore.setholidayFireworksLoaded(true) + + useTimeoutFn(() => { + settingStore.setShowFestivalText(true) + updateFestivalDate() + }, FESTIVAL_CONFIG.TEXT_DELAY) + } + + /** + * 启动烟花循环 + */ + const startFireworksLoop = () => { + let playedCount = 0 + // 使用节日配置的播放次数,如果没有则使用默认值 + const count = currentFestivalData.value?.count ?? FESTIVAL_CONFIG.DEFAULT_FIREWORKS_COUNT + + const { pause } = useIntervalFn(() => { + triggerFirework() + playedCount++ + + if (playedCount >= count) { + pause() + showFestivalText() + } + }, FESTIVAL_CONFIG.FIREWORK_INTERVAL) + + fireworksInterval = { pause } + } + + /** + * 开启节日庆祝 + */ + const openFestival = () => { + if (!currentFestivalData.value || !isShowFireworks.value) { + return + } + + const { start } = useTimeoutFn(startFireworksLoop, FESTIVAL_CONFIG.INITIAL_DELAY) + start() + } + + /** + * 清理烟花效果 + */ + const cleanup = () => { + if (fireworksInterval) { + fireworksInterval.pause() + fireworksInterval = null + } + settingStore.setShowFestivalText(false) + updateFestivalDate() + } + + return { + openFestival, + cleanup, + holidayFireworksLoaded, + currentFestivalData, + isShowFireworks + } +} diff --git a/admin/src/hooks/core/useChart.ts b/admin/src/hooks/core/useChart.ts new file mode 100644 index 0000000..29ba1d1 --- /dev/null +++ b/admin/src/hooks/core/useChart.ts @@ -0,0 +1,745 @@ +/** + * useChart - ECharts 图表管理 + * + * 提供完整的 ECharts 图表生命周期管理和配置能力,简化图表开发流程。 + * 自动处理图表初始化、更新、销毁、主题切换、响应式调整等复杂逻辑。 + * + * ## 核心功能 + * + * 1. 图表生命周期管理 - 自动处理初始化、更新、销毁,支持延迟加载和可见性检测 + * 2. 主题自动适配 - 响应系统主题变化,自动更新图表样式和配色 + * 3. 响应式调整 - 监听窗口大小、菜单展开等变化,自动调整图表尺寸 + * 4. 空状态处理 - 优雅的空数据展示,自动显示"暂无数据"提示 + * 5. 样式配置统一 - 提供坐标轴、图例、提示框等统一的样式配置方法 + * 6. 性能优化 - 防抖处理、样式缓存、requestAnimationFrame 优化 + * 7. 高级组件抽象 - useChartComponent 提供更高层次的图表组件封装 + * + * ## 使用示例 + * + * ```typescript + * // 基础用法 + * const { + * chartRef, + * initChart, + * updateChart, + * getAxisLineStyle, + * getTooltipStyle + * } = useChart() + * + * onMounted(() => { + * initChart({ + * xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] }, + * yAxis: { type: 'value' }, + * series: [{ data: [120, 200, 150], type: 'bar' }] + * }) + * }) + * + * // 高级用法 - 组件抽象 + * const chart = useChartComponent({ + * props, + * generateOptions: () => ({ + * // ECharts 配置 + * }), + * checkEmpty: () => data.value.length === 0, + * watchSources: [() => props.data] + * }) + * ``` + * + * @module useChart + * @author Art Design Pro Team + */ + +import { echarts, type EChartsOption } from '@/plugins/echarts' +import { storeToRefs } from 'pinia' +import { useSettingStore } from '@/store/modules/setting' +import { getCssVar } from '@/utils/ui' +import type { BaseChartProps, ChartThemeConfig, UseChartOptions } from '@/types/component/chart' + +// 图表主题配置 +export const useChartOps = (): ChartThemeConfig => ({ + /** */ + chartHeight: '16rem', + /** 字体大小 */ + fontSize: 13, + /** 字体颜色 */ + fontColor: '#999', + /** 主题颜色 */ + themeColor: getCssVar('--el-color-primary-light-1'), + /** 颜色组 */ + colors: [ + getCssVar('--el-color-primary-light-1'), + '#4ABEFF', + '#EDF2FF', + '#14DEBA', + '#FFAF20', + '#FA8A6C', + '#FFAF20' + ] +}) + +// 常量定义 +const RESIZE_DELAYS = [50, 100, 200, 350] as const +const MENU_RESIZE_DELAYS = [50, 100, 200] as const +const RESIZE_DEBOUNCE_DELAY = 100 + +export function useChart(options: UseChartOptions = {}) { + const { initOptions, initDelay = 0, threshold = 0.1, autoTheme = true } = options + + const settingStore = useSettingStore() + const { isDark, menuOpen, menuType } = storeToRefs(settingStore) + + const chartRef = ref() + let chart: echarts.ECharts | null = null + let intersectionObserver: IntersectionObserver | null = null + let pendingOptions: EChartsOption | null = null + let resizeTimeoutId: number | null = null + let resizeFrameId: number | null = null + let isDestroyed = false + let emptyStateDiv: HTMLElement | null = null + + // 清理定时器的统一方法 + const clearTimers = () => { + if (resizeTimeoutId) { + clearTimeout(resizeTimeoutId) + resizeTimeoutId = null + } + if (resizeFrameId) { + cancelAnimationFrame(resizeFrameId) + resizeFrameId = null + } + } + + // 使用 requestAnimationFrame 优化 resize 处理 + const requestAnimationResize = () => { + if (resizeFrameId) { + cancelAnimationFrame(resizeFrameId) + } + resizeFrameId = requestAnimationFrame(() => { + handleResize() + resizeFrameId = null + }) + } + + // 防抖的resize处理(用于窗口resize事件) + const debouncedResize = () => { + if (resizeTimeoutId) { + clearTimeout(resizeTimeoutId) + } + resizeTimeoutId = window.setTimeout(() => { + requestAnimationResize() + resizeTimeoutId = null + }, RESIZE_DEBOUNCE_DELAY) + } + + // 多延迟resize处理 - 统一方法 + const multiDelayResize = (delays: readonly number[]) => { + // 立即调用一次,快速响应 + nextTick(requestAnimationResize) + + // 使用延迟时间,确保图表正确适应变化 + delays.forEach((delay) => { + setTimeout(requestAnimationResize, delay) + }) + } + + // 收缩菜单时,重新计算图表大小(仅在图表存在时监听) + let menuOpenStopHandle: (() => void) | null = null + let menuTypeStopHandle: (() => void) | null = null + + const setupMenuWatchers = () => { + menuOpenStopHandle = watch(menuOpen, () => multiDelayResize(RESIZE_DELAYS)) + menuTypeStopHandle = watch(menuType, () => { + nextTick(requestAnimationResize) + setTimeout(() => multiDelayResize(MENU_RESIZE_DELAYS), 0) + }) + } + + const cleanupMenuWatchers = () => { + menuOpenStopHandle?.() + menuTypeStopHandle?.() + menuOpenStopHandle = null + menuTypeStopHandle = null + } + + // 主题变化时重新设置图表选项 + let themeStopHandle: (() => void) | null = null + + const setupThemeWatcher = () => { + if (autoTheme) { + themeStopHandle = watch(isDark, () => { + // 更新空状态样式 + emptyStateManager.updateStyle() + + if (chart && !isDestroyed) { + // 使用 requestAnimationFrame 优化主题更新 + requestAnimationFrame(() => { + if (chart && !isDestroyed) { + const currentOptions = chart.getOption() + if (currentOptions) { + updateChart(currentOptions as EChartsOption) + } + } + }) + } + }) + } + } + + const cleanupThemeWatcher = () => { + themeStopHandle?.() + themeStopHandle = null + } + + // 样式生成器 - 统一的样式配置 + const createLineStyle = (color: string, width = 1, type?: 'solid' | 'dashed') => ({ + color, + width, + ...(type && { type }) + }) + + // 缓存样式配置以减少重复计算 + const styleCache = { + axisLine: null as any, + splitLine: null as any, + axisLabel: null as any, + lastDarkValue: isDark.value + } + + const clearStyleCache = () => { + styleCache.axisLine = null + styleCache.splitLine = null + styleCache.axisLabel = null + styleCache.lastDarkValue = isDark.value + } + + // 坐标轴线样式 + const getAxisLineStyle = (show: boolean = true) => { + if (styleCache.lastDarkValue !== isDark.value) { + clearStyleCache() + } + if (!styleCache.axisLine) { + styleCache.axisLine = { + show, + lineStyle: createLineStyle(isDark.value ? '#444' : '#EDEDED') + } + } + return styleCache.axisLine + } + + // 分割线样式 + const getSplitLineStyle = (show: boolean = true) => { + if (styleCache.lastDarkValue !== isDark.value) { + clearStyleCache() + } + if (!styleCache.splitLine) { + styleCache.splitLine = { + show, + lineStyle: createLineStyle(isDark.value ? '#444' : '#EDEDED', 1, 'dashed') + } + } + return styleCache.splitLine + } + + // 坐标轴标签样式 + const getAxisLabelStyle = (show: boolean = true) => { + if (styleCache.lastDarkValue !== isDark.value) { + clearStyleCache() + } + if (!styleCache.axisLabel) { + const { fontColor, fontSize } = useChartOps() + styleCache.axisLabel = { + show, + color: fontColor, + fontSize + } + } + return styleCache.axisLabel + } + + // 坐标轴刻度样式(静态配置,无需缓存) + const getAxisTickStyle = () => ({ + show: false + }) + + // 获取动画配置 + const getAnimationConfig = (animationDelay: number = 50, animationDuration: number = 1500) => ({ + animationDelay: (idx: number) => idx * animationDelay + 200, + animationDuration: (idx: number) => animationDuration - idx * 50, + animationEasing: 'quarticOut' as const + }) + + // 获取统一的 tooltip 配置 + const getTooltipStyle = (trigger: 'item' | 'axis' = 'axis', customOptions: any = {}) => ({ + trigger, + backgroundColor: isDark.value ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.9)', + borderColor: isDark.value ? '#333' : '#ddd', + borderWidth: 1, + textStyle: { + color: isDark.value ? '#fff' : '#333' + }, + ...customOptions + }) + + // 获取统一的图例配置 + const getLegendStyle = ( + position: 'bottom' | 'top' | 'left' | 'right' = 'bottom', + customOptions: any = {} + ) => { + const baseConfig = { + textStyle: { + color: isDark.value ? '#fff' : '#333' + }, + itemWidth: 12, + itemHeight: 12, + itemGap: 20, + ...customOptions + } + + // 根据位置设置不同的配置 + switch (position) { + case 'bottom': + return { + ...baseConfig, + bottom: 0, + left: 'center', + orient: 'horizontal', + icon: 'roundRect' + } + case 'top': + return { + ...baseConfig, + top: 0, + left: 'center', + orient: 'horizontal', + icon: 'roundRect' + } + case 'left': + return { + ...baseConfig, + left: 0, + top: 'center', + orient: 'vertical', + icon: 'roundRect' + } + case 'right': + return { + ...baseConfig, + right: 0, + top: 'center', + orient: 'vertical', + icon: 'roundRect' + } + default: + return baseConfig + } + } + + // 根据图例位置计算 grid 配置 + const getGridWithLegend = ( + showLegend: boolean, + legendPosition: 'bottom' | 'top' | 'left' | 'right' = 'bottom', + baseGrid: any = {} + ) => { + const defaultGrid = { + top: 15, + right: 15, + bottom: 8, + left: 0, + containLabel: true, + ...baseGrid + } + + if (!showLegend) { + return defaultGrid + } + + // 根据图例位置调整 grid + switch (legendPosition) { + case 'bottom': + return { + ...defaultGrid, + bottom: 40 + } + case 'top': + return { + ...defaultGrid, + top: 40 + } + case 'left': + return { + ...defaultGrid, + left: 120 + } + case 'right': + return { + ...defaultGrid, + right: 120 + } + default: + return defaultGrid + } + } + + // 创建IntersectionObserver + const createIntersectionObserver = () => { + if (intersectionObserver || !chartRef.value) return + + intersectionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting && pendingOptions && !isDestroyed) { + // 使用 requestAnimationFrame 确保在下一帧初始化图表 + requestAnimationFrame(() => { + if (!isDestroyed && pendingOptions) { + try { + // 元素变为可见,初始化图表 + if (!chart) { + chart = echarts.init(entry.target as HTMLElement) + } + + // 触发自定义事件,让组件处理动画逻辑 + const event = new CustomEvent('chartVisible', { + detail: { options: pendingOptions } + }) + entry.target.dispatchEvent(event) + + pendingOptions = null + cleanupIntersectionObserver() + } catch (error) { + console.error('图表初始化失败:', error) + } + } + }) + } + }) + }, + { threshold } + ) + + intersectionObserver.observe(chartRef.value) + } + + // 清理IntersectionObserver + const cleanupIntersectionObserver = () => { + if (intersectionObserver) { + intersectionObserver.disconnect() + intersectionObserver = null + } + } + + // 检查容器是否可见 + const isContainerVisible = (element: HTMLElement): boolean => { + const rect = element.getBoundingClientRect() + return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && rect.bottom > 0 + } + + // 图表初始化核心逻辑 + const performChartInit = (options: EChartsOption) => { + if (!chart && chartRef.value && !isDestroyed) { + chart = echarts.init(chartRef.value) + // 图表创建后立即设置监听器 + setupMenuWatchers() + setupThemeWatcher() + } + if (chart && !isDestroyed) { + chart.setOption(options) + pendingOptions = null + } + } + + // 空状态管理器 + const emptyStateManager = { + create: () => { + if (!chartRef.value || emptyStateDiv) return + + emptyStateDiv = document.createElement('div') + emptyStateDiv.style.cssText = ` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 12px; + color: ${isDark.value ? '#555555' : '#B3B2B2'}; + background: transparent; + z-index: 10; + ` + emptyStateDiv.innerHTML = `暂无数据` + + // 确保父容器有相对定位 + if ( + chartRef.value.style.position !== 'relative' && + chartRef.value.style.position !== 'absolute' + ) { + chartRef.value.style.position = 'relative' + } + + chartRef.value.appendChild(emptyStateDiv) + }, + + remove: () => { + if (emptyStateDiv && chartRef.value) { + chartRef.value.removeChild(emptyStateDiv) + emptyStateDiv = null + } + }, + + updateStyle: () => { + if (emptyStateDiv) { + emptyStateDiv.style.color = isDark.value ? '#666' : '#999' + } + } + } + + // 初始化图表 + const initChart = (options: EChartsOption = {}, isEmpty: boolean = false) => { + if (!chartRef.value || isDestroyed) return + + const mergedOptions = { ...initOptions, ...options } + + try { + if (isEmpty) { + // 处理空数据情况 - 显示自定义空状态div + if (chart) { + chart.clear() + } + emptyStateManager.create() + return + } else { + // 有数据时移除空状态div + emptyStateManager.remove() + } + + if (isContainerVisible(chartRef.value)) { + // 容器可见,正常初始化 + if (initDelay > 0) { + setTimeout(() => performChartInit(mergedOptions), initDelay) + } else { + performChartInit(mergedOptions) + } + } else { + // 容器不可见,保存选项并设置监听器 + pendingOptions = mergedOptions + createIntersectionObserver() + } + } catch (error) { + console.error('图表初始化失败:', error) + } + } + + // 更新图表 + const updateChart = (options: EChartsOption) => { + if (isDestroyed) return + + try { + if (!chart) { + // 如果图表不存在,先初始化 + initChart(options) + return + } + chart.setOption(options) + } catch (error) { + console.error('图表更新失败:', error) + } + } + + // 处理窗口大小变化 + const handleResize = () => { + if (chart && !isDestroyed) { + try { + chart.resize() + } catch (error) { + console.error('图表resize失败:', error) + } + } + } + + // 销毁图表 + const destroyChart = () => { + isDestroyed = true + + if (chart) { + try { + chart.dispose() + } catch (error) { + console.error('图表销毁失败:', error) + } finally { + chart = null + } + } + + // 清理所有监听器和资源 + cleanupMenuWatchers() + cleanupThemeWatcher() + emptyStateManager.remove() + cleanupIntersectionObserver() + clearTimers() + clearStyleCache() + pendingOptions = null + } + + // 获取图表实例 + const getChartInstance = () => chart + + // 获取图表是否已初始化 + const isChartInitialized = () => chart !== null + + onMounted(() => { + window.addEventListener('resize', debouncedResize) + }) + + onBeforeUnmount(() => { + window.removeEventListener('resize', debouncedResize) + }) + + onUnmounted(() => { + destroyChart() + }) + + return { + isDark, + chartRef, + initChart, + updateChart, + handleResize, + destroyChart, + getChartInstance, + isChartInitialized, + emptyStateManager, + getAxisLineStyle, + getSplitLineStyle, + getAxisLabelStyle, + getAxisTickStyle, + getAnimationConfig, + getTooltipStyle, + getLegendStyle, + useChartOps, + getGridWithLegend + } +} + +// 高级图表组件抽象 +interface UseChartComponentOptions { + /** Props响应式对象 */ + props: T + /** 图表配置生成函数 */ + generateOptions: () => EChartsOption + /** 空数据检查函数 */ + checkEmpty?: () => boolean + /** 自定义监听的响应式数据 */ + watchSources?: (() => any)[] + /** 自定义可视事件处理 */ + onVisible?: () => void + /** useChart选项 */ + chartOptions?: UseChartOptions +} + +export function useChartComponent(options: UseChartComponentOptions) { + const { + props, + generateOptions, + checkEmpty, + watchSources = [], + onVisible, + chartOptions = {} + } = options + + const chart = useChart(chartOptions) + const { chartRef, initChart, isDark, emptyStateManager } = chart + + // 检查是否为空数据 + const isEmpty = computed(() => { + if (props.isEmpty) return true + if (checkEmpty) return checkEmpty() + return false + }) + + // 更新图表 + const updateChart = () => { + nextTick(() => { + if (isEmpty.value) { + // 处理空数据情况 - 显示自定义空状态div + if (chart.getChartInstance()) { + chart.getChartInstance()?.clear() + } + emptyStateManager.create() + } else { + // 有数据时移除空状态div并初始化图表 + emptyStateManager.remove() + initChart(generateOptions()) + } + }) + } + + // 处理图表进入可视区域时的逻辑 + const handleChartVisible = () => { + if (onVisible) { + onVisible() + } else { + updateChart() + } + } + + // 存储监听器停止函数 + const stopHandles: (() => void)[] = [] + + // 设置数据监听 + const setupWatchers = () => { + // 监听自定义数据源 + if (watchSources.length > 0) { + const stopHandle = watch(watchSources, updateChart, { deep: true }) + stopHandles.push(stopHandle) + } + + // 监听主题变化 + const themeStopHandle = watch(isDark, () => { + emptyStateManager.updateStyle() + updateChart() + }) + stopHandles.push(themeStopHandle) + } + + // 清理所有监听器 + const cleanupWatchers = () => { + stopHandles.forEach((stop) => stop()) + stopHandles.length = 0 + } + + // 设置生命周期 + const setupLifecycle = () => { + onMounted(() => { + updateChart() + + // 监听图表可见事件 + if (chartRef.value) { + chartRef.value.addEventListener('chartVisible', handleChartVisible) + } + }) + + onBeforeUnmount(() => { + // 清理事件监听器 + if (chartRef.value) { + chartRef.value.removeEventListener('chartVisible', handleChartVisible) + } + // 清理所有监听器 + cleanupWatchers() + // 清理空状态div + emptyStateManager.remove() + }) + } + + // 初始化 + setupWatchers() + setupLifecycle() + + return { + ...chart, + isEmpty, + updateChart, + handleChartVisible + } +} diff --git a/admin/src/hooks/core/useCommon.ts b/admin/src/hooks/core/useCommon.ts new file mode 100644 index 0000000..c936854 --- /dev/null +++ b/admin/src/hooks/core/useCommon.ts @@ -0,0 +1,87 @@ +/** + * useCommon - 通用功能集合 + * + * 提供常用的页面操作功能,包括页面刷新、滚动控制、路径获取等。 + * 这些功能在多个页面和组件中都会用到,统一封装便于复用。 + * + * ## 主要功能 + * + * 1. 首页路径 - 获取系统配置的首页路径 + * 2. 页面刷新 - 刷新当前页面内容 + * 3. 滚动控制 - 提供多种滚动到顶部和指定位置的方法 + * 4. 平滑滚动 - 支持平滑滚动动画效果 + * + * @module useCommon + * @author Art Design Pro Team + */ + +import { computed } from 'vue' +import { useMenuStore } from '@/store/modules/menu' +import { useSettingStore } from '@/store/modules/setting' + +export function useCommon() { + const menuStore = useMenuStore() + const settingStore = useSettingStore() + + /** + * 首页路径 + * 从菜单 store 中获取配置的首页路径 + */ + const homePath = computed(() => menuStore.getHomePath()) + + /** + * 刷新当前页面 + * 通过切换 setting store 中的 refresh 状态触发页面重新渲染 + */ + const refresh = () => { + settingStore.reload() + } + + /** + * 滚动到页面顶部 + * 查找主内容区域并将其滚动位置重置为顶部 + */ + const scrollToTop = () => { + const scrollContainer = document.getElementById('app-main') + if (scrollContainer) { + scrollContainer.scrollTop = 0 + } + } + + /** + * 平滑滚动到页面顶部 + * 使用 smooth 行为实现平滑滚动效果 + */ + const smoothScrollToTop = () => { + const scrollContainer = document.getElementById('app-main') + if (scrollContainer) { + scrollContainer.scrollTo({ + top: 0, + behavior: 'smooth' + }) + } + } + + /** + * 滚动到指定位置 + * @param top 目标滚动位置(像素) + * @param smooth 是否使用平滑滚动 + */ + const scrollTo = (top: number, smooth: boolean = false) => { + const scrollContainer = document.getElementById('app-main') + if (scrollContainer) { + scrollContainer.scrollTo({ + top, + behavior: smooth ? 'smooth' : 'auto' + }) + } + } + + return { + homePath, + refresh, + scrollTo, + scrollToTop, + smoothScrollToTop + } +} diff --git a/admin/src/hooks/core/useFastEnter.ts b/admin/src/hooks/core/useFastEnter.ts new file mode 100644 index 0000000..555eb65 --- /dev/null +++ b/admin/src/hooks/core/useFastEnter.ts @@ -0,0 +1,55 @@ +/** + * useFastEnter - 快速入口管理 + * + * 管理顶部栏的快速入口功能,提供应用列表和快速链接的配置和过滤。 + * 支持动态启用/禁用、自定义排序、响应式宽度控制等功能。 + * + * ## 主要功能 + * + * 1. 应用列表管理 - 获取启用的应用列表,自动按排序权重排序 + * 2. 快速链接管理 - 获取启用的快速链接,支持自定义排序 + * 3. 响应式配置 - 所有配置自动响应变化,无需手动更新 + * 4. 宽度控制 - 提供最小显示宽度配置,支持响应式布局 + * + * @module useFastEnter + * @author Art Design Pro Team + */ + +import { computed } from 'vue' +import appConfig from '@/config' +import type { FastEnterApplication, FastEnterQuickLink } from '@/types/config' + +export function useFastEnter() { + // 获取快速入口配置 + const fastEnterConfig = computed(() => appConfig.fastEnter) + + // 获取启用的应用列表(按排序权重排序) + const enabledApplications = computed(() => { + if (!fastEnterConfig.value?.applications) return [] + + return fastEnterConfig.value.applications + .filter((app) => app.enabled !== false) + .sort((a, b) => (a.order || 0) - (b.order || 0)) + }) + + // 获取启用的快速链接(按排序权重排序) + const enabledQuickLinks = computed(() => { + if (!fastEnterConfig.value?.quickLinks) return [] + + return fastEnterConfig.value.quickLinks + .filter((link) => link.enabled !== false) + .sort((a, b) => (a.order || 0) - (b.order || 0)) + }) + + // 获取最小显示宽度 + const minWidth = computed(() => { + return fastEnterConfig.value?.minWidth || 1200 + }) + + return { + fastEnterConfig, + enabledApplications, + enabledQuickLinks, + minWidth + } +} diff --git a/admin/src/hooks/core/useHeaderBar.ts b/admin/src/hooks/core/useHeaderBar.ts new file mode 100644 index 0000000..be10712 --- /dev/null +++ b/admin/src/hooks/core/useHeaderBar.ts @@ -0,0 +1,201 @@ +/** + * useHeaderBar - 顶部栏功能管理 + * + * 统一管理顶部栏各个功能模块的显示状态和配置信息。 + * 提供灵活的功能开关控制,支持动态显示/隐藏顶部栏的各个功能按钮。 + * + * ## 主要功能 + * + * 1. 功能开关控制 - 统一管理菜单按钮、刷新按钮、快速入口等功能的显示状态 + * 2. 配置信息获取 - 获取各个功能模块的详细配置信息 + * 3. 功能列表查询 - 快速获取所有启用或禁用的功能列表 + * 4. 响应式状态 - 所有状态自动响应配置和 store 变化 + * + * @module useHeaderBar + * @author Art Design Pro Team + */ + +import { computed } from 'vue' +import { storeToRefs } from 'pinia' +import { useSettingStore } from '@/store/modules/setting' +import { headerBarConfig } from '@/config/modules/headerBar' +import { HeaderBarFeatureConfig } from '@/types' + +/** + * 顶部栏功能管理 + * @returns 顶部栏功能相关的状态和方法 + */ +export function useHeaderBar() { + const settingStore = useSettingStore() + + // 获取顶部栏配置 + const headerBarConfigRef = computed(() => headerBarConfig) + + // 从store中获取相关状态 + const { showMenuButton, showFastEnter, showRefreshButton, showCrumbs, showLanguage } = + storeToRefs(settingStore) + + /** + * 检查特定功能是否启用 + * @param feature 功能名称 + * @returns 是否启用 + */ + const isFeatureEnabled = (feature: keyof HeaderBarFeatureConfig): boolean => { + return headerBarConfigRef.value[feature]?.enabled ?? false + } + + /** + * 获取功能配置信息 + * @param feature 功能名称 + * @returns 功能配置信息 + */ + const getFeatureConfig = (feature: keyof HeaderBarFeatureConfig) => { + return headerBarConfigRef.value[feature] + } + + // 检查菜单按钮是否显示 + const shouldShowMenuButton = computed(() => { + return isFeatureEnabled('menuButton') && showMenuButton.value + }) + + // 检查刷新按钮是否显示 + const shouldShowRefreshButton = computed(() => { + return isFeatureEnabled('refreshButton') && showRefreshButton.value + }) + + // 检查快速入口是否显示 + const shouldShowFastEnter = computed(() => { + return isFeatureEnabled('fastEnter') && showFastEnter.value + }) + + // 检查面包屑是否显示 + const shouldShowBreadcrumb = computed(() => { + return isFeatureEnabled('breadcrumb') && showCrumbs.value + }) + + // 检查全局搜索是否显示 + const shouldShowGlobalSearch = computed(() => { + return isFeatureEnabled('globalSearch') + }) + + // 检查全屏按钮是否显示 + const shouldShowFullscreen = computed(() => { + return isFeatureEnabled('fullscreen') + }) + + // 检查通知中心是否显示 + const shouldShowNotification = computed(() => { + return isFeatureEnabled('notification') + }) + + // 检查聊天功能是否显示 + const shouldShowChat = computed(() => { + return isFeatureEnabled('chat') + }) + + // 检查语言切换是否显示 + const shouldShowLanguage = computed(() => { + return isFeatureEnabled('language') && showLanguage.value + }) + + // 检查设置面板是否显示 + const shouldShowSettings = computed(() => { + return isFeatureEnabled('settings') + }) + + // 检查主题切换是否显示 + const shouldShowThemeToggle = computed(() => { + return isFeatureEnabled('themeToggle') + }) + + // 获取快速入口的最小宽度 + const fastEnterMinWidth = computed(() => { + const config = getFeatureConfig('fastEnter') + return (config as any)?.minWidth || 1200 + }) + + /** + * 检查功能是否启用(别名) + * @param feature 功能名称 + * @returns 是否启用 + */ + const isFeatureActive = (feature: keyof HeaderBarFeatureConfig): boolean => { + return isFeatureEnabled(feature) + } + + /** + * 获取功能配置(别名) + * @param feature 功能名称 + * @returns 功能配置 + */ + const getFeatureInfo = (feature: keyof HeaderBarFeatureConfig) => { + return getFeatureConfig(feature) + } + + /** + * 获取所有启用的功能列表 + * @returns 启用的功能名称数组 + */ + const getEnabledFeatures = (): (keyof HeaderBarFeatureConfig)[] => { + return Object.keys(headerBarConfigRef.value).filter( + (key) => headerBarConfigRef.value[key as keyof HeaderBarFeatureConfig]?.enabled + ) as (keyof HeaderBarFeatureConfig)[] + } + + /** + * 获取所有禁用的功能列表 + * @returns 禁用的功能名称数组 + */ + const getDisabledFeatures = (): (keyof HeaderBarFeatureConfig)[] => { + return Object.keys(headerBarConfigRef.value).filter( + (key) => !headerBarConfigRef.value[key as keyof HeaderBarFeatureConfig]?.enabled + ) as (keyof HeaderBarFeatureConfig)[] + } + + /** + * 获取所有启用的功能(别名) + * @returns 启用的功能列表 + */ + const getActiveFeatures = () => { + return getEnabledFeatures() + } + + /** + * 获取所有禁用的功能(别名) + * @returns 禁用的功能列表 + */ + const getInactiveFeatures = () => { + return getDisabledFeatures() + } + + return { + // 配置 + headerBarConfig: headerBarConfigRef, + + // 显示状态计算属性 + shouldShowMenuButton, // 是否显示菜单按钮 + shouldShowRefreshButton, // 是否显示刷新按钮 + shouldShowFastEnter, // 是否显示快速入口 + shouldShowBreadcrumb, // 是否显示面包屑 + shouldShowGlobalSearch, // 是否显示全局搜索 + shouldShowFullscreen, // 是否显示全屏按钮 + shouldShowNotification, // 是否显示通知中心 + shouldShowChat, // 是否显示聊天功能 + shouldShowLanguage, // 是否显示语言切换 + shouldShowSettings, // 是否显示设置面板 + shouldShowThemeToggle, // 是否显示主题切换 + + // 配置相关 + fastEnterMinWidth, // 快速入口最小宽度 + + // 方法 + isFeatureEnabled, // 检查功能是否启用 + isFeatureActive, // 检查功能是否启用(别名) + getFeatureConfig, // 获取功能配置 + getFeatureInfo, // 获取功能配置(别名) + getEnabledFeatures, // 获取所有启用的功能 + getDisabledFeatures, // 获取所有禁用的功能 + getActiveFeatures, // 获取所有启用的功能(别名) + getInactiveFeatures // 获取所有禁用的功能(别名) + } +} diff --git a/admin/src/hooks/core/useLayoutHeight.ts b/admin/src/hooks/core/useLayoutHeight.ts new file mode 100644 index 0000000..4b1171a --- /dev/null +++ b/admin/src/hooks/core/useLayoutHeight.ts @@ -0,0 +1,148 @@ +/** + * useLayoutHeight - 页面布局高度管理 + * + * 自动计算和管理页面内容区域的高度,确保内容区域能够正确填充剩余空间。 + * 监听头部元素高度变化,动态调整内容区域高度,避免出现滚动条或布局错乱。 + * + * ## 主要功能 + * + * 1. 动态高度计算 - 根据头部元素高度自动计算内容区域高度 + * 2. 响应式监听 - 自动监听元素尺寸变化并更新高度 + * 3. CSS 变量同步 - 自动更新 CSS 变量,方便全局使用 + * 4. 灵活配置 - 支持自定义间距、CSS 变量名等 + * 5. 自动查找模式 - 提供通过 ID 自动查找元素的便捷方式 + * + * @module useLayoutHeight + * @author Art Design Pro Team + */ + +import { ref, computed, watch, onMounted } from 'vue' +import { useElementSize } from '@vueuse/core' + +/** + * 页面容器高度配置 + */ +interface LayoutHeightOptions { + /** 额外的间距(默认 15px) */ + extraSpacing?: number + /** 是否自动更新 CSS 变量(默认 true) */ + updateCssVar?: boolean + /** CSS 变量名称(默认 '--art-full-height') */ + cssVarName?: string +} + +export function useLayoutHeight(options: LayoutHeightOptions = {}) { + const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options + + // 元素引用 + const headerRef = ref() + const contentHeaderRef = ref() + + // 使用 VueUse 自动监听元素尺寸变化 + const { height: headerHeight } = useElementSize(headerRef) + const { height: contentHeaderHeight } = useElementSize(contentHeaderRef) + + // 计算容器最小高度(响应式) + const containerMinHeight = computed(() => { + const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing + return `calc(100vh - ${totalHeight}px)` + }) + + if (updateCssVar) { + watch( + containerMinHeight, + (newHeight) => { + requestAnimationFrame(() => { + document.documentElement.style.setProperty(cssVarName, newHeight) + }) + }, + { immediate: true } + ) + } + + return { + /** 容器最小高度(响应式) */ + containerMinHeight, + /** 头部元素引用 */ + headerRef, + /** 内容头部元素引用 */ + contentHeaderRef, + /** 头部高度(响应式) */ + headerHeight, + /** 内容头部高度(响应式) */ + contentHeaderHeight + } +} + +/** + * 通过 ID 自动查找元素的布局高度管理 + * 适用于无法直接获取元素引用的场景 + * + * @param headerIds 头部元素的 ID 数组 + * @param options 配置选项 + * + * ``` + */ +export function useAutoLayoutHeight( + headerIds: string[] = ['app-header', 'app-content-header'], + options: LayoutHeightOptions = {} +) { + const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options + + // 创建元素引用 + const headerRef = ref() + const contentHeaderRef = ref() + + // 使用 VueUse 自动监听元素尺寸变化 + const { height: headerHeight } = useElementSize(headerRef) + const { height: contentHeaderHeight } = useElementSize(contentHeaderRef) + + // 计算容器最小高度(响应式) + const containerMinHeight = computed(() => { + const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing + return `calc(100vh - ${totalHeight}px)` + }) + + if (updateCssVar) { + watch( + containerMinHeight, + (newHeight) => { + requestAnimationFrame(() => { + document.documentElement.style.setProperty(cssVarName, newHeight) + }) + }, + { immediate: true } + ) + } + + // 在 DOM 挂载后查找元素 + onMounted(() => { + if (typeof document !== 'undefined') { + // 使用 nextTick 确保 DOM 完全渲染 + requestAnimationFrame(() => { + const header = document.getElementById(headerIds[0]) + const contentHeader = document.getElementById(headerIds[1]) + + if (header) { + headerRef.value = header + } + if (contentHeader) { + contentHeaderRef.value = contentHeader + } + }) + } + }) + + return { + /** 容器最小高度(响应式) */ + containerMinHeight, + /** 头部元素引用 */ + headerRef, + /** 内容头部元素引用 */ + contentHeaderRef, + /** 头部高度(响应式) */ + headerHeight, + /** 内容头部高度(响应式) */ + contentHeaderHeight + } +} diff --git a/admin/src/hooks/core/useTable.ts b/admin/src/hooks/core/useTable.ts new file mode 100644 index 0000000..d9def4f --- /dev/null +++ b/admin/src/hooks/core/useTable.ts @@ -0,0 +1,762 @@ +/** + * useTable - 企业级表格数据管理方案 + * + * 功能完整的表格数据管理解决方案,专为后台管理系统设计。 + * 封装了表格开发中的所有常见需求,让你专注于业务逻辑。 + * + * ## 主要功能 + * + * 1. 数据管理 - 自动处理 API 请求、响应转换、加载状态和错误处理 + * 2. 分页控制 - 自动同步分页状态、移动端适配、智能页码边界处理 + * 3. 搜索功能 - 防抖搜索优化、参数管理、一键重置、参数过滤 + * 4. 缓存系统 - 智能请求缓存、多种清理策略、自动过期管理、统计信息 + * 5. 刷新策略 - 提供 5 种刷新方法适配不同业务场景(新增/更新/删除/手动/定时) + * 6. 列配置管理 - 动态显示/隐藏列、列排序、配置持久化、批量操作(可选) + * + * @module useTable + * @author Art Design Pro Team + */ + +import { ref, reactive, computed, onMounted, onUnmounted, nextTick, readonly } from 'vue' +import { useWindowSize } from '@vueuse/core' +import { useTableColumns } from './useTableColumns' +import type { ColumnOption } from '@/types/component' +import { + TableCache, + CacheInvalidationStrategy, + type ApiResponse +} from '../../utils/table/tableCache' +import { + type TableError, + defaultResponseAdapter, + extractTableData, + updatePaginationFromResponse, + createSmartDebounce, + createErrorHandler +} from '../../utils/table/tableUtils' +import { tableConfig } from '../../utils/table/tableConfig' + +// 类型推导工具类型 +type InferApiParams = T extends (params: infer P) => any ? P : never +type InferApiResponse = T extends (params: any) => Promise ? R : never +type InferRecordType = T extends Api.Common.PaginatedResponse ? U : never + +// 优化的配置接口 - 支持自动类型推导 +export interface UseTableConfig< + TApiFn extends (params: any) => Promise = (params: any) => Promise, + TRecord = InferRecordType>, + TParams = InferApiParams, + TResponse = InferApiResponse +> { + // 核心配置 + core: { + /** API 请求函数 */ + apiFn: TApiFn + /** 默认请求参数 */ + apiParams?: Partial + /** 排除 apiParams 中的属性 */ + excludeParams?: string[] + /** 是否立即加载数据 */ + immediate?: boolean + /** 列配置工厂函数 */ + columnsFactory?: () => ColumnOption[] + /** 自定义分页字段映射 */ + paginationKey?: { + /** 当前页码字段名,默认为 'current' */ + current?: string + /** 每页条数字段名,默认为 'size' */ + size?: string + } + } + + // 数据处理 + transform?: { + /** 数据转换函数 */ + dataTransformer?: (data: TRecord[]) => TRecord[] + /** 响应数据适配器 */ + responseAdapter?: (response: TResponse) => ApiResponse + } + + // 性能优化 + performance?: { + /** 是否启用缓存 */ + enableCache?: boolean + /** 缓存时间(毫秒) */ + cacheTime?: number + /** 防抖延迟时间(毫秒) */ + debounceTime?: number + /** 最大缓存条数限制 */ + maxCacheSize?: number + } + + // 生命周期钩子 + hooks?: { + /** 数据加载成功回调(仅网络请求成功时触发) */ + onSuccess?: (data: TRecord[], response: ApiResponse) => void + /** 错误处理回调 */ + onError?: (error: TableError) => void + /** 缓存命中回调(从缓存获取数据时触发) */ + onCacheHit?: (data: TRecord[], response: ApiResponse) => void + /** 加载状态变化回调 */ + onLoading?: (loading: boolean) => void + /** 重置表单回调函数 */ + resetFormCallback?: () => void + } + + // 调试配置 + debug?: { + /** 是否启用日志输出 */ + enableLog?: boolean + /** 日志级别 */ + logLevel?: 'info' | 'warn' | 'error' + } +} + +export function useTable Promise>( + config: UseTableConfig +) { + return useTableImpl(config) +} + +/** + * useTable 的核心实现 - 强大的表格数据管理 Hook + * + * 提供完整的表格解决方案,包括: + * - 数据获取与缓存 + * - 分页控制 + * - 搜索功能 + * - 智能刷新策略 + * - 错误处理 + * - 列配置管理 + */ +function useTableImpl Promise>( + config: UseTableConfig +) { + type TRecord = InferRecordType> + type TParams = InferApiParams + const { + core: { + apiFn, + apiParams = {} as Partial, + excludeParams = [], + immediate = true, + columnsFactory, + paginationKey + }, + transform: { dataTransformer, responseAdapter = defaultResponseAdapter } = {}, + performance: { + enableCache = false, + cacheTime = 5 * 60 * 1000, + debounceTime = 300, + maxCacheSize = 50 + } = {}, + hooks: { onSuccess, onError, onCacheHit, resetFormCallback } = {}, + debug: { enableLog = false } = {} + } = config + + // 分页字段名配置:优先使用传入的配置,否则使用全局配置 + const pageKey = paginationKey?.current || tableConfig.paginationKey.current + const sizeKey = paginationKey?.size || tableConfig.paginationKey.size + + // 响应式触发器,用于手动更新缓存统计信息 + const cacheUpdateTrigger = ref(0) + + // 日志工具函数 + const logger = { + log: (message: string, ...args: unknown[]) => { + if (enableLog) { + console.log(`[useTable] ${message}`, ...args) + } + }, + warn: (message: string, ...args: unknown[]) => { + if (enableLog) { + console.warn(`[useTable] ${message}`, ...args) + } + }, + error: (message: string, ...args: unknown[]) => { + if (enableLog) { + console.error(`[useTable] ${message}`, ...args) + } + } + } + + // 缓存实例 + const cache = enableCache ? new TableCache(cacheTime, maxCacheSize, enableLog) : null + + // 加载状态机 + type LoadingState = 'idle' | 'loading' | 'success' | 'error' + const loadingState = ref('idle') + const loading = computed(() => loadingState.value === 'loading') + + // 错误状态 + const error = ref(null) + + // 表格数据 + const data = ref([]) + + // 请求取消控制器 + let abortController: AbortController | null = null + + // 缓存清理定时器 + let cacheCleanupTimer: NodeJS.Timeout | null = null + + // 搜索参数 + const searchParams = reactive( + Object.assign( + { + [pageKey]: 1, + [sizeKey]: 10 + }, + apiParams || {} + ) as TParams + ) + + // 分页配置 + const pagination = reactive({ + current: ((searchParams as Record)[pageKey] as number) || 1, + size: ((searchParams as Record)[sizeKey] as number) || 10, + total: 0 + }) + + // 移动端分页 (响应式) + const { width } = useWindowSize() + const mobilePagination = computed(() => ({ + ...pagination, + small: width.value < 768 + })) + + // 列配置 + const columnConfig = columnsFactory ? useTableColumns(columnsFactory) : null + const columns = columnConfig?.columns + const columnChecks = columnConfig?.columnChecks + + // 是否有数据 + const hasData = computed(() => data.value.length > 0) + + // 缓存统计信息 + const cacheInfo = computed(() => { + // 依赖触发器,确保缓存变化时重新计算 + void cacheUpdateTrigger.value + if (!cache) return { total: 0, size: '0KB', hitRate: '0 avg hits' } + return cache.getStats() + }) + + // 错误处理函数 + const handleError = createErrorHandler(onError, enableLog) + + // 清理缓存,根据不同的业务场景选择性地清理缓存 + const clearCache = (strategy: CacheInvalidationStrategy, context?: string): void => { + if (!cache) return + + let clearedCount = 0 + + switch (strategy) { + case CacheInvalidationStrategy.CLEAR_ALL: + cache.clear() + logger.log(`清空所有缓存 - ${context || ''}`) + break + + case CacheInvalidationStrategy.CLEAR_CURRENT: + clearedCount = cache.clearCurrentSearch(searchParams) + logger.log(`清空当前搜索缓存 ${clearedCount} 条 - ${context || ''}`) + break + + case CacheInvalidationStrategy.CLEAR_PAGINATION: + clearedCount = cache.clearPagination() + logger.log(`清空分页缓存 ${clearedCount} 条 - ${context || ''}`) + break + + case CacheInvalidationStrategy.KEEP_ALL: + default: + logger.log(`保持缓存不变 - ${context || ''}`) + break + } + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + } + + // 获取数据的核心方法 + const fetchData = async ( + params?: Partial, + useCache = enableCache + ): Promise> => { + // 取消上一个请求 + if (abortController) { + abortController.abort() + } + + // 创建新的取消控制器 + const currentController = new AbortController() + abortController = currentController + + // 状态机:进入 loading 状态 + loadingState.value = 'loading' + error.value = null + + try { + let requestParams = Object.assign( + {}, + searchParams, + { + [pageKey]: pagination.current, + [sizeKey]: pagination.size + }, + params || {} + ) as TParams + + // 剔除不需要的参数 + if (excludeParams.length > 0) { + const filteredParams = { ...requestParams } + excludeParams.forEach((key) => { + delete (filteredParams as Record)[key] + }) + requestParams = filteredParams as TParams + } + + // 检查缓存 + if (useCache && cache) { + const cachedItem = cache.get(requestParams) + if (cachedItem) { + data.value = cachedItem.data + updatePaginationFromResponse(pagination, cachedItem.response) + + // 修复:避免重复设置相同的值,防止响应式循环更新 + const paramsRecord = searchParams as Record + if (paramsRecord[pageKey] !== pagination.current) { + paramsRecord[pageKey] = pagination.current + } + if (paramsRecord[sizeKey] !== pagination.size) { + paramsRecord[sizeKey] = pagination.size + } + + // 状态机:缓存命中,进入 success 状态 + loadingState.value = 'success' + + // 缓存命中时触发专门的回调,而不是 onSuccess + if (onCacheHit) { + onCacheHit(cachedItem.data, cachedItem.response) + } + + logger.log(`缓存命中`) + return cachedItem.response + } + } + + const response = await apiFn(requestParams) + + // 检查请求是否被取消 + if (currentController.signal.aborted) { + throw new Error('请求已取消') + } + + // 使用响应适配器转换为标准格式 + const standardResponse = responseAdapter(response) + + // 处理响应数据 + let tableData = extractTableData(standardResponse) + + // 应用数据转换函数 + if (dataTransformer) { + tableData = dataTransformer(tableData) + } + + // 更新状态 + data.value = tableData + updatePaginationFromResponse(pagination, standardResponse) + + // 修复:避免重复设置相同的值,防止响应式循环更新 + const paramsRecord = searchParams as Record + if (paramsRecord[pageKey] !== pagination.current) { + paramsRecord[pageKey] = pagination.current + } + if (paramsRecord[sizeKey] !== pagination.size) { + paramsRecord[sizeKey] = pagination.size + } + + // 缓存数据 + if (useCache && cache) { + cache.set(requestParams, tableData, standardResponse) + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + logger.log(`数据已缓存`) + } + + // 状态机:请求成功,进入 success 状态 + loadingState.value = 'success' + + // 成功回调 + if (onSuccess) { + onSuccess(tableData, standardResponse) + } + + return standardResponse + } catch (err) { + if (err instanceof Error && err.message === '请求已取消') { + // 请求被取消,回到 idle 状态 + loadingState.value = 'idle' + return { records: [], total: 0, current: 1, size: 10 } + } + + // 状态机:请求失败,进入 error 状态 + loadingState.value = 'error' + data.value = [] + const tableError = handleError(err, '获取表格数据失败') + throw tableError + } finally { + // 只有当前控制器是活跃的才清空 + if (abortController === currentController) { + abortController = null + } + } + } + + // 获取数据 (保持当前页) + const getData = async (params?: Partial): Promise | void> => { + try { + return await fetchData(params) + } catch { + // 错误已在 fetchData 中处理 + return Promise.resolve() + } + } + + // 分页获取数据 (重置到第一页) - 专门用于搜索场景 + const getDataByPage = async (params?: Partial): Promise | void> => { + pagination.current = 1 + ;(searchParams as Record)[pageKey] = 1 + + // 搜索时清空当前搜索条件的缓存,确保获取最新数据 + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据') + + try { + return await fetchData(params, false) // 搜索时不使用缓存 + } catch { + // 错误已在 fetchData 中处理 + return Promise.resolve() + } + } + + // 智能防抖搜索函数 + const debouncedGetDataByPage = createSmartDebounce(getDataByPage, debounceTime) + + // 重置搜索参数 + const resetSearchParams = async (): Promise => { + // 取消防抖的搜索 + debouncedGetDataByPage.cancel() + + // 保存分页相关的默认值 + const paramsRecord = searchParams as Record + const defaultPagination = { + [pageKey]: 1, + [sizeKey]: (paramsRecord[sizeKey] as number) || 10 + } + + // 清空所有搜索参数 + Object.keys(searchParams).forEach((key) => { + delete paramsRecord[key] + }) + + // 重新设置默认参数 + Object.assign(searchParams, apiParams || {}, defaultPagination) + + // 重置分页 + pagination.current = 1 + pagination.size = defaultPagination[sizeKey] as number + + // 清空错误状态 + error.value = null + + // 清空缓存 + clearCache(CacheInvalidationStrategy.CLEAR_ALL, '重置搜索') + + // 重新获取数据 + await getData() + + // 执行重置回调 + if (resetFormCallback) { + await nextTick() + resetFormCallback() + } + } + + // 替换搜索参数:适用于表单查询,避免旧字段残留 + const replaceSearchParams = (params?: Partial): void => { + const paramsRecord = searchParams as Record + const currentSize = pagination.size || ((paramsRecord[sizeKey] as number) ?? 10) + + Object.keys(searchParams).forEach((key) => { + if (key !== pageKey && key !== sizeKey) { + delete paramsRecord[key] + } + }) + + Object.assign( + searchParams, + { + [pageKey]: 1, + [sizeKey]: currentSize + }, + params || {} + ) + + pagination.current = 1 + pagination.size = currentSize + } + + // 防重复调用的标志 + let isCurrentChanging = false + + // 处理分页大小变化 + const handleSizeChange = async (newSize: number): Promise => { + if (newSize <= 0) return + + debouncedGetDataByPage.cancel() + + const paramsRecord = searchParams as Record + pagination.size = newSize + pagination.current = 1 + paramsRecord[sizeKey] = newSize + paramsRecord[pageKey] = 1 + + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '分页大小变化') + + await getData() + } + + // 处理当前页变化 + const handleCurrentChange = async (newCurrent: number): Promise => { + if (newCurrent <= 0) return + + // 修复:防止重复调用 + if (isCurrentChanging) { + return + } + + // 修复:如果当前页没有变化,不需要重新请求 + if (pagination.current === newCurrent) { + logger.log('分页页码未变化,跳过请求') + return + } + + try { + isCurrentChanging = true + + // 修复:只更新必要的状态 + const paramsRecord = searchParams as Record + pagination.current = newCurrent + // 只有当 searchParams 的分页字段与新值不同时才更新 + if (paramsRecord[pageKey] !== newCurrent) { + paramsRecord[pageKey] = newCurrent + } + + await getData() + } finally { + isCurrentChanging = false + } + } + + // 针对不同业务场景的刷新方法 + + // 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) + const refreshCreate = async (): Promise => { + debouncedGetDataByPage.cancel() + pagination.current = 1 + ;(searchParams as Record)[pageKey] = 1 + clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据') + await getData() + } + + // 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) + const refreshUpdate = async (): Promise => { + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '编辑数据') + await getData() + } + + // 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) + const refreshRemove = async (): Promise => { + const { current } = pagination + + // 清除缓存并获取最新数据 + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '删除数据') + await getData() + + // 如果当前页为空且不是第一页,回到上一页 + if (data.value.length === 0 && current > 1) { + pagination.current = current - 1 + ;(searchParams as Record)[pageKey] = current - 1 + await getData() + } + } + + // 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) + const refreshData = async (): Promise => { + debouncedGetDataByPage.cancel() + clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新') + await getData() + } + + // 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) + const refreshSoft = async (): Promise => { + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '软刷新') + await getData() + } + + // 取消当前请求 + const cancelRequest = (): void => { + if (abortController) { + abortController.abort() + } + debouncedGetDataByPage.cancel() + } + + // 清空数据 + const clearData = (): void => { + data.value = [] + error.value = null + clearCache(CacheInvalidationStrategy.CLEAR_ALL, '清空数据') + } + + // 清理已过期的缓存条目,释放内存空间 + const clearExpiredCache = (): number => { + if (!cache) return 0 + const cleanedCount = cache.cleanupExpired() + if (cleanedCount > 0) { + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + } + return cleanedCount + } + + // 设置定期清理过期缓存 + if (enableCache && cache) { + cacheCleanupTimer = setInterval(() => { + const cleanedCount = cache.cleanupExpired() + if (cleanedCount > 0) { + logger.log(`自动清理 ${cleanedCount} 条过期缓存`) + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + } + }, cacheTime / 2) // 每半个缓存周期清理一次 + } + + // 挂载时自动加载数据 + if (immediate) { + onMounted(async () => { + await getData() + }) + } + + // 组件卸载时彻底清理 + onUnmounted(() => { + cancelRequest() + if (cache) { + cache.clear() + } + if (cacheCleanupTimer) { + clearInterval(cacheCleanupTimer) + } + }) + + // 优化的返回值结构 + return { + // 数据相关 + /** 表格数据 */ + data, + /** 数据加载状态 */ + loading: readonly(loading), + /** 错误状态 */ + error: readonly(error), + /** 数据是否为空 */ + isEmpty: computed(() => data.value.length === 0), + /** 是否有数据 */ + hasData, + + // 分页相关 + /** 分页状态信息 */ + pagination: readonly(pagination), + /** 移动端分页配置 */ + paginationMobile: mobilePagination, + /** 页面大小变化处理 */ + handleSizeChange, + /** 当前页变化处理 */ + handleCurrentChange, + + // 搜索相关 - 统一前缀 + /** 搜索参数 */ + searchParams, + /** 替换搜索参数(适用于表单查询,避免旧字段残留) */ + replaceSearchParams, + /** 重置搜索参数 */ + resetSearchParams, + + // 数据操作 - 更明确的操作意图 + /** 加载数据 */ + fetchData: getData, + /** 获取数据 */ + getData: getDataByPage, + /** 获取数据(防抖) */ + getDataDebounced: debouncedGetDataByPage, + /** 清空数据 */ + clearData, + + // 刷新策略 + /** 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) */ + refreshData, + /** 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) */ + refreshSoft, + /** 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) */ + refreshCreate, + /** 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) */ + refreshUpdate, + /** 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) */ + refreshRemove, + + // 缓存控制 + /** 缓存统计信息 */ + cacheInfo, + /** 清除缓存,根据不同的业务场景选择性地清理缓存: */ + clearCache, + // 支持4种清理策略 + // clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新') // 清空所有缓存 + // clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据') // 只清空当前搜索条件的缓存 + // clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据') // 清空分页相关缓存 + // clearCache(CacheInvalidationStrategy.KEEP_ALL, '保持缓存') // 不清理任何缓存 + /** 清理已过期的缓存条目,释放内存空间 */ + clearExpiredCache, + + // 请求控制 + /** 取消当前请求 */ + cancelRequest, + + // 列配置 (如果提供了 columnsFactory) + ...(columnConfig && { + /** 表格列配置 */ + columns, + /** 列显示控制 */ + columnChecks, + /** 新增列 */ + addColumn: columnConfig.addColumn, + /** 删除列 */ + removeColumn: columnConfig.removeColumn, + /** 切换列显示状态 */ + toggleColumn: columnConfig.toggleColumn, + /** 更新列配置 */ + updateColumn: columnConfig.updateColumn, + /** 批量更新列配置 */ + batchUpdateColumns: columnConfig.batchUpdateColumns, + /** 重新排序列 */ + reorderColumns: columnConfig.reorderColumns, + /** 获取指定列配置 */ + getColumnConfig: columnConfig.getColumnConfig, + /** 获取所有列配置 */ + getAllColumns: columnConfig.getAllColumns, + /** 重置所有列配置到默认状态 */ + resetColumns: columnConfig.resetColumns + }) + } +} + +// 重新导出类型和枚举,方便使用 +export { CacheInvalidationStrategy } from '../../utils/table/tableCache' +export type { ApiResponse, CacheItem } from '../../utils/table/tableCache' +export type { BaseRequestParams, TableError } from '../../utils/table/tableUtils' diff --git a/admin/src/hooks/core/useTableColumns.ts b/admin/src/hooks/core/useTableColumns.ts new file mode 100644 index 0000000..84b6e13 --- /dev/null +++ b/admin/src/hooks/core/useTableColumns.ts @@ -0,0 +1,312 @@ +/** + * useTableColumns - 表格列配置管理 + * + * 提供动态的表格列配置管理能力,支持运行时灵活控制列的显示、隐藏、排序等操作。 + * 通常与 useTable 配合使用,为表格提供完整的列管理功能。 + * + * ## 主要功能 + * + * 1. 列显示控制 - 动态显示/隐藏列,支持批量操作 + * 2. 列排序 - 拖拽或编程方式重新排列列顺序 + * 3. 列配置管理 - 新增、删除、更新列配置 + * 4. 特殊列支持 - 自动处理 selection、expand、index 等特殊列 + * 5. 状态持久化 - 保持列的显示状态,支持重置到初始状态 + * + * ## 使用示例 + * + * ```typescript + * const { columns, columnChecks, toggleColumn, reorderColumns } = useTableColumns(() => [ + * { prop: 'name', label: '姓名', visible: true }, + * { prop: 'email', label: '邮箱', visible: true }, + * { prop: 'status', label: '状态', visible: false } + * ]) + * + * // 切换列显示 + * toggleColumn('email', false) + * + * // 重新排序 + * reorderColumns(0, 2) + * ``` + * + * @module useTableColumns + * @author Art Design Pro Team + */ + +import { ref, computed, watch } from 'vue' +import { $t } from '@/locales' +import type { ColumnOption } from '@/types/component' + +/** + * 特殊列类型 + */ +const SPECIAL_COLUMNS: Record = { + selection: { prop: '__selection__', label: $t('table.column.selection') }, + expand: { prop: '__expand__', label: $t('table.column.expand') }, + index: { prop: '__index__', label: $t('table.column.index') } +} + +/** + * 获取列的唯一标识 + */ +export const getColumnKey = (col: ColumnOption) => + SPECIAL_COLUMNS[col.type as keyof typeof SPECIAL_COLUMNS]?.prop ?? (col.prop as string) + +/** + * 获取列的显示状态 + * 优先使用 visible 字段,如果不存在则使用 checked 字段 + */ +export const getColumnVisibility = (col: ColumnOption): boolean => { + // visible 优先级高于 checked + if (col.visible !== undefined) { + return col.visible + } + // 如果 visible 未定义,使用 checked,默认为 true + return col.checked ?? true +} + +/** + * 获取列的检查状态 + */ +export const getColumnChecks = (columns: ColumnOption[]) => + columns.map((col) => { + const special = col.type && SPECIAL_COLUMNS[col.type] + const visibility = getColumnVisibility(col) + + if (special) { + return { ...col, prop: special.prop, label: special.label, checked: true, visible: true } + } + return { ...col, checked: visibility, visible: visibility } + }) + +/** + * 动态列配置接口 + */ +export interface DynamicColumnConfig { + /** + * 新增列(支持单个或批量) + * @param column 列配置或列配置数组 + * @param index 可选的插入位置,默认末尾(批量时为第一个列的位置) + */ + addColumn: (column: ColumnOption | ColumnOption[], index?: number) => void + /** + * 删除列(支持单个或批量) + * @param prop 列的唯一标识或标识数组 + */ + removeColumn: (prop: string | string[]) => void + /** + * 切换列显示状态(支持单个或批量) + * @param prop 列的唯一标识或标识数组 + * @param visible 可选的显示状态,默认取反 + */ + toggleColumn: (prop: string | string[], visible?: boolean) => void + + /** + * 更新列(支持单个或批量) + * @param prop 列的唯一标识或更新配置数组 + * @param updates 列配置更新(当 prop 为字符串时使用) + */ + updateColumn: ( + prop: string | Array<{ prop: string; updates: Partial> }>, + updates?: Partial> + ) => void + /** + * 批量更新列(兼容旧版本,推荐使用 updateColumn 的数组模式) + * @param updates 列更新配置 + * @deprecated 推荐使用 updateColumn 的数组模式 + */ + batchUpdateColumns: (updates: Array<{ prop: string; updates: Partial> }>) => void + /** + * 重新排序列 + * @param fromIndex 源索引 + * @param toIndex 目标索引 + */ + reorderColumns: (fromIndex: number, toIndex: number) => void + /** + * 获取列配置 + * @param prop 列的唯一标识 + * @returns 列配置 + */ + getColumnConfig: (prop: string) => ColumnOption | undefined + /** + * 获取所有列配置 + * @returns 所有列配置 + */ + getAllColumns: () => ColumnOption[] + /** + * 重置所有列 + */ + resetColumns: () => void +} + +export function useTableColumns( + columnsFactory: () => ColumnOption[] +): { + columns: any + columnChecks: any +} & DynamicColumnConfig { + const dynamicColumns = ref[]>(columnsFactory()) + const columnChecks = ref[]>(getColumnChecks(dynamicColumns.value)) + + // 当 dynamicColumns 变动时,重新生成 columnChecks 且保留已存在的显示状态 + watch( + dynamicColumns, + (newCols) => { + const visibilityMap = new Map( + columnChecks.value.map((c) => [getColumnKey(c), getColumnVisibility(c)]) + ) + const newChecks = getColumnChecks(newCols).map((c) => { + const key = getColumnKey(c) + const visibility = visibilityMap.has(key) ? visibilityMap.get(key) : getColumnVisibility(c) + return { + ...c, + checked: visibility, + visible: visibility + } + }) + columnChecks.value = newChecks + }, + { deep: true } + ) + + // 当前显示列(基于 columnChecks 的 checked 或 visible) + const columns = computed(() => { + const colMap = new Map(dynamicColumns.value.map((c) => [getColumnKey(c), c])) + return columnChecks.value + .filter((c) => getColumnVisibility(c)) + .map((c) => colMap.get(getColumnKey(c))) + .filter(Boolean) as ColumnOption[] + }) + + // 支持 updater 返回新数组或直接在传入数组上 mutate + const setDynamicColumns = (updater: (cols: ColumnOption[]) => void | ColumnOption[]) => { + const copy = [...dynamicColumns.value] + const result = updater(copy) + dynamicColumns.value = Array.isArray(result) ? result : copy + } + + return { + columns, + columnChecks, + + /** + * 新增列(支持单个或批量) + */ + addColumn: (column: ColumnOption | ColumnOption[], index?: number) => + setDynamicColumns((cols) => { + const next = [...cols] + const columnsToAdd = Array.isArray(column) ? column : [column] + const insertIndex = + typeof index === 'number' && index >= 0 && index <= next.length ? index : next.length + + // 批量插入 + next.splice(insertIndex, 0, ...columnsToAdd) + return next + }), + + /** + * 删除列(支持单个或批量) + */ + removeColumn: (prop: string | string[]) => + setDynamicColumns((cols) => { + const propsToRemove = Array.isArray(prop) ? prop : [prop] + return cols.filter((c) => !propsToRemove.includes(getColumnKey(c))) + }), + + /** + * 更新列(支持单个或批量) + */ + updateColumn: ( + prop: string | Array<{ prop: string; updates: Partial> }>, + updates?: Partial> + ) => { + // 批量模式:prop 是数组 + if (Array.isArray(prop)) { + setDynamicColumns((cols) => { + const map = new Map(prop.map((u) => [u.prop, u.updates])) + return cols.map((c) => { + const key = getColumnKey(c) + const upd = map.get(key) + return upd ? { ...c, ...upd } : c + }) + }) + } + // 单个模式:prop 是字符串 + else if (updates) { + setDynamicColumns((cols) => + cols.map((c) => (getColumnKey(c) === prop ? { ...c, ...updates } : c)) + ) + } + }, + + /** + * 切换列显示状态(支持单个或批量) + */ + toggleColumn: (prop: string | string[], visible?: boolean) => { + const propsToToggle = Array.isArray(prop) ? prop : [prop] + const next = [...columnChecks.value] + + propsToToggle.forEach((p) => { + const i = next.findIndex((c) => getColumnKey(c) === p) + if (i > -1) { + const currentVisibility = getColumnVisibility(next[i]) + const newVisibility = visible ?? !currentVisibility + // 同时更新 checked 和 visible 以保持兼容性 + next[i] = { ...next[i], checked: newVisibility, visible: newVisibility } + } + }) + + columnChecks.value = next + }, + + /** + * 重置所有列 + */ + resetColumns: () => { + dynamicColumns.value = columnsFactory() + }, + + /** + * 批量更新列(兼容旧版本) + * @deprecated 推荐使用 updateColumn 的数组模式 + */ + batchUpdateColumns: (updates) => + setDynamicColumns((cols) => { + const map = new Map(updates.map((u) => [u.prop, u.updates])) + return cols.map((c) => { + const key = getColumnKey(c) + const upd = map.get(key) + return upd ? { ...c, ...upd } : c + }) + }), + + /** + * 重新排序列 + */ + reorderColumns: (fromIndex: number, toIndex: number) => + setDynamicColumns((cols) => { + if ( + fromIndex < 0 || + fromIndex >= cols.length || + toIndex < 0 || + toIndex >= cols.length || + fromIndex === toIndex + ) { + return cols + } + const next = [...cols] + const [moved] = next.splice(fromIndex, 1) + next.splice(toIndex, 0, moved) + return next + }), + + /** + * 获取列配置 + */ + getColumnConfig: (prop: string) => dynamicColumns.value.find((c) => getColumnKey(c) === prop), + + /** + * 获取所有列配置 + */ + getAllColumns: () => [...dynamicColumns.value] + } +} diff --git a/admin/src/hooks/core/useTableHeight.ts b/admin/src/hooks/core/useTableHeight.ts new file mode 100644 index 0000000..8fdf6da --- /dev/null +++ b/admin/src/hooks/core/useTableHeight.ts @@ -0,0 +1,105 @@ +/** + * useTableHeight - 表格高度自动计算 + * + * 自动计算表格容器的最佳高度,确保表格在不同布局场景下都能正确显示。 + * 根据表格头部、分页器等元素的高度动态调整容器高度,避免出现滚动条或布局错乱。 + * + * ## 主要功能 + * + * 1. 动态高度计算 - 根据表格头部、分页器高度自动计算容器高度 + * 2. 响应式更新 - 配置变化时自动重新计算高度 + * 3. 灵活配置 - 支持自定义各部分高度和间距 + * 4. 智能适配 - 无额外元素时自动使用 100% 高度 + * + * @module useTableHeight + * @author Art Design Pro Team + */ + +import { computed, type Ref } from 'vue' + +/** + * 表格高度计算器配置接口 + */ +interface TableHeightOptions { + /** 是否显示表格头部 */ + showTableHeader: Ref + /** 分页器高度 */ + paginationHeight: Ref + /** 表格头部高度 */ + tableHeaderHeight: Ref + /** 分页器间距 */ + paginationSpacing: Ref +} + +/** + * 表格高度计算器类 + */ +class TableHeightCalculator { + // 常量配置 + private static readonly DEFAULT_TABLE_HEADER_HEIGHT = 44 + private static readonly TABLE_HEADER_SPACING = 12 + + constructor(private options: TableHeightOptions) {} + + /** + * 计算容器高度 + */ + calculate(): { height: string } { + const offset = this.calculateOffset() + return { + height: offset === 0 ? '100%' : `calc(100% - ${offset}px)` + } + } + + /** + * 计算偏移量 + */ + private calculateOffset(): number { + if (!this.options.showTableHeader.value) { + return this.calculatePaginationOffset() + } + + const headerHeight = this.getHeaderHeight() + const paginationOffset = this.calculatePaginationOffset() + + return headerHeight + paginationOffset + TableHeightCalculator.TABLE_HEADER_SPACING + } + + /** + * 获取表格头部高度 + */ + private getHeaderHeight(): number { + return this.options.tableHeaderHeight.value || TableHeightCalculator.DEFAULT_TABLE_HEADER_HEIGHT + } + + /** + * 计算分页器偏移量 + */ + private calculatePaginationOffset(): number { + const { paginationHeight, paginationSpacing } = this.options + return paginationHeight.value === 0 ? 0 : paginationHeight.value + paginationSpacing.value + } +} + +/** + * 表格高度计算 Hook + * + * 提供表格容器高度的自动计算功能,支持: + * - 表格头部高度 + * - 分页器高度 + * - 动态间距计算 + * + * @param options 配置选项 + * @returns 容器高度计算结果 + */ +export function useTableHeight(options: TableHeightOptions) { + const containerHeight = computed(() => { + const calculator = new TableHeightCalculator(options) + return calculator.calculate() + }) + + return { + /** 容器高度样式对象 */ + containerHeight + } +} diff --git a/admin/src/hooks/core/useTheme.ts b/admin/src/hooks/core/useTheme.ts new file mode 100644 index 0000000..187c3e0 --- /dev/null +++ b/admin/src/hooks/core/useTheme.ts @@ -0,0 +1,174 @@ +/** + * useTheme - 系统主题管理 + * + * 提供完整的主题切换和管理功能,支持亮色、暗色和自动模式。 + * 自动处理主题切换时的过渡效果,确保切换流畅无闪烁。 + * + * ## 主要功能 + * + * 1. 主题切换 - 支持亮色、暗色、自动三种主题模式 + * 2. 自动模式 - 根据系统偏好自动切换主题 + * 3. 颜色适配 - 自动调整主题色的明暗变体(9 个层级) + * 4. 过渡优化 - 切换时临时禁用过渡效果,避免闪烁 + * 5. 状态持久化 - 主题设置自动保存到 store + * + * ## 使用示例 + * + * ```typescript + * const { switchThemeStyles } = useTheme() + * + * // 切换到暗色主题 + * switchThemeStyles(SystemThemeEnum.DARK) + * + * // 切换到亮色主题 + * switchThemeStyles(SystemThemeEnum.LIGHT) + * + * // 切换到自动模式(跟随系统) + * switchThemeStyles(SystemThemeEnum.AUTO) + * ``` + * + * @module useTheme + * @author Art Design Pro Team + */ + +import { useSettingStore } from '@/store/modules/setting' +import { SystemThemeEnum } from '@/enums/appEnum' +import AppConfig from '@/config' +import { SystemThemeTypes } from '@/types/store' +import { getDarkColor, getLightColor, setElementThemeColor } from '@/utils/ui' +import { usePreferredDark } from '@vueuse/core' +import { watch } from 'vue' + +export function useTheme() { + const settingStore = useSettingStore() + + // 禁用过渡效果 + const disableTransitions = () => { + const style = document.createElement('style') + style.setAttribute('id', 'disable-transitions') + style.textContent = '* { transition: none !important; }' + document.head.appendChild(style) + } + + // 启用过渡效果 + const enableTransitions = () => { + const style = document.getElementById('disable-transitions') + if (style) { + style.remove() + } + } + + // 设置系统主题 + const setSystemTheme = (theme: SystemThemeEnum, themeMode?: SystemThemeEnum) => { + // 临时禁用过渡效果 + disableTransitions() + + const el = document.getElementsByTagName('html')[0] + const isDark = theme === SystemThemeEnum.DARK + + if (!themeMode) { + themeMode = theme + } + + const currentTheme = AppConfig.systemThemeStyles[theme as keyof SystemThemeTypes] + + if (currentTheme) { + el.setAttribute('class', currentTheme.className) + } + + // 设置按钮颜色加深或变浅 + const primary = settingStore.systemThemeColor + + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty( + `--el-color-primary-light-${i}`, + isDark ? `${getDarkColor(primary, i / 10)}` : `${getLightColor(primary, i / 10)}` + ) + } + + // 更新store中的主题设置 + settingStore.setGlopTheme(theme, themeMode) + + // 使用 requestAnimationFrame 确保在下一帧恢复过渡效果 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + enableTransitions() + }) + }) + } + + // 使用 VueUse 的 usePreferredDark 检测系统主题偏好 + const prefersDark = usePreferredDark() + + // 自动设置系统主题 + const setSystemAutoTheme = () => { + const theme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT + setSystemTheme(theme, SystemThemeEnum.AUTO) + } + + // 切换主题 + const switchThemeStyles = (theme: SystemThemeEnum) => { + if (theme === SystemThemeEnum.AUTO) { + setSystemAutoTheme() + } else { + setSystemTheme(theme) + } + } + + return { + setSystemTheme, + setSystemAutoTheme, + switchThemeStyles, + prefersDark + } +} + +/** + * 初始化主题系统 + */ +export function initializeTheme() { + const settingStore = useSettingStore() + const prefersDark = usePreferredDark() + + // 根据系统偏好应用主题 + const applyThemeByMode = () => { + const el = document.getElementsByTagName('html')[0] + let actualTheme = settingStore.systemThemeType + + // 如果是 AUTO 模式,检测系统偏好 + if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) { + actualTheme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT + // 更新实际应用的主题类型 + settingStore.systemThemeType = actualTheme + } + + // 设置主题 class + const currentTheme = AppConfig.systemThemeStyles[actualTheme as keyof SystemThemeTypes] + if (currentTheme) { + el.setAttribute('class', currentTheme.className) + } + + // 设置主题颜色 + setElementThemeColor(settingStore.systemThemeColor) + + // 设置圆角 + document.documentElement.style.setProperty('--custom-radius', `${settingStore.customRadius}rem`) + } + + // 应用主题 + applyThemeByMode() + + // 如果是 AUTO 模式,监听系统主题变化(使用 VueUse 的响应式特性) + if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) { + watch( + prefersDark, + () => { + // 只有在 AUTO 模式下才响应系统主题变化 + if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) { + applyThemeByMode() + } + }, + { immediate: false } + ) + } +} diff --git a/admin/src/hooks/index.ts b/admin/src/hooks/index.ts new file mode 100644 index 0000000..472b09c --- /dev/null +++ b/admin/src/hooks/index.ts @@ -0,0 +1,32 @@ +// 通用功能集合 +export { useCommon } from './core/useCommon' + +// 应用模式 +export { useAppMode } from './core/useAppMode' + +// 权限控制 +export { useAuth } from './core/useAuth' + +// 表格数据管理方案 +export { useTable } from './core/useTable' + +// 表格列配置管理 +export { useTableColumns } from './core/useTableColumns' + +// 主题相关 +export { useTheme } from './core/useTheme' + +// 礼花+文字滚动 +export { useCeremony } from './core/useCeremony' + +// 顶栏快速入口 +export { useFastEnter } from './core/useFastEnter' + +// 顶栏功能管理 +export { useHeaderBar } from './core/useHeaderBar' + +// 图表相关 +export { useChart, useChartComponent, useChartOps } from './core/useChart' + +// 布局高度 +export { useLayoutHeight, useAutoLayoutHeight } from './core/useLayoutHeight' diff --git a/admin/src/locales/index.ts b/admin/src/locales/index.ts new file mode 100644 index 0000000..36c2648 --- /dev/null +++ b/admin/src/locales/index.ts @@ -0,0 +1,123 @@ +/** + * 国际化配置 + * + * 基于 vue-i18n 实现的多语言国际化解决方案。 + * 支持中文和英文切换,自动从本地存储恢复用户的语言偏好。 + * + * ## 主要功能 + * + * - 多语言支持 - 支持中文(简体)和英文两种语言 + * - 语言切换 - 运行时动态切换语言,无需刷新页面 + * - 持久化存储 - 自动保存和恢复用户的语言偏好 + * - 全局注入 - 在任何组件中都可以使用 $t 函数进行翻译 + * - 类型安全 - 提供 TypeScript 类型支持 + * + * ## 支持的语言 + * + * - zh: 简体中文 + * - en: English + * + * @module locales + * @author Art Design Pro Team + */ + +import { createI18n } from 'vue-i18n' +import type { I18n, I18nOptions } from 'vue-i18n' +import { LanguageEnum } from '@/enums/appEnum' +import { getSystemStorage } from '@/utils/storage' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' + +// 同步导入语言文件 +import enMessages from './langs/en.json' +import zhMessages from './langs/zh.json' + +/** + * 存储键管理器实例 + */ +const storageKeyManager = new StorageKeyManager() + +/** + * 语言消息对象 + */ +const messages = { + [LanguageEnum.EN]: enMessages, + [LanguageEnum.ZH]: zhMessages +} + +/** + * 语言选项列表 + * 用于语言切换下拉框 + */ +export const languageOptions = [ + { value: LanguageEnum.ZH, label: '简体中文' }, + { value: LanguageEnum.EN, label: 'English' } +] + +/** + * 从存储中获取语言设置 + * @returns 语言设置,如果获取失败则返回默认语言 + */ +const getDefaultLanguage = (): LanguageEnum => { + // 尝试从版本化的存储中获取语言设置 + try { + const storageKey = storageKeyManager.getStorageKey('user') + const userStore = localStorage.getItem(storageKey) + + if (userStore) { + const { language } = JSON.parse(userStore) + if (language && Object.values(LanguageEnum).includes(language)) { + return language + } + } + } catch (error) { + console.warn('[i18n] 从版本化存储获取语言设置失败:', error) + } + + // 尝试从系统存储中获取语言设置 + try { + const sys = getSystemStorage() + if (sys) { + const { user } = JSON.parse(sys) + if (user?.language && Object.values(LanguageEnum).includes(user.language)) { + return user.language + } + } + } catch (error) { + console.warn('[i18n] 从系统存储获取语言设置失败:', error) + } + + // 返回默认语言 + console.debug('[i18n] 使用默认语言:', LanguageEnum.ZH) + return LanguageEnum.ZH +} + +/** + * i18n 配置选项 + */ +const i18nOptions: I18nOptions = { + locale: getDefaultLanguage(), + legacy: false, + globalInjection: true, + fallbackLocale: LanguageEnum.ZH, + messages +} + +/** + * i18n 实例 + */ +const i18n: I18n = createI18n(i18nOptions) + +/** + * 翻译函数类型 + */ +interface Translation { + (key: string): string +} + +/** + * 全局翻译函数 + * 可在任何地方使用,无需导入 useI18n + */ +export const $t = i18n.global.t as Translation + +export default i18n diff --git a/admin/src/locales/langs/en.json b/admin/src/locales/langs/en.json new file mode 100644 index 0000000..b98b778 --- /dev/null +++ b/admin/src/locales/langs/en.json @@ -0,0 +1,257 @@ +{ + "httpMsg": { + "unauthorized": "Unauthorized access, please login again", + "forbidden": "Access to this resource is forbidden", + "notFound": "The requested resource does not exist", + "methodNotAllowed": "Request method not allowed", + "requestTimeout": "Request timeout, please try again later", + "internalServerError": "Internal server error, please try again later", + "badGateway": "Bad gateway error, please try again later", + "serviceUnavailable": "Service temporarily unavailable, please try again later", + "gatewayTimeout": "Gateway timeout, please try again later", + "requestCancelled": "Request cancelled", + "networkError": "Network connection error, please check your connection", + "requestFailed": "Request failed", + "requestConfigError": "Request configuration error" + }, + "topBar": { + "search": { + "title": "Search" + }, + "user": { + "changePassword": "Change Password", + "lockScreen": "Lock Screen", + "logout": "Log Out" + }, + "guide": { + "title": "Click here to view", + "theme": "Theme style", + "menu": "Open top menu", + "description": "More configurations" + } + }, + "common": { + "tips": "Prompt", + "cancel": "Cancel", + "confirm": "Confirm", + "logOutTips": "Do you want to log out?" + }, + "search": { + "placeholder": "Search page", + "historyTitle": "Search history", + "switchKeydown": "Navigate", + "selectKeydown": "Select", + "exitKeydown": "Close" + }, + "setting": { + "menuType": { + "title": "Menu Layout", + "list": [ + "Vertical", + "Horizontal", + "Mixed", + "Dual" + ] + }, + "theme": { + "title": "Theme Style", + "list": [ + "Light", + "Dark", + "System" + ] + }, + "menu": { + "title": "Menu Style" + }, + "color": { + "title": "Theme Color" + }, + "box": { + "title": "Box Style", + "list": [ + "Border", + "Shadow" + ] + }, + "container": { + "title": "Container Width", + "list": [ + "Full", + "Boxed" + ] + }, + "basics": { + "title": "Basic Config", + "list": { + "multiTab": "Show work tab", + "accordion": "Sidebar opens accordion", + "collapseSidebar": "Show sidebar button", + "reloadPage": "Show reload page button", + "fastEnter": "Show fast enter", + "breadcrumb": "Show crumb navigation", + "language": "Show multilingual selection", + "progressBar": "Show top progress bar", + "weakMode": "Color Weakness Mode", + "watermark": "Global watermark", + "menuWidth": "Menu width", + "tabStyle": "Tab style", + "pageTransition": "Page animation", + "borderRadius": "Custom radius" + } + }, + "tabStyle": { + "default": "Default", + "card": "Card", + "google": "Chrome" + }, + "transition": { + "list": { + "none": "None", + "fade": "Fade", + "slideLeft": "Slide Left", + "slideBottom": "Slide Bottom", + "slideTop": "Slide Top" + } + }, + "actions": { + "resetConfig": "Reset Config", + "copyConfig": "Copy Config", + "copySuccess": "Configuration copied to clipboard, paste it into src/config/setting.ts file", + "copyFailed": "Copy failed, please try again", + "resetFailed": "Reset failed, please refresh the page and try again" + } + }, + "notice": { + "title": "Notice", + "btnRead": "Mark as read", + "bar": [ + "Notice", + "Message", + "Todo" + ], + "text": [ + "No" + ], + "viewAll": "View all" + }, + "worktab": { + "btn": { + "refresh": "Refresh", + "fixed": "Fixed", + "unfixed": "Unfixed", + "closeLeft": "Close left", + "closeRight": "Close right", + "closeOther": "Close other", + "closeAll": "Close all" + } + }, + "login": { + "leftView": { + "title": "all-kefu Multi-tenant AI Support", + "subTitle": "Tenant config · Conversations · Statistics" + }, + "title": "Welcome Back", + "subTitle": "Sign in with your account and password", + "placeholder": { + "username": "Username", + "password": "Password", + "slider": "Drag to verify" + }, + "sliderText": "Drag to verify", + "sliderSuccessText": "Verified", + "rememberPwd": "Remember me", + "btnText": "Sign in", + "success": { + "title": "Success", + "message": "Welcome back" + } + }, + "lockScreen": { + "pwdError": "Password error", + "lock": { + "inputPlaceholder": "Please input lock screen password", + "btnText": "Lock" + }, + "unlock": { + "inputPlaceholder": "Please input unlock password", + "btnText": "Unlock", + "backBtnText": "Back to login" + } + }, + "greeting": { + "dawn": "Good morning!", + "morning": "Good morning!", + "afternoon": "Good afternoon!", + "evening": "Good evening!" + }, + "exceptionPage": { + "403": "Sorry, you do not have permission to access this page", + "404": "Sorry, the page you are trying to access does not exist", + "500": "Sorry, there was an error on the server", + "gohome": "Go Home" + }, + "menus": { + "login": { + "title": "Login" + }, + "forceChangePassword": { + "title": "Change Password" + }, + "outside": { + "title": "Embedded" + }, + "dashboard": { + "title": "Dashboard", + "console": "Console" + }, + "tenants": { + "title": "Tenants", + "list": "Tenant List" + }, + "conversations": { + "title": "Conversations", + "list": "Conversation List" + }, + "config": { + "title": "Config Center", + "center": "Configuration" + }, + "stats": { + "title": "Statistics", + "overview": "Overview" + }, + "settings": { + "title": "Platform Settings", + "platform": "Platform" + } + }, + "table": { + "form": { + "reset": "Reset", + "submit": "Submit" + }, + "searchBar": { + "reset": "Reset", + "search": "Search", + "expand": "Expand", + "collapse": "Collapse", + "searchInputPlaceholder": "Please enter", + "searchSelectPlaceholder": "Please select" + }, + "selection": "Select", + "sizeOptions": { + "small": "Compact", + "default": "Default", + "large": "Loose" + }, + "column": { + "selection": "Select", + "expand": "Expand", + "index": "Index" + }, + "zebra": "Zebra", + "border": "Border", + "headerBackground": "Header BG" + } +} diff --git a/admin/src/locales/langs/zh.json b/admin/src/locales/langs/zh.json new file mode 100644 index 0000000..cddf7ca --- /dev/null +++ b/admin/src/locales/langs/zh.json @@ -0,0 +1,257 @@ +{ + "httpMsg": { + "unauthorized": "未授权访问,请重新登录", + "forbidden": "禁止访问该资源", + "notFound": "请求的资源不存在", + "methodNotAllowed": "请求方法不允许", + "requestTimeout": "请求超时,请稍后重试", + "internalServerError": "服务器内部错误,请稍后重试", + "badGateway": "网关错误,请稍后重试", + "serviceUnavailable": "服务暂时不可用,请稍后重试", + "gatewayTimeout": "网关超时,请稍后重试", + "requestCancelled": "请求已取消", + "networkError": "网络连接异常,请检查网络连接", + "requestFailed": "请求失败", + "requestConfigError": "请求配置错误" + }, + "topBar": { + "search": { + "title": "搜索" + }, + "user": { + "changePassword": "修改密码", + "lockScreen": "锁定屏幕", + "logout": "退出登录" + }, + "guide": { + "title": "点击这里查看", + "theme": "主题风格", + "menu": "开启顶栏菜单", + "description": "等更多配置" + } + }, + "common": { + "tips": "提示", + "cancel": "取消", + "confirm": "确定", + "logOutTips": "您是否要退出登录?" + }, + "search": { + "placeholder": "搜索页面", + "historyTitle": "搜索历史", + "switchKeydown": "切换", + "selectKeydown": "选择", + "exitKeydown": "关闭" + }, + "setting": { + "menuType": { + "title": "菜单布局", + "list": [ + "垂直", + "水平", + "混合", + "双列" + ] + }, + "theme": { + "title": "主题风格", + "list": [ + "浅色", + "深色", + "系统" + ] + }, + "menu": { + "title": "菜单风格" + }, + "color": { + "title": "系统主题色" + }, + "box": { + "title": "盒子样式", + "list": [ + "边框", + "阴影" + ] + }, + "container": { + "title": "容器宽度", + "list": [ + "铺满", + "定宽" + ] + }, + "basics": { + "title": "基础配置", + "list": { + "multiTab": "开启多标签栏", + "accordion": "侧边栏开启手风琴模式", + "collapseSidebar": "显示折叠侧边栏按钮", + "fastEnter": "显示快速入口", + "reloadPage": "显示重载页面按钮", + "breadcrumb": "显示全局面包屑导航", + "language": "显示多语言选择", + "progressBar": "显示顶部进度条", + "weakMode": "色弱模式", + "watermark": "全局水印", + "menuWidth": "菜单宽度", + "tabStyle": "标签页风格", + "pageTransition": "页面切换动画", + "borderRadius": "自定义圆角" + } + }, + "tabStyle": { + "default": "默认", + "card": "卡片", + "google": "谷歌" + }, + "transition": { + "list": { + "none": "无动画", + "fade": "淡入淡出", + "slideLeft": "左侧滑入", + "slideBottom": "下方滑入", + "slideTop": "上方滑入" + } + }, + "actions": { + "resetConfig": "重置配置", + "copyConfig": "复制配置", + "copySuccess": "配置已复制到剪贴板,可粘贴到 src/config/setting.ts 文件中", + "copyFailed": "复制失败,请重试", + "resetFailed": "重置失败,请刷新页面后重试" + } + }, + "notice": { + "title": "通知", + "btnRead": "标为已读", + "bar": [ + "通知", + "消息", + "代办" + ], + "text": [ + "暂无" + ], + "viewAll": "查看全部" + }, + "worktab": { + "btn": { + "refresh": "刷新", + "fixed": "固定", + "unfixed": "取消固定", + "closeLeft": "关闭左侧", + "closeRight": "关闭右侧", + "closeOther": "关闭其他", + "closeAll": "关闭全部" + } + }, + "login": { + "leftView": { + "title": "all-kefu 多租户 AI 客服平台", + "subTitle": "租户配置 · 会话线索 · 数据统计,一站式管理" + }, + "title": "欢迎回来", + "subTitle": "输入您的账号和密码登录", + "placeholder": { + "username": "请输入账号", + "password": "请输入密码", + "slider": "请拖动滑块完成验证" + }, + "sliderText": "按住滑块拖动", + "sliderSuccessText": "验证成功", + "rememberPwd": "记住密码", + "btnText": "登录", + "success": { + "title": "登录成功", + "message": "欢迎回来" + } + }, + "lockScreen": { + "pwdError": "密码错误", + "lock": { + "inputPlaceholder": "请输入锁屏密码", + "btnText": "锁定" + }, + "unlock": { + "inputPlaceholder": "请输入解锁密码", + "btnText": "解锁", + "backBtnText": "返回登录" + } + }, + "greeting": { + "dawn": "凌晨了!", + "morning": "上午好!", + "afternoon": "下午好!", + "evening": "晚上好!" + }, + "exceptionPage": { + "403": "抱歉,您无权访问该页面", + "404": "抱歉,您访问的页面不存在", + "500": "抱歉,服务器出错了", + "gohome": "返回首页" + }, + "menus": { + "login": { + "title": "登录" + }, + "forceChangePassword": { + "title": "强制修改密码" + }, + "outside": { + "title": "内嵌页面" + }, + "dashboard": { + "title": "仪表盘", + "console": "工作台" + }, + "tenants": { + "title": "租户管理", + "list": "租户列表" + }, + "conversations": { + "title": "会话与线索", + "list": "会话列表" + }, + "config": { + "title": "配置中心", + "center": "配置管理" + }, + "stats": { + "title": "统计分析", + "overview": "数据总览" + }, + "settings": { + "title": "平台设置", + "platform": "平台参数" + } + }, + "table": { + "form": { + "reset": "重置", + "submit": "提交" + }, + "searchBar": { + "reset": "重置", + "search": "查询", + "expand": "展开", + "collapse": "收起", + "searchInputPlaceholder": "请输入", + "searchSelectPlaceholder": "请选择" + }, + "selection": "选择", + "sizeOptions": { + "small": "紧凑", + "default": "默认", + "large": "宽松" + }, + "column": { + "selection": "勾选", + "expand": "展开", + "index": "序号" + }, + "zebra": "斑马纹", + "border": "边框", + "headerBackground": "表头背景" + } +} diff --git a/admin/src/main.ts b/admin/src/main.ts new file mode 100644 index 0000000..7df948a --- /dev/null +++ b/admin/src/main.ts @@ -0,0 +1,25 @@ +import App from './App.vue' +import { createApp } from 'vue' +import { initStore } from './store' // Store +import { initRouter } from './router' // Router +import language from './locales' // 国际化 +import '@styles/core/tailwind.css' // tailwind +import '@styles/index.scss' // 样式 +import '@utils/sys/console.ts' // 控制台输出内容 +import { setupGlobDirectives } from './directives' +import { setupErrorHandle } from './utils/sys/error-handle' + +document.addEventListener( + 'touchstart', + function () {}, + { passive: false } +) + +const app = createApp(App) +initStore(app) +initRouter(app) +setupGlobDirectives(app) +setupErrorHandle(app) + +app.use(language) +app.mount('#app') \ No newline at end of file diff --git a/admin/src/plugins/echarts.ts b/admin/src/plugins/echarts.ts new file mode 100644 index 0000000..4f56d89 --- /dev/null +++ b/admin/src/plugins/echarts.ts @@ -0,0 +1,76 @@ +/** + * ECharts 插件配置 + * + * 按需导入 ECharts 图表和组件,减小打包体积。 + * 只注册项目中实际使用的图表类型和组件。 + * + * @module plugins/echarts + * @author Art Design Pro Team + */ + +// ECharts 按需导入配置 +import * as echarts from 'echarts/core' + +// 导入图表类型 +import { + BarChart, + LineChart, + PieChart, + ScatterChart, + RadarChart, + MapChart, + CandlestickChart +} from 'echarts/charts' + +// 导入组件 +import { + TitleComponent, + TooltipComponent, + GridComponent, + LegendComponent, + DataZoomComponent, + MarkPointComponent, + MarkLineComponent, + ToolboxComponent, + BrushComponent, + GeoComponent, + VisualMapComponent +} from 'echarts/components' + +// 导入渲染器 +import { CanvasRenderer } from 'echarts/renderers' + +// 注册必要的组件 +echarts.use([ + // 图表类型 + BarChart, + LineChart, + PieChart, + ScatterChart, + RadarChart, + MapChart, + CandlestickChart, + + // 组件 + TitleComponent, + TooltipComponent, + GridComponent, + LegendComponent, + DataZoomComponent, + MarkPointComponent, + MarkLineComponent, + ToolboxComponent, + BrushComponent, + GeoComponent, + VisualMapComponent, + + // 渲染器 + CanvasRenderer +]) + +// 导出 echarts 实例和类型 +export { echarts } +export type { EChartsOption, BarSeriesOption } from 'echarts' + +// 导出常用的图形工具 +export const graphic = echarts.graphic diff --git a/admin/src/plugins/index.ts b/admin/src/plugins/index.ts new file mode 100644 index 0000000..4536a86 --- /dev/null +++ b/admin/src/plugins/index.ts @@ -0,0 +1,6 @@ +/** + * 插件统一导出 + * 集中管理第三方库的封装和配置 + */ + +export * from './echarts' diff --git a/admin/src/router/core/ComponentLoader.ts b/admin/src/router/core/ComponentLoader.ts new file mode 100644 index 0000000..8af3ce3 --- /dev/null +++ b/admin/src/router/core/ComponentLoader.ts @@ -0,0 +1,82 @@ +/** + * 组件加载器 + * + * 负责动态加载 Vue 组件 + * + * @module router/core/ComponentLoader + * @author Art Design Pro Team + */ + +import { h } from 'vue' + +export class ComponentLoader { + private modules: Record Promise> + + constructor() { + // 动态导入 views 目录下所有 .vue 组件 + this.modules = import.meta.glob('../../views/**/*.vue') + } + + /** + * 加载组件 + */ + load(componentPath: string): () => Promise { + if (!componentPath) { + return this.createEmptyComponent() + } + + // 构建可能的路径 + const fullPath = `../../views${componentPath}.vue` + const fullPathWithIndex = `../../views${componentPath}/index.vue` + + // 先尝试直接路径,再尝试添加/index的路径 + const module = this.modules[fullPath] || this.modules[fullPathWithIndex] + + if (!module) { + console.error( + `[ComponentLoader] 未找到组件: ${componentPath},尝试过的路径: ${fullPath} 和 ${fullPathWithIndex}` + ) + return this.createErrorComponent(componentPath) + } + + return module + } + + /** + * 加载布局组件 + */ + loadLayout(): () => Promise { + return () => import('@/views/index/index.vue') + } + + /** + * 加载 iframe 组件 + */ + loadIframe(): () => Promise { + return () => import('@/views/outside/Iframe.vue') + } + + /** + * 创建空组件 + */ + private createEmptyComponent(): () => Promise { + return () => + Promise.resolve({ + render() { + return h('div', {}) + } + }) + } + + /** + * 创建错误提示组件 + */ + private createErrorComponent(componentPath: string): () => Promise { + return () => + Promise.resolve({ + render() { + return h('div', { class: 'route-error' }, `组件未找到: ${componentPath}`) + } + }) + } +} diff --git a/admin/src/router/core/IframeRouteManager.ts b/admin/src/router/core/IframeRouteManager.ts new file mode 100644 index 0000000..c054ca1 --- /dev/null +++ b/admin/src/router/core/IframeRouteManager.ts @@ -0,0 +1,78 @@ +/** + * Iframe 路由管理器 + * + * 负责管理 iframe 类型的路由 + * + * @module router/core/IframeRouteManager + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' + +export class IframeRouteManager { + private static instance: IframeRouteManager + private iframeRoutes: AppRouteRecord[] = [] + + private constructor() {} + + static getInstance(): IframeRouteManager { + if (!IframeRouteManager.instance) { + IframeRouteManager.instance = new IframeRouteManager() + } + return IframeRouteManager.instance + } + + /** + * 添加 iframe 路由 + */ + add(route: AppRouteRecord): void { + if (!this.iframeRoutes.find((r) => r.path === route.path)) { + this.iframeRoutes.push(route) + } + } + + /** + * 获取所有 iframe 路由 + */ + getAll(): AppRouteRecord[] { + return this.iframeRoutes + } + + /** + * 根据路径查找 iframe 路由 + */ + findByPath(path: string): AppRouteRecord | undefined { + return this.iframeRoutes.find((route) => route.path === path) + } + + /** + * 清空所有 iframe 路由 + */ + clear(): void { + this.iframeRoutes = [] + } + + /** + * 保存到 sessionStorage + */ + save(): void { + if (this.iframeRoutes.length > 0) { + sessionStorage.setItem('iframeRoutes', JSON.stringify(this.iframeRoutes)) + } + } + + /** + * 从 sessionStorage 加载 + */ + load(): void { + try { + const data = sessionStorage.getItem('iframeRoutes') + if (data) { + this.iframeRoutes = JSON.parse(data) + } + } catch (error) { + console.error('[IframeRouteManager] 加载 iframe 路由失败:', error) + this.iframeRoutes = [] + } + } +} diff --git a/admin/src/router/core/MenuProcessor.ts b/admin/src/router/core/MenuProcessor.ts new file mode 100644 index 0000000..c424867 --- /dev/null +++ b/admin/src/router/core/MenuProcessor.ts @@ -0,0 +1,264 @@ +/** + * 菜单处理器 + * + * 负责菜单数据的获取、过滤和处理 + * + * @module router/core/MenuProcessor + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' +import { useUserStore } from '@/store/modules/user' +import { asyncRoutes } from '../routes/asyncRoutes' +import { RoutesAlias } from '../routesAlias' +import { formatMenuTitle } from '@/utils' + +export class MenuProcessor { + /** + * 获取菜单数据 + */ + async getMenuList(): Promise { + const menuList = await this.processFrontendMenu() + + // 在规范化路径之前,验证原始路径配置 + this.validateMenuPaths(menuList) + + // 规范化路径(将相对路径转换为完整路径) + return this.normalizeMenuPaths(menuList) + } + + /** + * 处理前端控制模式的菜单(本平台固定前端权限模式: + * 菜单来自本地路由模块,按登录角色 meta.roles 过滤) + */ + private async processFrontendMenu(): Promise { + const userStore = useUserStore() + const roles = userStore.info?.roles + + let menuList = [...asyncRoutes] + + // 根据角色过滤菜单 + if (roles && roles.length > 0) { + menuList = this.filterMenuByRoles(menuList, roles) + } + + return this.filterEmptyMenus(menuList) + } + + /** + * 根据角色过滤菜单 + */ + private filterMenuByRoles(menu: AppRouteRecord[], roles: string[]): AppRouteRecord[] { + return menu.reduce((acc: AppRouteRecord[], item) => { + const itemRoles = item.meta?.roles + const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role)) + + if (hasPermission) { + const filteredItem = { ...item } + if (filteredItem.children?.length) { + filteredItem.children = this.filterMenuByRoles(filteredItem.children, roles) + } + acc.push(filteredItem) + } + + return acc + }, []) + } + + /** + * 递归过滤空菜单项 + */ + private filterEmptyMenus(menuList: AppRouteRecord[]): AppRouteRecord[] { + return menuList + .map((item) => { + // 如果有子菜单,先递归过滤子菜单 + if (item.children && item.children.length > 0) { + const filteredChildren = this.filterEmptyMenus(item.children) + return { + ...item, + children: filteredChildren + } + } + return item + }) + .filter((item) => { + // 如果定义了 children 属性(即使是空数组),说明这是一个目录菜单,应该保留 + if ('children' in item) { + return true + } + + // 如果有外链或 iframe,保留 + if (item.meta?.isIframe === true || item.meta?.link) { + return true + } + + // 如果有有效的 component,保留 + if (item.component && item.component !== '' && item.component !== RoutesAlias.Layout) { + return true + } + + // 其他情况过滤掉 + return false + }) + } + + /** + * 验证菜单列表是否有效 + */ + validateMenuList(menuList: AppRouteRecord[]): boolean { + return Array.isArray(menuList) && menuList.length > 0 + } + + /** + * 规范化菜单路径 + * 将相对路径转换为完整路径,确保菜单跳转正确 + */ + private normalizeMenuPaths(menuList: AppRouteRecord[], parentPath = ''): AppRouteRecord[] { + return menuList.map((item) => { + // 构建完整路径 + const fullPath = this.buildFullPath(item.path || '', parentPath) + + // 递归处理子菜单 + const children = item.children?.length + ? this.normalizeMenuPaths(item.children, fullPath) + : item.children + + const redirect = item.redirect || this.resolveDefaultRedirect(children) + + return { + ...item, + path: fullPath, + redirect, + children + } + }) + } + + /** + * 为目录型菜单推导默认跳转地址 + */ + private resolveDefaultRedirect(children?: AppRouteRecord[]): string | undefined { + if (!children?.length) { + return undefined + } + + for (const child of children) { + if (this.isNavigableRoute(child)) { + return child.path + } + + const nestedRedirect = this.resolveDefaultRedirect(child.children) + if (nestedRedirect) { + return nestedRedirect + } + } + + return undefined + } + + /** + * 判断子路由是否可以作为默认落点 + */ + private isNavigableRoute(route: AppRouteRecord): boolean { + return Boolean( + route.path && + route.path !== '/' && + !route.meta?.link && + route.meta?.isIframe !== true && + route.component && + route.component !== '' + ) + } + + /** + * 验证菜单路径配置 + * 检测非一级菜单是否错误使用了 / 开头的路径 + */ + /** + * 验证菜单路径配置 + * 检测非一级菜单是否错误使用了 / 开头的路径 + */ + private validateMenuPaths(menuList: AppRouteRecord[], level = 1): void { + menuList.forEach((route) => { + if (!route.children?.length) return + + const parentName = String(route.name || route.path || '未知路由') + + route.children.forEach((child) => { + const childPath = child.path || '' + + // 跳过合法的绝对路径:外部链接和 iframe 路由 + if (this.isValidAbsolutePath(childPath)) return + + // 检测非法的绝对路径 + if (childPath.startsWith('/')) { + this.logPathError(child, childPath, parentName, level) + } + }) + + // 递归检查更深层级的子路由 + this.validateMenuPaths(route.children, level + 1) + }) + } + + /** + * 判断是否为合法的绝对路径 + */ + private isValidAbsolutePath(path: string): boolean { + return ( + path.startsWith('http://') || + path.startsWith('https://') || + path.startsWith('/outside/iframe/') + ) + } + + /** + * 输出路径配置错误日志 + */ + private logPathError( + route: AppRouteRecord, + path: string, + parentName: string, + level: number + ): void { + const routeName = String(route.name || path || '未知路由') + const menuTitle = route.meta?.title || routeName + const suggestedPath = path.split('/').pop() || path.slice(1) + + console.error( + `[路由配置错误] 菜单 "${formatMenuTitle(menuTitle)}" (name: ${routeName}, path: ${path}) 配置错误\n` + + ` 位置: ${parentName} > ${routeName}\n` + + ` 问题: ${level + 1}级菜单的 path 不能以 / 开头\n` + + ` 当前配置: path: '${path}'\n` + + ` 应该改为: path: '${suggestedPath}'` + ) + } + + /** + * 构建完整路径 + */ + private buildFullPath(path: string, parentPath: string): string { + if (!path) return '' + + // 外部链接直接返回 + if (path.startsWith('http://') || path.startsWith('https://')) { + return path + } + + // 如果已经是绝对路径,直接返回 + if (path.startsWith('/')) { + return path + } + + // 拼接父路径和当前路径 + if (parentPath) { + // 移除父路径末尾的斜杠,移除子路径开头的斜杠,然后拼接 + const cleanParent = parentPath.replace(/\/$/, '') + const cleanChild = path.replace(/^\//, '') + return `${cleanParent}/${cleanChild}` + } + + // 没有父路径,添加前导斜杠 + return `/${path}` + } +} diff --git a/admin/src/router/core/RoutePermissionValidator.ts b/admin/src/router/core/RoutePermissionValidator.ts new file mode 100644 index 0000000..1e607e0 --- /dev/null +++ b/admin/src/router/core/RoutePermissionValidator.ts @@ -0,0 +1,161 @@ +/** + * 路由权限验证模块 + * + * 提供路由权限验证和路径检查功能 + * + * ## 主要功能 + * + * - 验证路径是否在用户菜单权限中 + * - 构建菜单路径集合(扁平化处理) + * - 支持动态路由参数匹配 + * - 路径前缀匹配 + * + * ## 使用场景 + * + * - 路由守卫中验证用户权限 + * - 动态路由注册后的权限检查 + * - 防止用户访问无权限的页面 + * + * @module router/core/RoutePermissionValidator + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' + +/** + * 路由权限验证器 + */ +export class RoutePermissionValidator { + /** + * 验证路径是否在用户菜单权限中 + * @param targetPath 目标路径 + * @param menuList 菜单列表 + * @returns 是否有权限访问 + */ + static hasPermission(targetPath: string, menuList: AppRouteRecord[]): boolean { + // 根路径始终允许访问 + if (targetPath === '/') { + return true + } + + return this.matchRoute(targetPath, menuList) + } + + /** + * 构建菜单路径集合(扁平化处理) + * @param menuList 菜单列表 + * @param pathSet 路径集合 + * @returns 路径集合 + */ + static buildMenuPathSet( + menuList: AppRouteRecord[], + pathSet: Set = new Set() + ): Set { + if (!Array.isArray(menuList) || menuList.length === 0) { + return pathSet + } + + for (const menuItem of menuList) { + if (!menuItem.path) { + continue + } + + // 标准化路径并添加到集合 + const menuPath = menuItem.path.startsWith('/') ? menuItem.path : `/${menuItem.path}` + pathSet.add(menuPath) + + // 递归处理子菜单 + if (menuItem.children?.length) { + this.buildMenuPathSet(menuItem.children, pathSet) + } + } + + return pathSet + } + + /** + * 检查目标路径是否匹配集合中的某个路径前缀 + * 用于支持动态路由参数匹配,如 /user/123 匹配 /user + * @param targetPath 目标路径 + * @param pathSet 路径集合 + * @returns 是否匹配 + */ + static checkPathPrefix(targetPath: string, pathSet: Set): boolean { + // 遍历路径集合,检查是否有前缀匹配 + for (const menuPath of pathSet) { + if (targetPath.startsWith(`${menuPath}/`)) { + return true + } + } + return false + } + + /** + * 递归匹配路由配置,支持隐藏路由和动态参数路由 + */ + static matchRoute(targetPath: string, routes: AppRouteRecord[]): boolean { + if (!Array.isArray(routes) || routes.length === 0) { + return false + } + + for (const route of routes) { + if (!route.path) { + continue + } + + const routePath = route.path.startsWith('/') ? route.path : `/${route.path}` + + if ( + routePath === targetPath || + this.isDynamicRouteMatch(targetPath, routePath) || + targetPath.startsWith(`${routePath}/`) + ) { + return true + } + + if (route.children?.length && this.matchRoute(targetPath, route.children)) { + return true + } + } + + return false + } + + /** + * 检查目标路径是否匹配动态参数路由,如 /demo/123 匹配 /demo/:id + */ + static isDynamicRouteMatch(targetPath: string, routePath: string): boolean { + if (!routePath.includes(':')) { + return false + } + + const pattern = routePath + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/:([^/]+)/g, '[^/]+') + .replace(/\\\*/g, '.*') + + return new RegExp(`^${pattern}$`).test(targetPath) + } + + /** + * 验证并返回有效的路径 + * 如果目标路径无权限,返回首页路径 + * @param targetPath 目标路径 + * @param menuList 菜单列表 + * @param homePath 首页路径 + * @returns 验证后的路径 + */ + static validatePath( + targetPath: string, + menuList: AppRouteRecord[], + homePath: string = '/' + ): { path: string; hasPermission: boolean } { + const hasPermission = this.hasPermission(targetPath, menuList) + + if (hasPermission) { + return { path: targetPath, hasPermission: true } + } + + return { path: homePath, hasPermission: false } + } +} diff --git a/admin/src/router/core/RouteRegistry.ts b/admin/src/router/core/RouteRegistry.ts new file mode 100644 index 0000000..e1acb9e --- /dev/null +++ b/admin/src/router/core/RouteRegistry.ts @@ -0,0 +1,90 @@ +/** + * 路由注册核心类 + * + * 负责动态路由的注册、验证和管理 + * + * @module router/core/RouteRegistry + * @author Art Design Pro Team + */ + +import type { Router, RouteRecordRaw } from 'vue-router' +import type { AppRouteRecord } from '@/types/router' +import { ComponentLoader } from './ComponentLoader' +import { RouteValidator } from './RouteValidator' +import { RouteTransformer } from './RouteTransformer' + +export class RouteRegistry { + private router: Router + private componentLoader: ComponentLoader + private validator: RouteValidator + private transformer: RouteTransformer + private removeRouteFns: (() => void)[] = [] + private registered = false + + constructor(router: Router) { + this.router = router + this.componentLoader = new ComponentLoader() + this.validator = new RouteValidator() + this.transformer = new RouteTransformer(this.componentLoader) + } + + /** + * 注册动态路由 + */ + register(menuList: AppRouteRecord[]): void { + if (this.registered) { + console.warn('[RouteRegistry] 路由已注册,跳过重复注册') + return + } + + // 验证路由配置 + const validationResult = this.validator.validate(menuList) + if (!validationResult.valid) { + throw new Error(`路由配置验证失败: ${validationResult.errors.join(', ')}`) + } + + // 转换并注册路由 + const removeRouteFns: (() => void)[] = [] + + menuList.forEach((route) => { + if (route.name && !this.router.hasRoute(route.name)) { + const routeConfig = this.transformer.transform(route) + const removeRouteFn = this.router.addRoute(routeConfig as RouteRecordRaw) + removeRouteFns.push(removeRouteFn) + } + }) + + this.removeRouteFns = removeRouteFns + this.registered = true + } + + /** + * 移除所有动态路由 + */ + unregister(): void { + this.removeRouteFns.forEach((fn) => fn()) + this.removeRouteFns = [] + this.registered = false + } + + /** + * 检查是否已注册 + */ + isRegistered(): boolean { + return this.registered + } + + /** + * 获取移除函数列表(用于 store 管理) + */ + getRemoveRouteFns(): (() => void)[] { + return this.removeRouteFns + } + + /** + * 标记为已注册(用于错误处理场景,避免重复请求) + */ + markAsRegistered(): void { + this.registered = true + } +} diff --git a/admin/src/router/core/RouteTransformer.ts b/admin/src/router/core/RouteTransformer.ts new file mode 100644 index 0000000..0f6900c --- /dev/null +++ b/admin/src/router/core/RouteTransformer.ts @@ -0,0 +1,132 @@ +/** + * 路由转换器 + * + * 负责将菜单数据转换为 Vue Router 路由配置 + * + * @module router/core/RouteTransformer + * @author Art Design Pro Team + */ + +import type { RouteRecordRaw } from 'vue-router' +import type { AppRouteRecord } from '@/types/router' +import { ComponentLoader } from './ComponentLoader' +import { IframeRouteManager } from './IframeRouteManager' + +interface ConvertedRoute extends Omit { + id?: number + children?: ConvertedRoute[] + component?: RouteRecordRaw['component'] | (() => Promise) +} + +export class RouteTransformer { + private componentLoader: ComponentLoader + private iframeManager: IframeRouteManager + + constructor(componentLoader: ComponentLoader) { + this.componentLoader = componentLoader + this.iframeManager = IframeRouteManager.getInstance() + } + + /** + * 转换路由配置 + */ + transform(route: AppRouteRecord, depth = 0): ConvertedRoute { + const { component, children, ...routeConfig } = route + + // 基础路由配置 + const converted: ConvertedRoute = { + ...routeConfig, + component: undefined + } + + // 处理不同类型的路由 + if (route.meta.isIframe) { + this.handleIframeRoute(converted, route, depth) + } else if (this.isFirstLevelRoute(route, depth)) { + this.handleFirstLevelRoute(converted, route, component as string) + } else { + this.handleNormalRoute(converted, component as string) + } + + // 递归处理子路由 + if (children?.length) { + converted.children = children.map((child) => this.transform(child, depth + 1)) + } + + return converted + } + + /** + * 判断是否为一级路由(需要 Layout 包裹) + */ + private isFirstLevelRoute(route: AppRouteRecord, depth: number): boolean { + return depth === 0 && (!route.children || route.children.length === 0) + } + + /** + * 处理 iframe 类型路由 + */ + private handleIframeRoute( + targetRoute: ConvertedRoute, + sourceRoute: AppRouteRecord, + depth: number + ): void { + if (depth === 0) { + // 顶级 iframe:用 Layout 包裹 + targetRoute.component = this.componentLoader.loadLayout() + targetRoute.path = this.extractFirstSegment(sourceRoute.path || '') + targetRoute.name = '' + + targetRoute.children = [ + { + ...sourceRoute, + component: this.componentLoader.loadIframe() + } as ConvertedRoute + ] + } else { + // 非顶级(嵌套)iframe:直接使用 Iframe.vue + targetRoute.component = this.componentLoader.loadIframe() + } + + // 记录 iframe 路由 + this.iframeManager.add(sourceRoute) + } + + /** + * 处理一级菜单路由 + */ + private handleFirstLevelRoute( + converted: ConvertedRoute, + route: AppRouteRecord, + component: string | undefined + ): void { + converted.component = this.componentLoader.loadLayout() + converted.path = this.extractFirstSegment(route.path || '') + converted.name = '' + route.meta.isFirstLevel = true + + converted.children = [ + { + ...route, + component: component ? this.componentLoader.load(component) : undefined + } as ConvertedRoute + ] + } + + /** + * 处理普通路由 + */ + private handleNormalRoute(converted: ConvertedRoute, component: string | undefined): void { + if (component) { + converted.component = this.componentLoader.load(component) + } + } + + /** + * 提取路径的第一段 + */ + private extractFirstSegment(path: string): string { + const segments = path.split('/').filter(Boolean) + return segments.length > 0 ? `/${segments[0]}` : '/' + } +} diff --git a/admin/src/router/core/RouteValidator.ts b/admin/src/router/core/RouteValidator.ts new file mode 100644 index 0000000..f8e58fc --- /dev/null +++ b/admin/src/router/core/RouteValidator.ts @@ -0,0 +1,187 @@ +/** + * 路由验证器 + * + * 负责验证路由配置的合法性 + * + * @module router/core/RouteValidator + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' +import { RoutesAlias } from '../routesAlias' + +export interface ValidationResult { + valid: boolean + errors: string[] + warnings: string[] +} + +export class RouteValidator { + // 用于记录已经提示过的路由,避免重复提示 + private warnedRoutes = new Set() + + /** + * 验证路由配置 + */ + validate(routes: AppRouteRecord[]): ValidationResult { + const errors: string[] = [] + const warnings: string[] = [] + + // 检测重复路由 + this.checkDuplicates(routes, errors, warnings) + + // 检测组件配置 + this.checkComponents(routes, errors, warnings) + + // 检测嵌套菜单的 /index/index 配置 + this.checkNestedIndexComponent(routes) + + return { + valid: errors.length === 0, + errors, + warnings + } + } + + /** + * 检测重复路由 + */ + private checkDuplicates( + routes: AppRouteRecord[], + errors: string[], + warnings: string[], + parentPath = '' + ): void { + const routeNameMap = new Map() + const componentPathMap = new Map() + + const checkRoutes = (routes: AppRouteRecord[], parentPath = '') => { + routes.forEach((route) => { + const currentPath = route.path || '' + const fullPath = this.resolvePath(parentPath, currentPath) + + // 名称重复检测 + if (route.name) { + const routeName = String(route.name) + if (routeNameMap.has(routeName)) { + warnings.push(`路由名称重复: "${routeName}" (${fullPath})`) + } else { + routeNameMap.set(routeName, fullPath) + } + } + + // 组件路径重复检测 + if (route.component && typeof route.component === 'string') { + const componentPath = route.component + if (componentPath !== RoutesAlias.Layout) { + const componentKey = `${parentPath}:${componentPath}` + if (componentPathMap.has(componentKey)) { + warnings.push(`组件路径重复: "${componentPath}" (${fullPath})`) + } else { + componentPathMap.set(componentKey, fullPath) + } + } + } + + // 递归处理子路由 + if (route.children?.length) { + checkRoutes(route.children, fullPath) + } + }) + } + + checkRoutes(routes, parentPath) + } + + /** + * 检测组件配置 + */ + private checkComponents( + routes: AppRouteRecord[], + errors: string[], + warnings: string[], + parentPath = '' + ): void { + routes.forEach((route) => { + const hasExternalLink = !!route.meta?.link?.trim() + const hasChildren = Array.isArray(route.children) && route.children.length > 0 + const routePath = route.path || '[未定义路径]' + const isIframe = route.meta?.isIframe + + // 如果配置了 component,则无需校验 + if (route.component) { + // 递归检查子路由 + if (route.children?.length) { + const fullPath = this.resolvePath(parentPath, route.path || '') + this.checkComponents(route.children, errors, warnings, fullPath) + } + return + } + + // 一级菜单:必须指定 Layout,除非是外链或 iframe + if (parentPath === '' && !hasExternalLink && !isIframe) { + errors.push(`一级菜单(${routePath}) 缺少 component,必须指向 ${RoutesAlias.Layout}`) + return + } + + // 非一级菜单:如果既不是外链、iframe,也没有子路由,则必须配置 component + if (!hasExternalLink && !isIframe && !hasChildren) { + errors.push(`路由(${routePath}) 缺少 component 配置`) + } + + // 递归检查子路由 + if (route.children?.length) { + const fullPath = this.resolvePath(parentPath, route.path || '') + this.checkComponents(route.children, errors, warnings, fullPath) + } + }) + } + + /** + * 检测嵌套菜单的 Layout 组件配置 + * 只有一级菜单才能使用 Layout,二级及以下菜单不能使用 + */ + private checkNestedIndexComponent(routes: AppRouteRecord[], level = 1): void { + routes.forEach((route) => { + // 检查二级及以下菜单是否错误使用了 Layout + if (level > 1 && route.component === RoutesAlias.Layout) { + this.logLayoutError(route, level) + } + + // 递归检查子路由 + if (route.children?.length) { + this.checkNestedIndexComponent(route.children, level + 1) + } + }) + } + + /** + * 输出 Layout 组件配置错误日志 + */ + private logLayoutError(route: AppRouteRecord, level: number): void { + const routeName = String(route.name || route.path || '未知路由') + const routeKey = `${routeName}_${route.path}` + + // 避免重复提示 + if (this.warnedRoutes.has(routeKey)) return + this.warnedRoutes.add(routeKey) + + const menuTitle = route.meta?.title || routeName + const routePath = route.path || '/' + + console.error( + `[路由配置错误] 菜单 "${menuTitle}" (name: ${routeName}, path: ${routePath}) 配置错误\n` + + ` 问题: ${level}级菜单不能使用 ${RoutesAlias.Layout} 作为 component\n` + + ` 说明: 只有一级菜单才能使用 ${RoutesAlias.Layout},二级及以下菜单应该指向具体的组件路径\n` + + ` 当前配置: component: '${RoutesAlias.Layout}'\n` + + ` 应该改为: component: '/your/component/path' 或留空 ''(如果是目录菜单)` + ) + } + + /** + * 路径解析 + */ + private resolvePath(parent: string, child: string): string { + return [parent.replace(/\/$/, ''), child.replace(/^\//, '')].filter(Boolean).join('/') + } +} diff --git a/admin/src/router/core/index.ts b/admin/src/router/core/index.ts new file mode 100644 index 0000000..fcfecfc --- /dev/null +++ b/admin/src/router/core/index.ts @@ -0,0 +1,14 @@ +/** + * 路由核心模块导出 + * + * @module router/core + * @author Art Design Pro Team + */ + +export { RouteRegistry } from './RouteRegistry' +export { ComponentLoader } from './ComponentLoader' +export { RouteValidator } from './RouteValidator' +export { RouteTransformer } from './RouteTransformer' +export { IframeRouteManager } from './IframeRouteManager' +export { MenuProcessor } from './MenuProcessor' +export { RoutePermissionValidator } from './RoutePermissionValidator' diff --git a/admin/src/router/guards/afterEach.ts b/admin/src/router/guards/afterEach.ts new file mode 100644 index 0000000..d60572d --- /dev/null +++ b/admin/src/router/guards/afterEach.ts @@ -0,0 +1,34 @@ +import { nextTick } from 'vue' +import { useSettingStore } from '@/store/modules/setting' +import { Router } from 'vue-router' +import NProgress from 'nprogress' +import { useCommon } from '@/hooks/core/useCommon' +import { loadingService } from '@/utils/ui' +import { getPendingLoading, resetPendingLoading } from './beforeEach' + +/** 路由全局后置守卫 */ +export function setupAfterEachGuard(router: Router) { + const { scrollToTop } = useCommon() + + router.afterEach(() => { + scrollToTop() + + // 关闭进度条 + const settingStore = useSettingStore() + if (settingStore.showNprogress) { + NProgress.done() + // 确保进度条完全移除,避免残影 + setTimeout(() => { + NProgress.remove() + }, 600) + } + + // 关闭 loading 效果 + if (getPendingLoading()) { + nextTick(() => { + loadingService.hideLoading() + resetPendingLoading() + }) + } + }) +} diff --git a/admin/src/router/guards/beforeEach.ts b/admin/src/router/guards/beforeEach.ts new file mode 100644 index 0000000..bc97e09 --- /dev/null +++ b/admin/src/router/guards/beforeEach.ts @@ -0,0 +1,418 @@ +/** + * 路由全局前置守卫模块 + * + * 提供完整的路由导航守卫功能 + * + * ## 主要功能 + * + * - 登录状态验证和重定向 + * - 动态路由注册和权限控制 + * - 菜单数据获取和处理(前端/后端模式) + * - 用户信息获取和缓存 + * - 页面标题设置 + * - 工作标签页管理 + * - 进度条和加载动画控制 + * - 静态路由识别和处理 + * - 错误处理和异常跳转 + * + * ## 使用场景 + * + * - 路由跳转前的权限验证 + * - 动态菜单加载和路由注册 + * - 用户登录状态管理 + * - 页面访问控制 + * - 路由级别的加载状态管理 + * + * ## 工作流程 + * + * 1. 检查登录状态,未登录跳转到登录页 + * 2. 首次访问时获取用户信息和菜单数据 + * 3. 根据权限动态注册路由 + * 4. 设置页面标题和工作标签页 + * 5. 处理根路径重定向到首页 + * 6. 未匹配路由跳转到 404 页面 + * + * @module router/guards/beforeEach + * @author Art Design Pro Team + */ +import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router' +import { nextTick } from 'vue' +import NProgress from 'nprogress' +import { useSettingStore } from '@/store/modules/setting' +import { useUserStore } from '@/store/modules/user' +import { useMenuStore } from '@/store/modules/menu' +import { setWorktab } from '@/utils/navigation' +import { setPageTitle } from '@/utils/router' +import { RoutesAlias } from '../routesAlias' +import { staticRoutes } from '../routes/staticRoutes' +import { loadingService } from '@/utils/ui' +import { useCommon } from '@/hooks/core/useCommon' +import { useWorktabStore } from '@/store/modules/worktab' +import { fetchGetUserInfo } from '@/api/auth' +import { ApiStatus } from '@/utils/http/status' +import { isHttpError } from '@/utils/http/error' +import { RouteRegistry, MenuProcessor, IframeRouteManager, RoutePermissionValidator } from '../core' + +// 路由注册器实例 +let routeRegistry: RouteRegistry | null = null + +// 菜单处理器实例 +const menuProcessor = new MenuProcessor() + +// 跟踪是否需要关闭 loading +let pendingLoading = false + +// 路由初始化失败标记,防止死循环 +// 一旦设置为 true,只有刷新页面或重新登录才能重置 +let routeInitFailed = false + +// 路由初始化进行中标记,防止并发请求 +let routeInitInProgress = false + +/** + * 获取 pendingLoading 状态 + */ +export function getPendingLoading(): boolean { + return pendingLoading +} + +/** + * 重置 pendingLoading 状态 + */ +export function resetPendingLoading(): void { + pendingLoading = false +} + +/** + * 获取路由初始化失败状态 + */ +export function getRouteInitFailed(): boolean { + return routeInitFailed +} + +/** + * 重置路由初始化状态(用于重新登录场景) + */ +export function resetRouteInitState(): void { + routeInitFailed = false + routeInitInProgress = false +} + +/** + * 设置路由全局前置守卫 + */ +export function setupBeforeEachGuard(router: Router): void { + // 初始化路由注册器 + routeRegistry = new RouteRegistry(router) + + router.beforeEach( + async ( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext + ) => { + try { + await handleRouteGuard(to, from, next, router) + } catch (error) { + console.error('[RouteGuard] 路由守卫处理失败:', error) + closeLoading() + next({ name: 'Exception500' }) + } + } + ) +} + +/** + * 关闭 loading 效果 + */ +function closeLoading(): void { + if (pendingLoading) { + nextTick(() => { + loadingService.hideLoading() + pendingLoading = false + }) + } +} + +/** + * 处理路由守卫逻辑 + */ +async function handleRouteGuard( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext, + router: Router +): Promise { + const settingStore = useSettingStore() + const userStore = useUserStore() + + // 启动进度条 + if (settingStore.showNprogress) { + NProgress.start() + } + + // 1. 检查登录状态 + if (!handleLoginStatus(to, userStore, next)) { + return + } + + // 2. 检查路由初始化是否已失败(防止死循环) + if (routeInitFailed) { + // 已经失败过,直接放行到错误页面,不再重试 + if (to.matched.length > 0) { + next() + } else { + // 未匹配到路由,跳转到 500 页面 + next({ name: 'Exception500', replace: true }) + } + return + } + + // 3. 处理动态路由注册 + if (!routeRegistry?.isRegistered() && userStore.isLogin) { + // 防止并发请求(快速连续导航场景) + if (routeInitInProgress) { + // 正在初始化中,等待完成后重新导航 + next(false) + return + } + await handleDynamicRoutes(to, next, router) + return + } + + // 4. 处理根路径重定向 + if (handleRootPathRedirect(to, next)) { + return + } + + // 5. 处理已匹配的路由 + if (to.matched.length > 0) { + setWorktab(to) + setPageTitle(to) + next() + return + } + + // 6. 未匹配到路由,跳转到 404 + next({ name: 'Exception404' }) +} + +/** + * 处理登录状态 + * @returns true 表示可以继续,false 表示已处理跳转 + */ +function handleLoginStatus( + to: RouteLocationNormalized, + userStore: ReturnType, + next: NavigationGuardNext +): boolean { + // 已登录或访问登录页或静态路由,直接放行 + if (userStore.isLogin || to.path === RoutesAlias.Login || isStaticRoute(to.path)) { + return true + } + + // 未登录且访问需要权限的页面,跳转到登录页并携带 redirect 参数 + userStore.logOut() + next({ + name: 'Login', + query: { redirect: to.fullPath } + }) + return false +} + +/** + * 检查路由是否为静态路由 + */ +function isStaticRoute(path: string): boolean { + const checkRoute = (routes: any[], targetPath: string): boolean => { + return routes.some((route) => { + // 404 catch-all 路由不应视为可匿名访问的静态页, + // 否则未登录时手动输入任意地址会直接落到 404,无法跳转登录页。 + if (route.name === 'Exception404') { + return false + } + + // 处理动态路由参数匹配 + const routePath = route.path + const pattern = routePath.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*') + const regex = new RegExp(`^${pattern}$`) + + if (regex.test(targetPath)) { + return true + } + if (route.children && route.children.length > 0) { + return checkRoute(route.children, targetPath) + } + return false + }) + } + + return checkRoute(staticRoutes, path) +} + +/** + * 处理动态路由注册 + */ +async function handleDynamicRoutes( + to: RouteLocationNormalized, + next: NavigationGuardNext, + router: Router +): Promise { + // 标记初始化进行中 + routeInitInProgress = true + + // 显示 loading + pendingLoading = true + loadingService.showLoading() + + try { + // 1. 获取用户信息 + await fetchUserInfo() + + // 2. 获取菜单数据 + const menuList = await menuProcessor.getMenuList() + + // 3. 验证菜单数据 + if (!menuProcessor.validateMenuList(menuList)) { + throw new Error('获取菜单列表失败,请重新登录') + } + + // 4. 注册动态路由 + routeRegistry?.register(menuList) + + // 5. 保存菜单数据到 store + const menuStore = useMenuStore() + menuStore.setMenuList(menuList) + menuStore.addRemoveRouteFns(routeRegistry?.getRemoveRouteFns() || []) + + // 6. 保存 iframe 路由 + IframeRouteManager.getInstance().save() + + // 7. 验证工作标签页 + useWorktabStore().validateWorktabs(router) + + // 8. 静态路由不依赖菜单权限,初始化后直接恢复目标地址。 + if (isStaticRoute(to.path)) { + routeInitInProgress = false + next({ + path: to.path, + query: to.query, + hash: to.hash, + replace: true + }) + return + } + + // 8. 验证目标路径权限 + const { homePath } = useCommon() + const { path: validatedPath, hasPermission } = RoutePermissionValidator.validatePath( + to.path, + menuList, + homePath.value || '/' + ) + + // 初始化成功,重置进行中标记 + routeInitInProgress = false + + // 9. 重新导航到目标路由 + if (!hasPermission) { + // 无权限访问,跳转到首页 + closeLoading() + + // 输出警告信息 + console.warn(`[RouteGuard] 用户无权限访问路径: ${to.path},已跳转到首页`) + + // 直接跳转到首页 + next({ + path: validatedPath, + replace: true + }) + } else { + // 有权限,正常导航 + next({ + path: to.path, + query: to.query, + hash: to.hash, + replace: true + }) + } + } catch (error) { + console.error('[RouteGuard] 动态路由注册失败:', error) + + // 关闭 loading + closeLoading() + + // 401 错误:axios 拦截器已处理退出登录,取消当前导航 + if (isUnauthorizedError(error)) { + // 重置状态,允许重新登录后再次初始化 + routeInitInProgress = false + next(false) + return + } + + // 标记初始化失败,防止死循环 + routeInitFailed = true + routeInitInProgress = false + + // 输出详细错误信息,便于排查 + if (isHttpError(error)) { + console.error(`[RouteGuard] 错误码: ${error.code}, 消息: ${error.message}`) + } + + // 跳转到 500 页面,使用 replace 避免产生历史记录 + next({ name: 'Exception500', replace: true }) + } +} + +/** + * 获取用户信息 + */ +async function fetchUserInfo(): Promise { + const userStore = useUserStore() + const data = await fetchGetUserInfo() + userStore.setUserInfo(data) + // 检查并清理工作台标签页(如果是不同用户登录) + userStore.checkAndClearWorktabs() +} + +/** + * 重置路由相关状态 + */ +export function resetRouterState(delay: number): void { + setTimeout(() => { + routeRegistry?.unregister() + IframeRouteManager.getInstance().clear() + + const menuStore = useMenuStore() + menuStore.removeAllDynamicRoutes() + menuStore.setMenuList([]) + + // 重置路由初始化状态,允许重新登录后再次初始化 + resetRouteInitState() + }, delay) +} + +/** + * 处理根路径重定向到首页 + * @returns true 表示已处理跳转,false 表示无需跳转 + */ +function handleRootPathRedirect(to: RouteLocationNormalized, next: NavigationGuardNext): boolean { + if (to.path !== '/') { + return false + } + + const { homePath } = useCommon() + if (homePath.value && homePath.value !== '/') { + next({ path: homePath.value, replace: true }) + return true + } + + return false +} + +/** + * 判断是否为未授权错误(401) + */ +function isUnauthorizedError(error: unknown): boolean { + return isHttpError(error) && error.code === ApiStatus.unauthorized +} diff --git a/admin/src/router/index.ts b/admin/src/router/index.ts new file mode 100644 index 0000000..286ae58 --- /dev/null +++ b/admin/src/router/index.ts @@ -0,0 +1,23 @@ +import type { App } from 'vue' +import { createRouter, createWebHashHistory } from 'vue-router' +import { staticRoutes } from './routes/staticRoutes' +import { configureNProgress } from '@/utils/router' +import { setupBeforeEachGuard } from './guards/beforeEach' +import { setupAfterEachGuard } from './guards/afterEach' + +// 创建路由实例 +export const router = createRouter({ + history: createWebHashHistory(), + routes: staticRoutes // 静态路由 +}) + +// 初始化路由 +export function initRouter(app: App): void { + configureNProgress() // 顶部进度条 + setupBeforeEachGuard(router) // 路由前置守卫 + setupAfterEachGuard(router) // 路由后置守卫 + app.use(router) +} + +// 主页路径,默认使用菜单第一个有效路径,配置后使用此路径 +export const HOME_PAGE_PATH = '' diff --git a/admin/src/router/modules/config.ts b/admin/src/router/modules/config.ts new file mode 100644 index 0000000..7be14da --- /dev/null +++ b/admin/src/router/modules/config.ts @@ -0,0 +1,24 @@ +import { AppRouteRecord } from '@/types/router' + +/** 配置中心——超管与租户管理员均可见(租户配置组/站点/品牌) */ +export const configRoutes: AppRouteRecord = { + name: 'Config', + path: '/config', + component: '/index/index', + meta: { + title: 'menus.config.title', + icon: 'ri:settings-4-line' + }, + children: [ + { + path: 'center', + name: 'ConfigCenter', + component: '/config/center', + meta: { + title: 'menus.config.center', + icon: 'ri:equalizer-line', + keepAlive: false + } + } + ] +} diff --git a/admin/src/router/modules/conversations.ts b/admin/src/router/modules/conversations.ts new file mode 100644 index 0000000..ef96bc3 --- /dev/null +++ b/admin/src/router/modules/conversations.ts @@ -0,0 +1,24 @@ +import { AppRouteRecord } from '@/types/router' + +/** 会话与线索——超管与租户管理员均可见 */ +export const conversationsRoutes: AppRouteRecord = { + name: 'Conversations', + path: '/conversations', + component: '/index/index', + meta: { + title: 'menus.conversations.title', + icon: 'ri:chat-3-line' + }, + children: [ + { + path: 'list', + name: 'ConversationList', + component: '/conversations/list', + meta: { + title: 'menus.conversations.list', + icon: 'ri:chat-4-line', + keepAlive: false + } + } + ] +} diff --git a/admin/src/router/modules/dashboard.ts b/admin/src/router/modules/dashboard.ts new file mode 100644 index 0000000..b1d2455 --- /dev/null +++ b/admin/src/router/modules/dashboard.ts @@ -0,0 +1,24 @@ +import { AppRouteRecord } from '@/types/router' + +export const dashboardRoutes: AppRouteRecord = { + name: 'Dashboard', + path: '/dashboard', + component: '/index/index', + meta: { + title: 'menus.dashboard.title', + icon: 'ri:dashboard-line' + }, + children: [ + { + path: 'console', + name: 'Console', + component: '/dashboard/console', + meta: { + title: 'menus.dashboard.console', + icon: 'ri:home-smile-2-line', + keepAlive: false, + fixedTab: true + } + } + ] +} diff --git a/admin/src/router/modules/index.ts b/admin/src/router/modules/index.ts new file mode 100644 index 0000000..c7271fc --- /dev/null +++ b/admin/src/router/modules/index.ts @@ -0,0 +1,20 @@ +import { AppRouteRecord } from '@/types/router' +import { dashboardRoutes } from './dashboard' +import { tenantsRoutes } from './tenants' +import { conversationsRoutes } from './conversations' +import { configRoutes } from './config' +import { statsRoutes } from './stats' +import { settingsRoutes } from './settings' + +/** + * 导出所有模块化路由(前端权限模式:菜单按 meta.roles 过滤, + * 角色值与后端一致:super 全量;tenant 隐藏 租户管理/平台设置) + */ +export const routeModules: AppRouteRecord[] = [ + dashboardRoutes, + tenantsRoutes, + conversationsRoutes, + configRoutes, + statsRoutes, + settingsRoutes +] diff --git a/admin/src/router/modules/settings.ts b/admin/src/router/modules/settings.ts new file mode 100644 index 0000000..cd86376 --- /dev/null +++ b/admin/src/router/modules/settings.ts @@ -0,0 +1,25 @@ +import { AppRouteRecord } from '@/types/router' + +/** 平台设置——仅超管(super)可见,租户管理员整个菜单组隐藏 */ +export const settingsRoutes: AppRouteRecord = { + name: 'Settings', + path: '/settings', + component: '/index/index', + meta: { + title: 'menus.settings.title', + icon: 'ri:shield-keyhole-line', + roles: ['super'] + }, + children: [ + { + path: 'platform', + name: 'PlatformSettings', + component: '/settings/platform', + meta: { + title: 'menus.settings.platform', + icon: 'ri:shield-star-line', + keepAlive: false + } + } + ] +} diff --git a/admin/src/router/modules/stats.ts b/admin/src/router/modules/stats.ts new file mode 100644 index 0000000..63102f8 --- /dev/null +++ b/admin/src/router/modules/stats.ts @@ -0,0 +1,24 @@ +import { AppRouteRecord } from '@/types/router' + +/** 统计分析——超管与租户管理员均可见 */ +export const statsRoutes: AppRouteRecord = { + name: 'Stats', + path: '/stats', + component: '/index/index', + meta: { + title: 'menus.stats.title', + icon: 'ri:bar-chart-2-line' + }, + children: [ + { + path: 'overview', + name: 'StatsOverview', + component: '/stats/overview', + meta: { + title: 'menus.stats.overview', + icon: 'ri:line-chart-line', + keepAlive: false + } + } + ] +} diff --git a/admin/src/router/modules/tenants.ts b/admin/src/router/modules/tenants.ts new file mode 100644 index 0000000..afbe573 --- /dev/null +++ b/admin/src/router/modules/tenants.ts @@ -0,0 +1,25 @@ +import { AppRouteRecord } from '@/types/router' + +/** 租户管理——仅超管(super)可见,租户管理员整个菜单组隐藏 */ +export const tenantsRoutes: AppRouteRecord = { + name: 'Tenants', + path: '/tenants', + component: '/index/index', + meta: { + title: 'menus.tenants.title', + icon: 'ri:building-2-line', + roles: ['super'] + }, + children: [ + { + path: 'list', + name: 'TenantList', + component: '/tenants/list', + meta: { + title: 'menus.tenants.list', + icon: 'ri:building-line', + keepAlive: false + } + } + ] +} diff --git a/admin/src/router/routes/asyncRoutes.ts b/admin/src/router/routes/asyncRoutes.ts new file mode 100644 index 0000000..ccf1201 --- /dev/null +++ b/admin/src/router/routes/asyncRoutes.ts @@ -0,0 +1,9 @@ +// 权限文档:https://www.artd.pro/docs/zh/guide/in-depth/permission.html +import { AppRouteRecord } from '@/types/router' +import { routeModules } from '../modules' + +/** + * 动态路由(需要权限才能访问的路由) + * 用于渲染菜单以及根据菜单权限动态加载路由,如果没有权限无法访问 + */ +export const asyncRoutes: AppRouteRecord[] = routeModules diff --git a/admin/src/router/routes/staticRoutes.ts b/admin/src/router/routes/staticRoutes.ts new file mode 100644 index 0000000..9326e47 --- /dev/null +++ b/admin/src/router/routes/staticRoutes.ts @@ -0,0 +1,66 @@ +import { AppRouteRecordRaw } from '@/utils/router' + +/** + * 静态路由配置(不需要权限就能访问的路由) + * + * 属性说明: + * isHideTab: true 表示不在标签页中显示 + * + * 注意事项: + * 1、path、name 不要和动态路由冲突,否则会导致路由冲突无法访问 + * 2、静态路由不管是否登录都可以访问 + */ +export const staticRoutes: AppRouteRecordRaw[] = [ + // 不需要登录就能访问的路由示例 + // { + // path: '/welcome', + // name: 'WelcomeStatic', + // component: () => import('@views/dashboard/console/index.vue'), + // meta: { title: 'menus.dashboard.title' } + // }, + { + path: '/auth/login', + name: 'Login', + component: () => import('@views/auth/login/index.vue'), + meta: { title: 'menus.login.title', isHideTab: true } + }, + { + path: '/auth/force-change-password', + name: 'ForceChangePassword', + component: () => import('@views/auth/force-change-password/index.vue'), + meta: { title: 'menus.forceChangePassword.title', isHideTab: true } + }, + { + path: '/403', + name: 'Exception403', + component: () => import('@views/exception/403/index.vue'), + meta: { title: '403', isHideTab: true } + }, + { + path: '/:pathMatch(.*)*', + name: 'Exception404', + component: () => import('@views/exception/404/index.vue'), + meta: { title: '404', isHideTab: true } + }, + { + path: '/500', + name: 'Exception500', + component: () => import('@views/exception/500/index.vue'), + meta: { title: '500', isHideTab: true } + }, + { + path: '/outside', + component: () => import('@views/index/index.vue'), + name: 'Outside', + meta: { title: 'menus.outside.title' }, + children: [ + // iframe 内嵌页面 + { + path: '/outside/iframe/:path', + name: 'Iframe', + component: () => import('@/views/outside/Iframe.vue'), + meta: { title: 'iframe' } + } + ] + } +] diff --git a/admin/src/router/routesAlias.ts b/admin/src/router/routesAlias.ts new file mode 100644 index 0000000..e66113b --- /dev/null +++ b/admin/src/router/routesAlias.ts @@ -0,0 +1,9 @@ +/** + * 公共路由别名 + * 存放系统级公共路由路径,如布局容器、登录页等 + */ +export enum RoutesAlias { + Layout = '/index/index', // 布局容器 + Login = '/auth/login', // 登录页 + ForceChangePassword = '/auth/force-change-password' // 强制修改密码页 +} diff --git a/admin/src/store/index.ts b/admin/src/store/index.ts new file mode 100644 index 0000000..b485999 --- /dev/null +++ b/admin/src/store/index.ts @@ -0,0 +1,52 @@ +/** + * Pinia Store 配置模块 + * + * 提供全局状态管理的初始化和配置 + * + * ## 主要功能 + * + * - Pinia Store 实例创建 + * - 持久化插件配置(pinia-plugin-persistedstate) + * - 版本化存储键管理 + * - 自动数据迁移(跨版本) + * - LocalStorage 序列化配置 + * - Store 初始化函数 + * + * ## 持久化策略 + * + * - 使用 StorageKeyManager 生成版本化的存储键 + * - 格式:sys-v{version}-{storeId} + * - 自动迁移旧版本数据到当前版本 + * - 使用 localStorage 作为存储介质 + * + * @module store/index + * @author Art Design Pro Team + */ +import type { App } from 'vue' +import { createPinia } from 'pinia' +import { createPersistedState } from 'pinia-plugin-persistedstate' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' + +export const store = createPinia() + +// 创建存储键管理器实例 +const storageKeyManager = new StorageKeyManager() + +// 配置持久化插件 +store.use( + createPersistedState({ + key: (storeId: string) => storageKeyManager.getStorageKey(storeId), + storage: localStorage, + serializer: { + serialize: JSON.stringify, + deserialize: JSON.parse + } + }) +) + +/** + * 初始化 Store + */ +export function initStore(app: App): void { + app.use(store) +} diff --git a/admin/src/store/modules/menu.ts b/admin/src/store/modules/menu.ts new file mode 100644 index 0000000..85d13da --- /dev/null +++ b/admin/src/store/modules/menu.ts @@ -0,0 +1,109 @@ +/** + * 菜单状态管理模块 + * + * 提供菜单数据和动态路由的状态管理 + * + * ## 主要功能 + * + * - 菜单列表存储和管理 + * - 首页路径配置 + * - 动态路由注册和移除 + * - 路由移除函数管理 + * - 菜单宽度配置 + * + * ## 使用场景 + * + * - 动态菜单加载和渲染 + * - 路由权限控制 + * - 首页路径动态设置 + * - 登出时清理动态路由 + * + * ## 工作流程 + * + * 1. 获取菜单数据(前端/后端模式) + * 2. 设置菜单列表和首页路径 + * 3. 注册动态路由并保存移除函数 + * 4. 登出时调用移除函数清理路由 + * + * @module store/modules/menu + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { AppRouteRecord } from '@/types/router' +import { getFirstMenuPath } from '@/utils' +import { HOME_PAGE_PATH } from '@/router' + +/** + * 菜单状态管理 + * 管理应用的菜单列表、首页路径、菜单宽度和动态路由移除函数 + */ +export const useMenuStore = defineStore('menuStore', () => { + /** 首页路径 */ + const homePath = ref(HOME_PAGE_PATH) + /** 菜单列表 */ + const menuList = ref([]) + /** 菜单宽度 */ + const menuWidth = ref('') + /** 存储路由移除函数的数组 */ + const removeRouteFns = ref<(() => void)[]>([]) + + /** + * 设置菜单列表 + * @param list 菜单路由记录数组 + */ + const setMenuList = (list: AppRouteRecord[]) => { + menuList.value = list + setHomePath(HOME_PAGE_PATH || getFirstMenuPath(list)) + } + + /** + * 获取首页路径 + * @returns 首页路径字符串 + */ + const getHomePath = () => homePath.value + + /** + * 设置主页路径 + * @param path 主页路径 + */ + const setHomePath = (path: string) => { + homePath.value = path + } + + /** + * 添加路由移除函数 + * @param fns 要添加的路由移除函数数组 + */ + const addRemoveRouteFns = (fns: (() => void)[]) => { + removeRouteFns.value.push(...fns) + } + + /** + * 移除所有动态路由 + * 执行所有存储的路由移除函数并清空数组 + */ + const removeAllDynamicRoutes = () => { + removeRouteFns.value.forEach((fn) => fn()) + removeRouteFns.value = [] + } + + /** + * 清空路由移除函数数组 + */ + const clearRemoveRouteFns = () => { + removeRouteFns.value = [] + } + + return { + menuList, + menuWidth, + removeRouteFns, + setMenuList, + getHomePath, + setHomePath, + addRemoveRouteFns, + removeAllDynamicRoutes, + clearRemoveRouteFns + } +}) diff --git a/admin/src/store/modules/setting.ts b/admin/src/store/modules/setting.ts new file mode 100644 index 0000000..2878259 --- /dev/null +++ b/admin/src/store/modules/setting.ts @@ -0,0 +1,450 @@ +/** + * 系统设置状态管理模块 + * + * 提供完整的系统设置状态管理 + * + * ## 主要功能 + * + * - 菜单布局配置(左侧、顶部、混合、双栏) + * - 主题管理(亮色、暗色、自动) + * - 菜单主题样式配置 + * - 界面显示开关(面包屑、标签页、语言切换等) + * - 功能开关(手风琴模式、色弱模式、水印等) + * - 样式配置(边框、圆角、容器宽度、页面过渡) + * - 节日功能配置 + * - Element Plus 主题色动态设置 + * + * ## 使用场景 + * + * - 设置面板配置管理 + * - 主题切换和样式定制 + * - 界面功能开关控制 + * - 用户偏好设置持久化 + * + * ## 持久化 + * + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-setting + * - 支持跨版本数据迁移 + * + * @module store/modules/setting + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { MenuThemeType } from '@/types/store' +import AppConfig from '@/config' +import { SystemThemeEnum, MenuThemeEnum, MenuTypeEnum, ContainerWidthEnum } from '@/enums/appEnum' +import { setElementThemeColor } from '@/utils/ui' +import { useCeremony } from '@/hooks/core/useCeremony' +import { StorageConfig } from '@/utils' +import { SETTING_DEFAULT_CONFIG } from '@/config/setting' + +/** + * 系统设置状态管理 + * 管理应用的菜单、主题、界面显示等各项设置 + */ +export const useSettingStore = defineStore( + 'settingStore', + () => { + // 菜单相关设置 + /** 菜单类型 */ + const menuType = ref(SETTING_DEFAULT_CONFIG.menuType) + /** 菜单展开宽度 */ + const menuOpenWidth = ref(SETTING_DEFAULT_CONFIG.menuOpenWidth) + /** 菜单是否展开 */ + const menuOpen = ref(SETTING_DEFAULT_CONFIG.menuOpen) + /** 双菜单是否显示文本 */ + const dualMenuShowText = ref(SETTING_DEFAULT_CONFIG.dualMenuShowText) + + // 主题相关设置 + /** 系统主题类型 */ + const systemThemeType = ref(SETTING_DEFAULT_CONFIG.systemThemeType) + /** 系统主题模式 */ + const systemThemeMode = ref(SETTING_DEFAULT_CONFIG.systemThemeMode) + /** 菜单主题类型 */ + const menuThemeType = ref(SETTING_DEFAULT_CONFIG.menuThemeType) + /** 系统主题颜色 */ + const systemThemeColor = ref(SETTING_DEFAULT_CONFIG.systemThemeColor) + + // 界面显示设置 + /** 是否显示菜单按钮 */ + const showMenuButton = ref(SETTING_DEFAULT_CONFIG.showMenuButton) + /** 是否显示快速入口 */ + const showFastEnter = ref(SETTING_DEFAULT_CONFIG.showFastEnter) + /** 是否显示刷新按钮 */ + const showRefreshButton = ref(SETTING_DEFAULT_CONFIG.showRefreshButton) + /** 是否显示面包屑 */ + const showCrumbs = ref(SETTING_DEFAULT_CONFIG.showCrumbs) + /** 是否显示工作台标签 */ + const showWorkTab = ref(SETTING_DEFAULT_CONFIG.showWorkTab) + /** 是否显示语言切换 */ + const showLanguage = ref(SETTING_DEFAULT_CONFIG.showLanguage) + /** 是否显示进度条 */ + const showNprogress = ref(SETTING_DEFAULT_CONFIG.showNprogress) + /** 是否显示设置引导 */ + const showSettingGuide = ref(SETTING_DEFAULT_CONFIG.showSettingGuide) + /** 是否显示节日文本 */ + const showFestivalText = ref(SETTING_DEFAULT_CONFIG.showFestivalText) + /** 是否显示水印 */ + const watermarkVisible = ref(SETTING_DEFAULT_CONFIG.watermarkVisible) + + // 功能设置 + /** 是否自动关闭 */ + const autoClose = ref(SETTING_DEFAULT_CONFIG.autoClose) + /** 是否唯一展开 */ + const uniqueOpened = ref(SETTING_DEFAULT_CONFIG.uniqueOpened) + /** 是否色弱模式 */ + const colorWeak = ref(SETTING_DEFAULT_CONFIG.colorWeak) + /** 是否刷新 */ + const refresh = ref(SETTING_DEFAULT_CONFIG.refresh) + /** 是否加载节日烟花 */ + const holidayFireworksLoaded = ref(SETTING_DEFAULT_CONFIG.holidayFireworksLoaded) + + // 样式设置 + /** 边框模式 */ + const boxBorderMode = ref(SETTING_DEFAULT_CONFIG.boxBorderMode) + /** 页面过渡效果 */ + const pageTransition = ref(SETTING_DEFAULT_CONFIG.pageTransition) + /** 标签页样式 */ + const tabStyle = ref(SETTING_DEFAULT_CONFIG.tabStyle) + /** 自定义圆角 */ + const customRadius = ref(SETTING_DEFAULT_CONFIG.customRadius) + /** 容器宽度 */ + const containerWidth = ref(SETTING_DEFAULT_CONFIG.containerWidth) + + // 节日相关 + /** 节日日期 */ + const festivalDate = ref('') + + /** + * 获取菜单主题 + * 根据当前主题类型和暗色模式返回对应的主题配置 + */ + const getMenuTheme = computed((): MenuThemeType => { + const list = AppConfig.themeList.filter((item) => item.theme === menuThemeType.value) + if (isDark.value) { + return AppConfig.darkMenuStyles[0] + } else { + return list[0] + } + }) + + /** + * 判断是否为暗色模式 + */ + const isDark = computed((): boolean => { + return systemThemeType.value === SystemThemeEnum.DARK + }) + + /** + * 获取菜单展开宽度 + */ + const getMenuOpenWidth = computed((): string => { + return menuOpenWidth.value + 'px' || SETTING_DEFAULT_CONFIG.menuOpenWidth + 'px' + }) + + /** + * 获取自定义圆角 + */ + const getCustomRadius = computed((): string => { + return customRadius.value + 'rem' || SETTING_DEFAULT_CONFIG.customRadius + 'rem' + }) + + /** + * 是否显示烟花 + * 根据当前日期和节日日期判断是否显示烟花效果 + */ + const isShowFireworks = computed((): boolean => { + return festivalDate.value === useCeremony().currentFestivalData.value?.date ? false : true + }) + + /** + * 切换菜单布局 + * @param type 菜单类型 + */ + const switchMenuLayouts = (type: MenuTypeEnum) => { + menuType.value = type + } + + /** + * 设置菜单展开宽度 + * @param width 宽度值 + */ + const setMenuOpenWidth = (width: number) => { + menuOpenWidth.value = width + } + + /** + * 设置全局主题 + * @param theme 主题类型 + * @param themeMode 主题模式 + */ + const setGlopTheme = (theme: SystemThemeEnum, themeMode: SystemThemeEnum) => { + systemThemeType.value = theme + systemThemeMode.value = themeMode + localStorage.setItem(StorageConfig.THEME_KEY, theme) + } + + /** + * 切换菜单样式 + * @param theme 菜单主题 + */ + const switchMenuStyles = (theme: MenuThemeEnum) => { + menuThemeType.value = theme + } + + /** + * 设置Element Plus主题颜色 + * @param theme 主题颜色 + */ + const setElementTheme = (theme: string) => { + systemThemeColor.value = theme + setElementThemeColor(theme) + } + + /** + * 切换边框模式 + */ + const setBorderMode = () => { + boxBorderMode.value = !boxBorderMode.value + } + + /** + * 设置容器宽度 + * @param width 容器宽度枚举值 + */ + const setContainerWidth = (width: ContainerWidthEnum) => { + containerWidth.value = width + } + + /** + * 切换唯一展开模式 + */ + const setUniqueOpened = () => { + uniqueOpened.value = !uniqueOpened.value + } + + /** + * 切换菜单按钮显示 + */ + const setButton = () => { + showMenuButton.value = !showMenuButton.value + } + + /** + * 切换快速入口显示 + */ + const setFastEnter = () => { + showFastEnter.value = !showFastEnter.value + } + + /** + * 切换自动关闭 + */ + const setAutoClose = () => { + autoClose.value = !autoClose.value + } + + /** + * 切换刷新按钮显示 + */ + const setShowRefreshButton = () => { + showRefreshButton.value = !showRefreshButton.value + } + + /** + * 切换面包屑显示 + */ + const setCrumbs = () => { + showCrumbs.value = !showCrumbs.value + } + + /** + * 设置工作台标签显示 + * @param show 是否显示 + */ + const setWorkTab = (show: boolean) => { + showWorkTab.value = show + } + + /** + * 切换语言切换显示 + */ + const setLanguage = () => { + showLanguage.value = !showLanguage.value + } + + /** + * 切换进度条显示 + */ + const setNprogress = () => { + showNprogress.value = !showNprogress.value + } + + /** + * 切换色弱模式 + */ + const setColorWeak = () => { + colorWeak.value = !colorWeak.value + } + + /** + * 隐藏设置引导 + */ + const hideSettingGuide = () => { + showSettingGuide.value = false + } + + /** + * 显示设置引导 + */ + const openSettingGuide = () => { + showSettingGuide.value = true + } + + /** + * 设置页面过渡效果 + * @param transition 过渡效果名称 + */ + const setPageTransition = (transition: string) => { + pageTransition.value = transition + } + + /** + * 设置标签页样式 + * @param style 样式名称 + */ + const setTabStyle = (style: string) => { + tabStyle.value = style + } + + /** + * 设置菜单展开状态 + * @param open 是否展开 + */ + const setMenuOpen = (open: boolean) => { + menuOpen.value = open + } + + /** + * 刷新页面 + */ + const reload = () => { + refresh.value = !refresh.value + } + + /** + * 设置水印显示 + * @param visible 是否显示 + */ + const setWatermarkVisible = (visible: boolean) => { + watermarkVisible.value = visible + } + + /** + * 设置自定义圆角 + * @param radius 圆角值 + */ + const setCustomRadius = (radius: string) => { + customRadius.value = radius + document.documentElement.style.setProperty('--custom-radius', `${radius}rem`) + } + + /** + * 设置节日烟花加载状态 + * @param isLoad 是否已加载 + */ + const setholidayFireworksLoaded = (isLoad: boolean) => { + holidayFireworksLoaded.value = isLoad + } + + /** + * 设置节日文本显示 + * @param show 是否显示 + */ + const setShowFestivalText = (show: boolean) => { + showFestivalText.value = show + } + + const setFestivalDate = (date: string) => { + festivalDate.value = date + } + + const setDualMenuShowText = (show: boolean) => { + dualMenuShowText.value = show + } + + return { + menuType, + menuOpenWidth, + systemThemeType, + systemThemeMode, + menuThemeType, + systemThemeColor, + boxBorderMode, + uniqueOpened, + showMenuButton, + showFastEnter, + showRefreshButton, + showCrumbs, + autoClose, + showWorkTab, + showLanguage, + showNprogress, + colorWeak, + showSettingGuide, + pageTransition, + tabStyle, + menuOpen, + refresh, + watermarkVisible, + customRadius, + holidayFireworksLoaded, + showFestivalText, + festivalDate, + dualMenuShowText, + containerWidth, + getMenuTheme, + isDark, + getMenuOpenWidth, + getCustomRadius, + isShowFireworks, + switchMenuLayouts, + setMenuOpenWidth, + setGlopTheme, + switchMenuStyles, + setElementTheme, + setBorderMode, + setContainerWidth, + setUniqueOpened, + setButton, + setFastEnter, + setAutoClose, + setShowRefreshButton, + setCrumbs, + setWorkTab, + setLanguage, + setNprogress, + setColorWeak, + hideSettingGuide, + openSettingGuide, + setPageTransition, + setTabStyle, + setMenuOpen, + reload, + setWatermarkVisible, + setCustomRadius, + setholidayFireworksLoaded, + setShowFestivalText, + setFestivalDate, + setDualMenuShowText + } + }, + { + persist: { + key: 'setting', + storage: localStorage + } + } +) diff --git a/admin/src/store/modules/table.ts b/admin/src/store/modules/table.ts new file mode 100644 index 0000000..094c310 --- /dev/null +++ b/admin/src/store/modules/table.ts @@ -0,0 +1,97 @@ +/** + * 表格状态管理模块 + * + * 提供表格显示配置的状态管理 + * + * ## 主要功能 + * + * - 表格尺寸配置(紧凑、默认、宽松) + * - 斑马纹显示开关 + * - 边框显示开关 + * - 表头背景显示开关 + * - 全屏模式开关 + * + * ## 使用场景 + * - 表格组件样式配置 + * - 用户表格偏好设置 + * - 表格工具栏功能控制 + * + * ## 持久化 + * + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-table + * - 用户配置跨页面保持 + * + * @module store/modules/table + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { TableSizeEnum } from '@/enums/formEnum' + +// 表格 +export const useTableStore = defineStore( + 'tableStore', + () => { + // 表格大小 + const tableSize = ref(TableSizeEnum.DEFAULT) + // 斑马纹 + const isZebra = ref(false) + // 边框 + const isBorder = ref(false) + // 表头背景 + const isHeaderBackground = ref(false) + + // 是否全屏 + const isFullScreen = ref(false) + + /** + * 设置表格大小 + * @param size 表格大小枚举值 + */ + const setTableSize = (size: TableSizeEnum) => (tableSize.value = size) + + /** + * 设置斑马纹显示状态 + * @param value 是否显示斑马纹 + */ + const setIsZebra = (value: boolean) => (isZebra.value = value) + + /** + * 设置表格边框显示状态 + * @param value 是否显示边框 + */ + const setIsBorder = (value: boolean) => (isBorder.value = value) + + /** + * 设置表头背景显示状态 + * @param value 是否显示表头背景 + */ + const setIsHeaderBackground = (value: boolean) => (isHeaderBackground.value = value) + + /** + * 设置是否全屏 + * @param value 是否全屏 + */ + const setIsFullScreen = (value: boolean) => (isFullScreen.value = value) + + return { + tableSize, + isZebra, + isBorder, + isHeaderBackground, + setTableSize, + setIsZebra, + setIsBorder, + setIsHeaderBackground, + isFullScreen, + setIsFullScreen + } + }, + { + persist: { + key: 'table', + storage: localStorage + } + } +) diff --git a/admin/src/store/modules/user.ts b/admin/src/store/modules/user.ts new file mode 100644 index 0000000..08f7684 --- /dev/null +++ b/admin/src/store/modules/user.ts @@ -0,0 +1,235 @@ +/** + * 用户状态管理模块 + * + * 提供用户相关的状态管理 + * + * ## 主要功能 + * + * - 用户登录状态管理 + * - 用户信息存储 + * - 访问令牌和刷新令牌管理 + * - 语言设置 + * - 搜索历史记录 + * - 锁屏状态和密码管理 + * - 登出清理逻辑 + * + * ## 使用场景 + * + * - 用户登录和认证 + * - 权限验证 + * - 个人信息展示 + * - 多语言切换 + * - 锁屏功能 + * - 搜索历史管理 + * + * ## 持久化 + * + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-user + * - 登出时自动清理 + * + * @module store/modules/user + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { LanguageEnum } from '@/enums/appEnum' +import { router } from '@/router' +import { useSettingStore } from './setting' +import { useWorktabStore } from './worktab' +import { AppRouteRecord } from '@/types/router' +import { setPageTitle } from '@/utils/router' +import { resetRouterState } from '@/router/guards/beforeEach' +import { useMenuStore } from './menu' +import { StorageConfig } from '@/utils/storage/storage-config' + +/** + * 用户状态管理 + * 管理用户登录状态、个人信息、语言设置、搜索历史、锁屏状态等 + */ +export const useUserStore = defineStore( + 'userStore', + () => { + // 语言设置 + const language = ref(LanguageEnum.ZH) + // 登录状态 + const isLogin = ref(false) + // 锁屏状态 + const isLock = ref(false) + // 锁屏密码 + const lockPassword = ref('') + // 用户信息 + const info = ref>({}) + // 搜索历史记录 + const searchHistory = ref([]) + // 访问令牌 + const accessToken = ref('') + // 刷新令牌 + const refreshToken = ref('') + + // 计算属性:获取用户信息 + const getUserInfo = computed(() => info.value) + // 计算属性:获取设置状态 + const getSettingState = computed(() => useSettingStore().$state) + // 计算属性:获取工作台状态 + const getWorktabState = computed(() => useWorktabStore().$state) + + /** + * 设置用户信息 + * @param newInfo 新的用户信息 + */ + const setUserInfo = (newInfo: Api.Auth.UserInfo) => { + info.value = newInfo + } + + /** + * 设置登录状态 + * @param status 登录状态 + */ + const setLoginStatus = (status: boolean) => { + isLogin.value = status + } + + /** + * 设置语言 + * @param lang 语言枚举值 + */ + const setLanguage = (lang: LanguageEnum) => { + setPageTitle(router.currentRoute.value) + language.value = lang + } + + /** + * 设置搜索历史 + * @param list 搜索历史列表 + */ + const setSearchHistory = (list: AppRouteRecord[]) => { + searchHistory.value = list + } + + /** + * 设置锁屏状态 + * @param status 锁屏状态 + */ + const setLockStatus = (status: boolean) => { + isLock.value = status + } + + /** + * 设置锁屏密码 + * @param password 锁屏密码 + */ + const setLockPassword = (password: string) => { + lockPassword.value = password + } + + /** + * 设置令牌 + * @param newAccessToken 访问令牌 + * @param newRefreshToken 刷新令牌(可选) + */ + const setToken = (newAccessToken: string, newRefreshToken?: string) => { + accessToken.value = newAccessToken + if (newRefreshToken) { + refreshToken.value = newRefreshToken + } + } + + /** + * 退出登录 + * 清空所有用户相关状态并跳转到登录页 + * 如果是同一账号重新登录,保留工作台标签页 + */ + const logOut = () => { + // 保存当前用户 ID,用于下次登录时判断是否为同一用户 + const currentUserId = info.value.userId + if (currentUserId) { + localStorage.setItem(StorageConfig.LAST_USER_ID_KEY, String(currentUserId)) + } + + // 清空用户信息 + info.value = {} + // 重置登录状态 + isLogin.value = false + // 重置锁屏状态 + isLock.value = false + // 清空锁屏密码 + lockPassword.value = '' + // 清空访问令牌 + accessToken.value = '' + // 清空刷新令牌 + refreshToken.value = '' + // 注意:不清空工作台标签页,等下次登录时根据用户判断 + // 移除iframe路由缓存 + sessionStorage.removeItem('iframeRoutes') + // 清空主页路径 + useMenuStore().setHomePath('') + // 重置路由状态 + resetRouterState(500) + // 跳转到登录页,携带当前路由作为 redirect 参数 + const currentRoute = router.currentRoute.value + const redirect = currentRoute.path !== '/login' ? currentRoute.fullPath : undefined + router.push({ + name: 'Login', + query: redirect ? { redirect } : undefined + }) + } + + /** + * 检查并清理工作台标签页 + * 如果不是同一用户登录,清空工作台标签页 + * 应在登录成功后调用 + */ + const checkAndClearWorktabs = () => { + const lastUserId = localStorage.getItem(StorageConfig.LAST_USER_ID_KEY) + const currentUserId = info.value.userId + + // 无法获取当前用户 ID,跳过检查 + if (!currentUserId) return + + // 首次登录或缓存已清除,保留现有标签页 + if (!lastUserId) { + return + } + + // 不同用户登录,清空工作台标签页 + if (String(currentUserId) !== lastUserId) { + const worktabStore = useWorktabStore() + worktabStore.opened = [] + worktabStore.keepAliveExclude = [] + } + + // 清除临时存储 + localStorage.removeItem(StorageConfig.LAST_USER_ID_KEY) + } + + return { + language, + isLogin, + isLock, + lockPassword, + info, + searchHistory, + accessToken, + refreshToken, + getUserInfo, + getSettingState, + getWorktabState, + setUserInfo, + setLoginStatus, + setLanguage, + setSearchHistory, + setLockStatus, + setLockPassword, + setToken, + logOut, + checkAndClearWorktabs + } + }, + { + persist: { + key: 'user', + storage: localStorage + } + } +) diff --git a/admin/src/store/modules/worktab.ts b/admin/src/store/modules/worktab.ts new file mode 100644 index 0000000..caa0d90 --- /dev/null +++ b/admin/src/store/modules/worktab.ts @@ -0,0 +1,568 @@ +/** + * 工作标签页状态管理模块 + * + * 提供多标签页功能的完整状态管理 + * + * ## 主要功能 + * + * - 标签页打开和关闭 + * - 标签页固定和取消固定 + * - 批量关闭(左侧、右侧、其他、全部) + * - 标签页缓存管理(KeepAlive) + * - 标签页标题自定义 + * - 标签页路由验证 + * - 动态路由参数处理 + * + * ## 使用场景 + * + * - 多标签页导航 + * - 页面缓存控制 + * - 标签页右键菜单 + * - 固定常用页面 + * - 批量关闭标签 + * + * ## 核心特性 + * + * - 智能标签页复用(同路由名称复用) + * - 固定标签页保护(不可关闭) + * - KeepAlive 缓存排除管理 + * - 路由有效性验证 + * - 首页自动保留 + * + * ## 持久化 + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-worktab + * - 刷新页面保持标签状态 + * + * @module store/modules/worktab + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { router } from '@/router' +import { LocationQueryRaw, Router } from 'vue-router' +import { WorkTab } from '@/types' +import { useCommon } from '@/hooks/core/useCommon' + +interface WorktabState { + current: Partial + opened: WorkTab[] + keepAliveExclude: string[] +} + +/** + * 工作台标签页管理 Store + */ +export const useWorktabStore = defineStore( + 'worktabStore', + () => { + // 状态定义 + const current = ref>({}) + const opened = ref([]) + const keepAliveExclude = ref([]) + + // 计算属性 + const hasOpenedTabs = computed(() => opened.value.length > 0) + const hasMultipleTabs = computed(() => opened.value.length > 1) + const currentTabIndex = computed(() => + current.value.path ? opened.value.findIndex((tab) => tab.path === current.value.path) : -1 + ) + + /** + * 查找标签页索引 + */ + const findTabIndex = (path: string): number => { + return opened.value.findIndex((tab) => tab.path === path) + } + + /** + * 获取标签页 + */ + const getTab = (path: string): WorkTab | undefined => { + return opened.value.find((tab) => tab.path === path) + } + + /** + * 检查标签页是否可关闭 + */ + const isTabClosable = (tab: WorkTab): boolean => { + return !tab.fixedTab + } + + /** + * 安全的路由跳转 + */ + const safeRouterPush = (tab: Partial): void => { + if (!tab.path) { + console.warn('尝试跳转到无效路径的标签页') + return + } + + try { + router.push({ + path: tab.path, + query: tab.query as LocationQueryRaw + }) + } catch (error) { + console.error('路由跳转失败:', error) + } + } + + /** + * 打开或激活一个选项卡 + */ + const openTab = (tab: WorkTab): void => { + if (!tab.path) { + console.warn('尝试打开无效的标签页') + return + } + + // 从 keepAlive 排除列表中移除 + if (tab.name) { + removeKeepAliveExclude(tab.name) + } + + // 先根据路由名称查找(应对动态路由参数导致的多开问题),找不到再根据路径查找 + let existingIndex = -1 + if (tab.name) { + existingIndex = opened.value.findIndex((t) => t.name === tab.name) + } + if (existingIndex === -1) { + existingIndex = findTabIndex(tab.path) + } + + if (existingIndex === -1) { + // 新增标签页 + const insertIndex = tab.fixedTab ? findFixedTabInsertIndex() : opened.value.length + const newTab = { ...tab } + + if (tab.fixedTab) { + opened.value.splice(insertIndex, 0, newTab) + } else { + opened.value.push(newTab) + } + + current.value = newTab + } else { + // 更新现有标签页(当动态路由参数或查询变更时,复用同一标签) + const existingTab = opened.value[existingIndex] + + opened.value[existingIndex] = { + ...existingTab, + path: tab.path, + params: tab.params, + query: tab.query, + title: tab.title || existingTab.title, + fixedTab: tab.fixedTab ?? existingTab.fixedTab, + keepAlive: tab.keepAlive ?? existingTab.keepAlive, + name: tab.name || existingTab.name, + icon: tab.icon || existingTab.icon + } + + current.value = opened.value[existingIndex] + } + } + + /** + * 查找固定标签页的插入位置 + */ + const findFixedTabInsertIndex = (): number => { + let insertIndex = 0 + for (let i = 0; i < opened.value.length; i++) { + if (opened.value[i].fixedTab) { + insertIndex = i + 1 + } else { + break + } + } + return insertIndex + } + + /** + * 关闭指定的选项卡 + */ + const removeTab = (path: string): void => { + const targetTab = getTab(path) + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试关闭不存在的标签页: ${path}`) + return + } + + if (targetTab && !isTabClosable(targetTab)) { + console.warn(`尝试关闭固定标签页: ${path}`) + return + } + + // 从标签页列表中移除 + opened.value.splice(targetIndex, 1) + + // 处理缓存排除 + if (targetTab?.name) { + addKeepAliveExclude(targetTab) + } + + const { homePath } = useCommon() + + // 如果关闭后无标签页,跳转首页 + if (!hasOpenedTabs.value) { + if (path !== homePath.value) { + current.value = {} + safeRouterPush({ path: homePath.value }) + } + return + } + + // 如果关闭的是当前激活标签,需要激活其他标签 + if (current.value.path === path) { + const newIndex = targetIndex >= opened.value.length ? opened.value.length - 1 : targetIndex + current.value = opened.value[newIndex] + safeRouterPush(current.value) + } + } + + /** + * 关闭左侧选项卡 + */ + const removeLeft = (path: string): void => { + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试关闭左侧标签页,但目标标签页不存在: ${path}`) + return + } + + // 获取左侧可关闭的标签页 + const leftTabs = opened.value.slice(0, targetIndex) + const closableLeftTabs = leftTabs.filter(isTabClosable) + + if (closableLeftTabs.length === 0) { + console.warn('左侧没有可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableLeftTabs) + + // 移除左侧可关闭的标签页 + opened.value = opened.value.filter( + (tab, index) => index >= targetIndex || !isTabClosable(tab) + ) + + // 确保当前标签是激活状态 + const targetTab = getTab(path) + if (targetTab) { + current.value = targetTab + } + } + + /** + * 关闭右侧选项卡 + */ + const removeRight = (path: string): void => { + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试关闭右侧标签页,但目标标签页不存在: ${path}`) + return + } + + // 获取右侧可关闭的标签页 + const rightTabs = opened.value.slice(targetIndex + 1) + const closableRightTabs = rightTabs.filter(isTabClosable) + + if (closableRightTabs.length === 0) { + console.warn('右侧没有可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableRightTabs) + + // 移除右侧可关闭的标签页 + opened.value = opened.value.filter( + (tab, index) => index <= targetIndex || !isTabClosable(tab) + ) + + // 确保当前标签是激活状态 + const targetTab = getTab(path) + if (targetTab) { + current.value = targetTab + } + } + + /** + * 关闭其他选项卡 + */ + const removeOthers = (path: string): void => { + const targetTab = getTab(path) + + if (!targetTab) { + console.warn(`尝试关闭其他标签页,但目标标签页不存在: ${path}`) + return + } + + // 获取其他可关闭的标签页 + const otherTabs = opened.value.filter((tab) => tab.path !== path) + const closableTabs = otherTabs.filter(isTabClosable) + + if (closableTabs.length === 0) { + console.warn('没有其他可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableTabs) + + // 只保留当前标签和固定标签 + opened.value = opened.value.filter((tab) => tab.path === path || !isTabClosable(tab)) + + // 确保当前标签是激活状态 + current.value = targetTab + } + + /** + * 关闭所有可关闭的标签页 + */ + const removeAll = (): void => { + const { homePath } = useCommon() + const hasFixedTabs = opened.value.some((tab) => tab.fixedTab) + + // 获取可关闭的标签页 + const closableTabs = opened.value.filter((tab) => { + if (!isTabClosable(tab)) return false + // 如果有固定标签,则所有可关闭的都可以关闭;否则保留首页 + return hasFixedTabs || tab.path !== homePath.value + }) + + if (closableTabs.length === 0) { + console.warn('没有可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableTabs) + + // 保留不可关闭的标签页和首页(当没有固定标签时) + opened.value = opened.value.filter((tab) => { + return !isTabClosable(tab) || (!hasFixedTabs && tab.path === homePath.value) + }) + + // 处理激活状态 + if (!hasOpenedTabs.value) { + current.value = {} + safeRouterPush({ path: homePath.value }) + return + } + + // 选择激活的标签页:优先首页,其次第一个可用标签 + const homeTab = opened.value.find((tab) => tab.path === homePath.value) + const targetTab = homeTab || opened.value[0] + + current.value = targetTab + safeRouterPush(targetTab) + } + + /** + * 将指定选项卡添加到 keepAlive 排除列表中 + */ + const addKeepAliveExclude = (tab: WorkTab): void => { + if (!tab.keepAlive || !tab.name) return + + if (!keepAliveExclude.value.includes(tab.name)) { + keepAliveExclude.value.push(tab.name) + } + } + + /** + * 从 keepAlive 排除列表中移除指定组件名称 + */ + const removeKeepAliveExclude = (name: string): void => { + if (!name) return + + keepAliveExclude.value = keepAliveExclude.value.filter((item) => item !== name) + } + + /** + * 将传入的一组选项卡的组件名称标记为排除缓存 + */ + const markTabsToRemove = (tabs: WorkTab[]): void => { + tabs.forEach((tab) => { + if (tab.name) { + addKeepAliveExclude(tab) + } + }) + } + + /** + * 切换指定标签页的固定状态 + */ + const toggleFixedTab = (path: string): void => { + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试切换不存在标签页的固定状态: ${path}`) + return + } + + const tab = { ...opened.value[targetIndex] } + tab.fixedTab = !tab.fixedTab + + // 移除原位置 + opened.value.splice(targetIndex, 1) + + if (tab.fixedTab) { + // 固定标签插入到所有固定标签的末尾 + const firstNonFixedIndex = opened.value.findIndex((t) => !t.fixedTab) + const insertIndex = firstNonFixedIndex === -1 ? opened.value.length : firstNonFixedIndex + opened.value.splice(insertIndex, 0, tab) + } else { + // 非固定标签插入到所有固定标签后 + const fixedCount = opened.value.filter((t) => t.fixedTab).length + opened.value.splice(fixedCount, 0, tab) + } + + // 更新当前标签引用 + if (current.value.path === path) { + current.value = tab + } + } + + /** + * 验证工作台标签页的路由有效性 + */ + const validateWorktabs = (routerInstance: Router): void => { + try { + // 动态路由校验:优先使用路由 name 判断有效性;否则用 resolve 匹配参数化路径 + const isTabRouteValid = (tab: Partial): boolean => { + try { + if (tab.name) { + const routes = routerInstance.getRoutes() + if (routes.some((r) => r.name === tab.name)) return true + } + if (tab.path) { + const resolved = routerInstance.resolve({ + path: tab.path, + query: (tab.query as LocationQueryRaw) || undefined + }) + return resolved.matched.length > 0 + } + return false + } catch { + return false + } + } + + // 过滤出有效的标签页 + const validTabs = opened.value.filter((tab) => isTabRouteValid(tab)) + + if (validTabs.length !== opened.value.length) { + console.warn('发现无效的标签页路由,已自动清理') + opened.value = validTabs + } + + // 验证当前激活标签的有效性 + const isCurrentValid = current.value && isTabRouteValid(current.value) + + if (!isCurrentValid && validTabs.length > 0) { + console.warn('当前激活标签无效,已自动切换') + current.value = validTabs[0] + } else if (!isCurrentValid) { + current.value = {} + } + } catch (error) { + console.error('验证工作台标签页失败:', error) + } + } + + /** + * 清空所有状态(用于登出等场景) + */ + const clearAll = (): void => { + current.value = {} + opened.value = [] + keepAliveExclude.value = [] + } + + /** + * 获取状态快照(用于持久化存储) + */ + const getStateSnapshot = (): WorktabState => { + return { + current: { ...current.value }, + opened: [...opened.value], + keepAliveExclude: [...keepAliveExclude.value] + } + } + + /** + * 获取标签页标题 + */ + const getTabTitle = (path: string): WorkTab | undefined => { + const tab = getTab(path) + return tab + } + + /** + * 更新标签页标题 + */ + const updateTabTitle = (path: string, title: string): void => { + const tab = getTab(path) + if (tab) { + tab.customTitle = title + } + } + + /** + * 重置标签页标题 + */ + const resetTabTitle = (path: string): void => { + const tab = getTab(path) + if (tab) { + tab.customTitle = '' + } + } + + return { + // 状态 + current, + opened, + keepAliveExclude, + + // 计算属性 + hasOpenedTabs, + hasMultipleTabs, + currentTabIndex, + + // 方法 + openTab, + removeTab, + removeLeft, + removeRight, + removeOthers, + removeAll, + toggleFixedTab, + validateWorktabs, + clearAll, + getStateSnapshot, + + // 工具方法 + findTabIndex, + getTab, + isTabClosable, + addKeepAliveExclude, + removeKeepAliveExclude, + markTabsToRemove, + getTabTitle, + updateTabTitle, + resetTabTitle + } + }, + { + persist: { + key: 'worktab', + storage: localStorage + } + } +) diff --git a/admin/src/types/api/api.d.ts b/admin/src/types/api/api.d.ts new file mode 100644 index 0000000..ad09c30 --- /dev/null +++ b/admin/src/types/api/api.d.ts @@ -0,0 +1,138 @@ +/** + * API 接口类型定义模块 + * + * 提供所有后端接口的类型定义 + * + * ## 主要功能 + * + * - 通用类型(分页参数、响应结构等) + * - 认证类型(登录、用户信息等) + * - 系统管理类型(用户、角色等) + * - 全局命名空间声明 + * + * ## 使用场景 + * + * - API 请求参数类型约束 + * - API 响应数据类型定义 + * - 接口文档类型同步 + * + * ## 注意事项 + * + * - 在 .vue 文件使用需要在 eslint.config.mjs 中配置 globals: { Api: 'readonly' } + * - 使用全局命名空间,无需导入即可使用 + * + * ## 使用方式 + * + * ```typescript + * const params: Api.Auth.LoginParams = { userName: 'admin', password: '123456' } + * const response: Api.Auth.UserInfo = await fetchUserInfo() + * ``` + * + * @module types/api/api + * @author Art Design Pro Team + */ + +declare namespace Api { + /** 通用类型 */ + namespace Common { + /** 分页参数 */ + interface PaginationParams { + /** 当前页码 */ + current: number + /** 每页条数 */ + size: number + /** 总条数 */ + total: number + } + + /** 通用搜索参数 */ + type CommonSearchParams = Pick + + /** 分页响应基础结构 */ + interface PaginatedResponse { + records: T[] + current: number + size: number + total: number + } + + /** 启用状态 */ + type EnableStatus = '1' | '2' + } + + /** 认证类型 */ + namespace Auth { + /** 登录参数 */ + interface LoginParams { + userName: string + password: string + } + + /** 登录响应 */ + interface LoginResponse { + token: string + refreshToken: string + } + + /** 用户信息 */ + interface UserInfo { + buttons: string[] + roles: string[] + userId: number + userName: string + email: string + avatar?: string + } + } + + /** 系统管理类型 */ + namespace SystemManage { + /** 用户列表 */ + type UserList = Api.Common.PaginatedResponse + + /** 用户列表项 */ + interface UserListItem { + id: number + avatar: string + status: string + userName: string + userGender: string + nickName: string + userPhone: string + userEmail: string + userRoles: string[] + createBy: string + createTime: string + updateBy: string + updateTime: string + } + + /** 用户搜索参数 */ + type UserSearchParams = Partial< + Pick & + Api.Common.CommonSearchParams + > + + /** 角色列表 */ + type RoleList = Api.Common.PaginatedResponse + + /** 角色列表项 */ + interface RoleListItem { + roleId: number + roleName: string + roleCode: string + description: string + enabled: boolean + createTime: string + } + + /** 角色搜索参数 */ + type RoleSearchParams = Partial< + Pick & + Api.Common.CommonSearchParams & { + startTime: string | null + endTime: string | null + } + > + } +} diff --git a/admin/src/types/common/index.ts b/admin/src/types/common/index.ts new file mode 100644 index 0000000..7e751d1 --- /dev/null +++ b/admin/src/types/common/index.ts @@ -0,0 +1,95 @@ +/** + * 通用类型定义模块 + * + * 提供项目中常用的通用类型定义 + * + * ## 主要功能 + * + * - 状态类型(启用/禁用) + * - 性别类型 + * - 排序方向类型 + * - 操作类型(增删改查) + * - 记录类型(键值对) + * - 时间范围类型 + * - 文件信息类型 + * - 坐标和尺寸类型 + * - 响应式断点类型 + * - 主题和语言类型 + * - 环境和弹窗类型 + * + * ## 使用场景 + * + * - 通用数据结构定义 + * - 类型约束和提示 + * - 减少重复类型定义 + * + * @module types/common/index + * @author Art Design Pro Team + */ + +// 导出响应类型 +export * from './response' + +// 状态类型 +export type Status = 0 | 1 // 0: 禁用, 1: 启用 + +// 性别类型 +export type Gender = 'male' | 'female' | 'unknown' + +// 排序方向 +export type SortOrder = 'ascending' | 'descending' + +// 操作类型 +export type ActionType = 'create' | 'update' | 'delete' | 'view' + +// 可选的记录类型 +export type Recordable = Record + +// 键值对类型 +export type KeyValue = { + key: string + value: T + label?: string +} + +// 时间范围类型 +export interface TimeRange { + startTime: string + endTime: string +} + +// 文件类型 +export interface FileInfo { + name: string + url: string + size: number + type: string + lastModified?: number +} + +// 坐标类型 +export interface Position { + x: number + y: number +} + +// 尺寸类型 +export interface Size { + width: number + height: number +} + +// 响应式断点类型 +export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +// 主题类型 +export type ThemeMode = 'light' | 'dark' | 'auto' + +// 语言类型 +export type Language = 'zh-CN' | 'en-US' + +// 环境类型 +export type Environment = 'development' | 'production' | 'test' + +// 弹窗类型 +export type DialogType = 'add' | 'edit' diff --git a/admin/src/types/common/response.ts b/admin/src/types/common/response.ts new file mode 100644 index 0000000..4a5fdab --- /dev/null +++ b/admin/src/types/common/response.ts @@ -0,0 +1,30 @@ +/** + * API 响应类型定义模块 + * + * 提供统一的 API 响应结构类型定义 + * + * ## 主要功能 + * + * - 基础响应结构定义 + * - 泛型支持(适配不同数据类型) + * - 统一的响应格式约束 + * + * ## 使用场景 + * + * - API 请求响应类型约束 + * - 接口数据类型定义 + * - 响应数据解析 + * + * @module types/common/response + * @author Art Design Pro Team + */ + +/** 基础 API 响应结构 */ +export interface BaseResponse { + /** 状态码 */ + code: number + /** 消息 */ + msg: string + /** 数据 */ + data: T +} diff --git a/admin/src/types/component/chart.ts b/admin/src/types/component/chart.ts new file mode 100644 index 0000000..c3225c9 --- /dev/null +++ b/admin/src/types/component/chart.ts @@ -0,0 +1,324 @@ +/** + * 图表组件类型定义模块 + * + * 提供 ECharts 图表组件的完整类型定义 + * + * ## 主要功能 + * + * - 基础图表配置类型 + * - 柱状图类型定义 + * - 折线图类型定义 + * - 饼图/环形图类型定义 + * - 雷达图类型定义 + * - K线图类型定义 + * - 散点图类型定义 + * - 地图图表类型定义 + * - 双向堆叠柱状图类型定义 + * - 图表主题配置类型 + * - 图表事件回调类型 + * + * ## 使用场景 + * + * - 图表组件 Props 类型约束 + * - 图表配置类型定义 + * - 图表数据结构定义 + * - 图表事件处理 + * + * @module types/component/chart + * @author Art Design Pro Team + */ +import type { EChartsOption } from '@/plugins/echarts' + +// 图例位置类型 +export type LegendPosition = 'bottom' | 'top' | 'left' | 'right' + +export type SymbolType = + | 'circle' + | 'rect' + | 'roundRect' + | 'triangle' + | 'diamond' + | 'pin' + | 'arrow' + | 'none' + +// 图表主题配置 +export interface ChartThemeConfig { + /** 图表高度 */ + chartHeight: string + /** 字体大小 */ + fontSize: number + /** 字体颜色 */ + fontColor: string + /** 主题颜色 */ + themeColor: string + /** 颜色组 */ + colors: string[] +} + +// 图表初始化选项 +export interface UseChartOptions { + /** 初始化选项 */ + initOptions?: EChartsOption + /** 延迟初始化时间(ms) */ + initDelay?: number + /** IntersectionObserver阈值 */ + threshold?: number + /** 是否自动响应主题变化 */ + autoTheme?: boolean +} + +// 基础图表 Props 接口 - 统一所有图表的基础属性 +export interface BaseChartProps { + /** 图表高度 */ + height?: string + /** 是否加载中 */ + loading?: boolean + isEmpty?: boolean + /** 颜色配置 */ + colors?: string[] +} + +// 轴线显示控制接口 - 统一轴线相关配置 +export interface AxisDisplayProps { + /** 是否显示坐标轴标签 */ + showAxisLabel?: boolean + /** 是否显示坐标轴线 */ + showAxisLine?: boolean + /** 是否显示分割线 */ + showSplitLine?: boolean +} + +// 交互显示控制接口 - 统一交互相关配置 +export interface InteractionProps { + /** 是否显示提示框 */ + showTooltip?: boolean + /** 是否显示图例 */ + showLegend?: boolean + /** 图例位置 */ + legendPosition?: LegendPosition +} + +// 柱状图数据项接口 +export interface BarDataItem { + /** 系列名称 */ + name: string + /** 数据值 */ + data: number[] + /** 柱状图宽度 */ + barWidth?: string | number + /** 堆叠分组名称 */ + stack?: string +} + +// 柱状图 Props 接口 - 统一柱状图配置 +export interface BarChartProps extends BaseChartProps, AxisDisplayProps, InteractionProps { + /** 图表数据 - 支持单组数据或多组数据 */ + data: number[] | BarDataItem[] + /** X轴标签数据 */ + xAxisData?: string[] + /** 柱状图宽度 */ + barWidth?: string | number + /** 是否堆叠显示 */ + stack?: boolean + /** 圆角 */ + borderRadius?: number | number[] +} + +// 折线图数据项接口 +export interface LineDataItem { + /** 系列名称 */ + name: string + /** 数据值 */ + data: number[] + /** 线条宽度 */ + lineWidth?: number + /** 是否显示区域填充 */ + showAreaColor?: boolean + /** 区域样式配置 */ + areaStyle?: { + /** 渐变开始透明度 */ + startOpacity?: number + /** 渐变结束透明度 */ + endOpacity?: number + /** 自定义 ECharts areaStyle 配置 */ + custom?: any + } + /** 是否平滑曲线 */ + smooth?: boolean + /** 数据点符号 */ + symbol?: SymbolType + /** 数据点大小 */ + symbolSize?: number +} + +// 折线图 Props 接口 - 统一折线图配置 +export interface LineChartProps extends BaseChartProps, AxisDisplayProps, InteractionProps { + /** 图表数据 - 支持单组数据或多组数据 */ + data: number[] | LineDataItem[] + /** X轴标签数据 */ + xAxisData?: string[] + /** 线条宽度 */ + lineWidth?: number + /** 是否显示区域填充 */ + showAreaColor?: boolean + /** 是否平滑曲线 */ + smooth?: boolean + /** 数据点符号 */ + symbol?: SymbolType + /** 数据点大小 */ + symbolSize?: number + /** 多数据动画延迟间隔(毫秒) */ + animationDelay?: number +} + +// 雷达图数据项接口 +export interface RadarDataItem { + /** 系列名称 */ + name: string + /** 数据值 */ + value: number[] +} + +// 雷达图 Props 接口 - 统一雷达图配置 +export interface RadarChartProps extends BaseChartProps, InteractionProps { + /** 雷达图指标配置 */ + indicator?: Array<{ name: string; max: number }> + /** 图表数据 */ + data?: RadarDataItem[] +} + +// 饼图/环形图数据项接口 +export interface PieDataItem { + /** 数据值 */ + value: number + /** 数据名称 */ + name: string +} + +// 环形图 Props 接口 - 统一环形图配置 +export interface RingChartProps extends BaseChartProps, InteractionProps { + /** 图表数据 */ + data: PieDataItem[] + /** 内外半径 */ + radius?: string[] + /** 边框圆角 */ + borderRadius?: number + /** 中心文本 */ + centerText?: string + /** 是否显示标签 */ + showLabel?: boolean +} + +// K线图数据项接口 +export interface KLineDataItem { + /** 时间标签 */ + time: string + /** 开盘价 */ + open: number + /** 收盘价 */ + close: number + /** 最高价 */ + high: number + /** 最低价 */ + low: number +} + +// K线图 Props 接口 - 统一K线图配置 +export interface KLineChartProps extends BaseChartProps { + /** 图表数据 */ + data?: KLineDataItem[] + /** 是否显示数据缩放控件 */ + showDataZoom?: boolean + /** 数据缩放初始开始位置 */ + dataZoomStart?: number + /** 数据缩放初始结束位置 */ + dataZoomEnd?: number +} + +// 散点图数据项接口 +export interface ScatterDataItem { + /** 坐标值 [x, y] */ + value: number[] +} + +// 散点图 Props 接口 - 统一散点图配置 +export interface ScatterChartProps extends BaseChartProps, AxisDisplayProps, InteractionProps { + /** 图表数据 */ + data?: ScatterDataItem[] + /** 散点大小 */ + symbolSize?: number +} + +// 双柱对比图 Props 接口 - 统一双柱对比图配置 +export interface DualBarCompareChartProps extends BaseChartProps { + /** 上方数据 */ + topData: number[] + /** 下方数据 */ + bottomData: number[] + /** X轴标签数据 */ + xAxisData: string[] + /** 上方柱子颜色 */ + topColor?: string + /** 下方柱子颜色 */ + bottomColor?: string + /** 柱状图宽度 */ + barWidth?: number +} + +// 地图图表 Props 接口 - 统一地图图表配置 +export interface MapChartProps extends BaseChartProps { + /** 地图数据 */ + mapData?: any[] + /** 选中区域 */ + selectedRegion?: string + /** 是否显示标签 */ + showLabels?: boolean + /** 是否显示散点 */ + showScatter?: boolean +} + +// 双向堆叠柱状图 Props 接口(人口金字塔样式) +export interface BidirectionalBarChartProps + extends BaseChartProps, + AxisDisplayProps, + InteractionProps { + /** 正向数据(向上显示) */ + positiveData: number[] + /** 负向数据(向下显示) */ + negativeData: number[] + /** X轴标签数据 */ + xAxisData?: string[] + /** 正向数据名称 */ + positiveName?: string + /** 负向数据名称 */ + negativeName?: string + /** 柱状图宽度 */ + barWidth?: string | number + /** Y轴最小值 */ + yAxisMin?: number + /** Y轴最大值 */ + yAxisMax?: number + /** 是否显示数据标签 */ + showDataLabel?: boolean + /** 正向数据圆角配置 */ + positiveBorderRadius?: number | number[] + /** 负向数据圆角配置 */ + negativeBorderRadius?: number | number[] +} + +// 图表配置生成器函数类型 +export type ChartOptionGenerator = () => EChartsOption + +// 图表事件回调类型 +export type ChartEventCallback = (params: any) => void + +// 图表错误信息接口 +export interface ChartError { + /** 错误码 */ + code: string + /** 错误信息 */ + message: string + /** 错误详情 */ + details?: any +} diff --git a/admin/src/types/component/index.ts b/admin/src/types/component/index.ts new file mode 100644 index 0000000..b9e90c6 --- /dev/null +++ b/admin/src/types/component/index.ts @@ -0,0 +1,145 @@ +/** + * 组件类型定义模块 + * + * 提供项目组件的类型定义 + * + * ## 主要功能 + * + * - 搜索组件类型定义 + * - 表格列配置类型 + * - 分页配置类型 + * - 表单规则类型 + * - 对话框配置类型 + * + * ## 使用场景 + * + * - 组件 Props 类型约束 + * - 组件配置类型定义 + * - 组件事件参数类型 + * + * @module types/component/index + * @author Art Design Pro Team + */ + +// 搜索组件类型 +export type SearchComponentType = + | 'input' + | 'select' + | 'radio' + | 'checkbox' + | 'date' + | 'datetime' + | 'daterange' + | 'datetimerange' + | 'month' + | 'monthrange' + | 'year' + | 'yearrange' + | 'week' + | 'time' + | 'timerange' + +// 搜索框值变化参数 +export interface SearchChangeParams { + prop: string + val: unknown +} + +// 表格列配置接口 +export interface ColumnOption { + // 列类型 + type?: 'selection' | 'expand' | 'index' | 'globalIndex' + // 列属性名 + prop?: string + // 列标题 + label?: string + // 列宽度 + width?: string | number + // 最小列宽度 + minWidth?: string | number + // 固定列 + fixed?: boolean | 'left' | 'right' + // 是否可排序 + sortable?: boolean | 'custom' + // 过滤器选项 + filters?: any[] + // 过滤方法 + filterMethod?: (value: any, row: any) => boolean + // 过滤器位置 + filterPlacement?: string + // 是否禁用 + disabled?: boolean + // 是否显示列 + visible?: boolean + // 是否选中显示 + checked?: boolean + // 自定义渲染函数 + formatter?: (row: T) => any + // 插槽相关配置 + // 是否使用插槽渲染内容 + useSlot?: boolean + // 插槽名称(默认为 prop 值) + slotName?: string + // 是否使用表头插槽 + useHeaderSlot?: boolean + // 表头插槽名称(默认为 `${prop}-header`) + headerSlotName?: string + // 其他属性 + [key: string]: any +} + +// 分页配置 +export interface PaginationConfig { + // 当前页 + currentPage: number + // 每页条数 + pageSize: number + // 总条数 + total: number + // 每页显示个数选择器的选项 + pageSizes?: number[] + // 组件布局 + layout?: string + // 是否为小型分页 + small?: boolean +} + +// 表单规则 +export interface FormRule { + // 是否必填 + required?: boolean + // 错误提示信息 + message?: string + // 触发方式 + trigger?: string | string[] + // 最小长度 + min?: number + // 最大长度 + max?: number + // 正则表达式 + pattern?: RegExp + // 自定义验证函数 + validator?: (rule: any, value: any, callback: any) => void +} + +// 对话框配置 +export interface DialogConfig { + // 标题 + title: string + // 是否显示 + visible: boolean + // 宽度 + width?: string | number + // 是否可以通过点击 modal 关闭 + closeOnClickModal?: boolean + // 是否可以通过按下 ESC 关闭 + closeOnPressEscape?: boolean + // 是否显示关闭按钮 + showClose?: boolean + // 是否在 Dialog 出现时将 body 滚动锁定 + lockScroll?: boolean + // 是否显示遮罩层 + modal?: boolean + // 自定义类名 + customClass?: string +} diff --git a/admin/src/types/config/index.ts b/admin/src/types/config/index.ts new file mode 100644 index 0000000..dd144de --- /dev/null +++ b/admin/src/types/config/index.ts @@ -0,0 +1,211 @@ +/** + * 配置类型定义模块 + * + * 提供系统配置相关的类型定义 + * + * ## 主要功能 + * + * - 主题设置类型 + * - 菜单布局类型 + * - 节日配置类型 + * - 系统基础配置类型 + * - 快速入口配置类型 + * - 顶部栏功能配置类型 + * - 环境配置类型 + * - 应用配置类型 + * + * ## 使用场景 + * + * - 系统配置文件类型约束 + * - 配置项类型定义 + * - 配置数据验证 + * + * @module types/config/index + * @author Art Design Pro Team + */ + +import { MenuTypeEnum, SystemThemeEnum } from '@/enums/appEnum' +import { MenuThemeType, SystemThemeTypes } from '@/types/store' + +// 主题设置 +export interface ThemeSetting { + /** 主题名称 */ + name: string + /** 系统主题类型 */ + theme: SystemThemeEnum + /** 主题颜色数组 */ + color: string[] + /** 左侧线条颜色 */ + leftLineColor: string + /** 右侧线条颜色 */ + rightLineColor: string + /** 主题图片 */ + img: string +} + +// 菜单布局 +export interface MenuLayout { + /** 布局名称 */ + name: string + /** 菜单类型值 */ + value: MenuTypeEnum + /** 布局预览图 */ + img: string + /** 布局描述 */ + description?: string +} + +// 节日配置 +export interface FestivalConfig { + /** 节日日期(单日)或开始日期(日期范围) */ + date: string + /** 节日结束日期(可选,用于跨日期节日) */ + endDate?: string + /** 节日名称 */ + name: string + /** 烟花图片 */ + image: string + /** 滚动文本 */ + scrollText: string + /** 是否激活 */ + isActive?: boolean + /** 烟花播放次数(可选,默认为 3 次) */ + count?: number +} + +// 系统基础配置 +export interface SystemBasicConfig { + // 系统名称 + name: string + // 系统描述 + description?: string + // 系统logo + logo?: string + // 系统favicon + favicon?: string + // 版权信息 + copyright?: string +} + +// 快速入口基础项 +export interface FastEnterBaseItem { + /** 名称 */ + name: string + /** 是否启用 */ + enabled?: boolean + /** 排序权重 */ + order?: number + /** 路由名称 */ + routeName?: string + /** 外部链接 */ + link?: string +} + +// 快速入口应用项 +export interface FastEnterApplication extends FastEnterBaseItem { + /** 应用描述 */ + description: string + /** 图标代码 */ + icon: string + /** 图标颜色 */ + iconColor: string +} + +// 快速链接项 +export type FastEnterQuickLink = FastEnterBaseItem + +// 快速入口配置 +export interface FastEnterConfig { + /** 应用列表 */ + applications: FastEnterApplication[] + /** 快速链接 */ + quickLinks: FastEnterQuickLink[] + /** 显示条件(屏幕宽度) */ + minWidth?: number +} + +// 系统配置 +export interface SystemConfig { + // 系统基础信息 + systemInfo: SystemBasicConfig + // 系统主题样式 + systemThemeStyles: SystemThemeTypes + // 设置主题列表 + settingThemeList: ThemeSetting[] + // 菜单布局列表 + menuLayoutList: MenuLayout[] + // 主题列表 + themeList: MenuThemeType[] + // 暗色菜单样式 + darkMenuStyles: MenuThemeType[] + // 系统主色调 + systemMainColor: readonly string[] + // 快速入口配置 + fastEnter?: FastEnterConfig + // 顶部栏功能配置 + headerBar?: HeaderBarFeatureConfig +} + +// 环境配置 +export interface EnvConfig { + // 环境名称 + NODE_ENV: string + // 应用版本 + VITE_VERSION: string + // 应用端口 + VITE_PORT: string + // 应用基础路径 + VITE_BASE_URL: string + // API 地址 + VITE_API_URL: string + // 是否开启 Mock + VITE_USE_MOCK?: string + // 是否开启压缩 + VITE_USE_GZIP?: string + // 是否开启 CDN + VITE_USE_CDN?: string +} + +// 应用配置 +export interface AppConfig extends SystemConfig { + // 环境配置 + env: EnvConfig + // 开发模式 + isDev: boolean + // 生产模式 + isProd: boolean + // 测试模式 + isTest: boolean +} + +// 功能配置项基础接口 +export interface FeatureConfigItem { + enabled: boolean + description: string +} + +// 顶部栏功能配置接口 +export interface HeaderBarFeatureConfig { + /** 菜单按钮 */ + menuButton: FeatureConfigItem + /** 刷新按钮 */ + refreshButton: FeatureConfigItem + /** 快速入口 */ + fastEnter: FeatureConfigItem + /** 面包屑导航 */ + breadcrumb: FeatureConfigItem + /** 全局搜索 */ + globalSearch: FeatureConfigItem + /** 全屏功能 */ + fullscreen: FeatureConfigItem + /** 通知功能 */ + notification: FeatureConfigItem + /** 聊天功能 */ + chat: FeatureConfigItem + /** 多语言切换 */ + language: FeatureConfigItem + /** 设置面板 */ + settings: FeatureConfigItem + /** 主题切换 */ + themeToggle: FeatureConfigItem +} diff --git a/admin/src/types/directive/directive.d.ts b/admin/src/types/directive/directive.d.ts new file mode 100644 index 0000000..eb0154e --- /dev/null +++ b/admin/src/types/directive/directive.d.ts @@ -0,0 +1,15 @@ +import type { + AuthDirective, + RolesDirective, + RippleDirective, + HighlightDirective +} from '@/directives' + +declare module 'vue' { + export interface GlobalDirectives { + vAuth: AuthDirective + vRoles: RolesDirective + vRipple: RippleDirective + vHighlight: HighlightDirective + } +} diff --git a/admin/src/types/index.ts b/admin/src/types/index.ts new file mode 100644 index 0000000..9032fd2 --- /dev/null +++ b/admin/src/types/index.ts @@ -0,0 +1,22 @@ +/** + * 类型定义统一导出模块 + * 提供全局类型定义的统一导出入口 + * + * @module types/index + * @author Art Design Pro Team + */ + +/** 通用类型定义(基础类型、工具类型等) */ +export * from './common' + +/** 组件相关类型定义 */ +export * from './component' + +/** 状态管理相关类型定义 */ +export * from './store' + +/** 路由相关类型定义 */ +export * from './router' + +/** 配置相关类型定义 */ +export * from './config' diff --git a/admin/src/types/router/index.ts b/admin/src/types/router/index.ts new file mode 100644 index 0000000..d9ef012 --- /dev/null +++ b/admin/src/types/router/index.ts @@ -0,0 +1,80 @@ +/** + * 路由类型定义模块 + * + * 提供路由相关的类型定义 + * + * ## 主要功能 + * + * - 路由元数据类型(标题、图标、权限等) + * - 应用路由记录类型 + * - 路由配置扩展 + * + * ## 使用场景 + * + * - 路由配置类型约束 + * - 路由元数据定义 + * - 菜单生成 + * - 权限控制 + * + * @module types/router/index + * @author Art Design Pro Team + */ + +import { RouteRecordRaw } from 'vue-router' + +/** + * 路由元数据接口 + * 定义路由的各种配置属性 + */ +export interface RouteMeta extends Record { + /** 路由标题 */ + title: string + /** 路由图标 */ + icon?: string + /** 是否显示徽章 */ + showBadge?: boolean + /** 文本徽章 */ + showTextBadge?: string + /** 是否在菜单中隐藏 */ + isHide?: boolean + /** 是否在标签页中隐藏 */ + isHideTab?: boolean + /** 外部链接 */ + link?: string + /** 是否为iframe */ + isIframe?: boolean + /** 是否缓存 */ + keepAlive?: boolean + /** 操作权限 */ + authList?: Array<{ + title: string + authMark: string + }> + /** 是否为一级菜单 */ + isFirstLevel?: boolean + /** 角色权限 */ + roles?: string[] + /** 是否固定标签页 */ + fixedTab?: boolean + /** 激活菜单路径 */ + activePath?: string + /** 是否为全屏页面 */ + isFullPage?: boolean + /** 是否为权限按钮行 */ + isAuthButton?: boolean + /** 权限标识 */ + authMark?: string + /** 父级路径 */ + parentPath?: string +} + +/** + * 应用路由记录接口 + * 扩展 Vue Router 的路由记录类型 + */ +export interface AppRouteRecord extends Omit { + id?: number + meta: RouteMeta + children?: AppRouteRecord[] + component?: string | (() => Promise) +} diff --git a/admin/src/types/store/index.ts b/admin/src/types/store/index.ts new file mode 100644 index 0000000..019801e --- /dev/null +++ b/admin/src/types/store/index.ts @@ -0,0 +1,157 @@ +/** + * Store 状态类型定义模块 + * + * 提供 Pinia Store 的状态类型定义 + * + * ## 主要功能 + * + * - 系统主题类型 + * - 菜单主题类型 + * - 设置状态类型 + * - 工作标签页类型 + * - 用户状态类型 + * - 菜单状态类型 + * - 根状态类型 + * + * ## 使用场景 + * + * - Store 状态类型约束 + * - 状态数据结构定义 + * - 类型提示和自动补全 + * + * @module types/store/index + * @author Art Design Pro Team + */ + +import { MenuThemeEnum, SystemThemeEnum } from '@/enums/appEnum' +import { LocationQueryRaw } from 'vue-router' + +// 系统主题样式(light | dark) +export interface SystemThemeType { + /** 主题类名 */ + className: string +} + +// 定义包含多个主题的类型 +export type SystemThemeTypes = { + [key in Exclude]: SystemThemeType +} + +// 菜单主题样式 +export interface MenuThemeType { + /** 主题类型 */ + theme: MenuThemeEnum + /** 背景颜色 */ + background: string + /** 系统名称颜色 */ + systemNameColor: string + /** 文本颜色 */ + textColor: string + /** 图标颜色 */ + iconColor: string + /** 背景图片 */ + img?: string +} + +// 设置中心 +export interface SettingState { + /** 主题 */ + theme: string + /** 是否只保持一个子菜单的展开 */ + uniqueOpened: boolean + /** 是否显示菜单按钮 */ + menuButton: boolean + /** 是否显示刷新按钮 */ + showRefreshButton: boolean + /** 是否显示面包屑 */ + showCrumbs: boolean + /** 是否自动关闭 */ + autoClose: boolean + /** 是否显示工作标签页 */ + showWorkTab: boolean + /** 是否显示语言切换 */ + showLanguage: boolean + /** 是否显示进度条 */ + showNprogress: boolean + /** 主题模式 */ + themeModel: string +} + +// 多标签 +export interface WorkTab { + /** 标签标题 */ + title: string + /** 自定义标题 */ + customTitle?: string + /** 路由路径 */ + path: string + /** 路由名称 */ + name: string + /** 是否缓存 */ + keepAlive: boolean + /** 是否固定标签 */ + fixedTab?: boolean + /** 路由参数 */ + params?: object + /** 路由查询参数 */ + query?: LocationQueryRaw + /** 图标 */ + icon?: string + /** 是否激活 */ + isActive?: boolean +} + +// 用户Store状态 +export interface UserState { + /** 用户信息 */ + userInfo: Api.Auth.UserInfo | null + /** 认证令牌 */ + token: string | null + /** 用户角色列表 */ + roles: string[] + /** 用户权限列表 */ + permissions: string[] +} + +// 设置Store状态 +export interface SettingStoreState extends SettingState { + // 额外的设置状态 + /** 菜单是否折叠 */ + collapsed: boolean + /** 设备类型 */ + device: 'desktop' | 'mobile' + /** 当前语言 */ + language: string +} + +// 工作标签页Store状态 +export interface WorkTabState { + /** 标签页列表 */ + tabs: WorkTab[] + /** 当前激活的标签页 */ + activeTab: string + /** 缓存的标签页列表 */ + cachedTabs: string[] +} + +// 菜单Store状态 +export interface MenuState { + /** 菜单列表 */ + menuList: any[] + /** 菜单是否已加载 */ + isLoaded: boolean + /** 菜单是否折叠 */ + collapsed: boolean +} + +// 根Store状态类型 +export interface RootState { + /** 用户状态 */ + user: UserState + /** 设置状态 */ + setting: SettingStoreState + /** 工作标签页状态 */ + workTab: WorkTabState + /** 菜单状态 */ + menu: MenuState +} diff --git a/admin/src/utils/form/index.ts b/admin/src/utils/form/index.ts new file mode 100644 index 0000000..ed23a46 --- /dev/null +++ b/admin/src/utils/form/index.ts @@ -0,0 +1,12 @@ +/** + * 表单工具函数统一导出 + * + * @module utils/form + * @author Art Design Pro Team + */ + +// 表单验证器 +export * from './validator' + +// 响应式布局 +export * from './responsive' diff --git a/admin/src/utils/form/responsive.ts b/admin/src/utils/form/responsive.ts new file mode 100644 index 0000000..c11df92 --- /dev/null +++ b/admin/src/utils/form/responsive.ts @@ -0,0 +1,122 @@ +/** + * 表单响应式布局工具模块 + * + * 提供表单项在不同屏幕尺寸下的智能布局计算 + * + * ## 主要功能 + * + * - 响应式断点管理(xs/sm/md/lg/xl) + * - 表单列宽自动降级(避免小屏幕压缩) + * - 基于阈值的智能 span 计算 + * - 响应式计算器工厂函数 + * - 可配置的断点规则 + * + * ## 使用场景 + * + * - 表单组件响应式布局 + * - 搜索表单自适应 + * - 移动端表单优化 + * - 多列表单布局 + * + * ## 断点说明(基于 Element Plus Grid 24 栅格系统): + * - xs (手机): < 768px,小于 12 时降级为 24(满宽) + * - sm (平板): ≥ 768px,小于 12 时降级为 12(半宽) + * - md (中等屏幕): ≥ 992px,小于 8 时降级为 8(三分之一宽) + * - lg (大屏幕): ≥ 1200px,直接使用设置的 span + * - xl (超大屏幕): ≥ 1920px,直接使用设置的 span + * + * ## 核心功能 + * + * - calculateResponsiveSpan: 计算响应式列宽 + * - createResponsiveSpanCalculator: 创建 span 计算器(柯里化) + * + * @module utils/form/responsive + * @author Art Design Pro Team + */ + +/** + * 响应式断点类型 + */ +export type ResponsiveBreakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +/** + * 断点配置映射 + */ +interface BreakpointConfig { + /** 最小 span 阈值 */ + threshold: number + /** 降级后的 span 值 */ + fallback: number +} + +/** + * 响应式断点配置 + */ +const BREAKPOINT_CONFIG: Record = { + xs: { threshold: 12, fallback: 24 }, // 手机:小于 12 时使用满宽 + sm: { threshold: 12, fallback: 12 }, // 平板:小于 12 时使用半宽 + md: { threshold: 8, fallback: 8 }, // 中等屏幕:小于 8 时使用三分之一宽 + lg: null, // 大屏幕:直接使用设置的 span + xl: null // 超大屏幕:直接使用设置的 span +} + +/** + * 计算响应式列宽 + * + * 根据屏幕尺寸智能降级,避免小屏幕上表单项被压缩过小 + * + * @param itemSpan 表单项自定义的 span 值 + * @param defaultSpan 默认的 span 值 + * @param breakpoint 当前断点 + * @returns 计算后的 span 值 + * + * @example + * ```ts + * // 在 xs 断点下,span 为 6 会降级为 24(满宽) + * calculateResponsiveSpan(6, 6, 'xs') // 24 + * + * // 在 md 断点下,span 为 6 会降级为 8(三分之一宽) + * calculateResponsiveSpan(6, 6, 'md') // 8 + * + * // 在 lg 断点下,直接使用原始 span + * calculateResponsiveSpan(6, 6, 'lg') // 6 + * ``` + */ +export function calculateResponsiveSpan( + itemSpan: number | undefined, + defaultSpan: number, + breakpoint: ResponsiveBreakpoint +): number { + const finalSpan = itemSpan ?? defaultSpan + const config = BREAKPOINT_CONFIG[breakpoint] + + // 如果没有配置(lg/xl),直接返回原始 span + if (!config) { + return finalSpan + } + + // 如果 span 小于阈值,使用降级值 + return finalSpan >= config.threshold ? finalSpan : config.fallback +} + +/** + * 创建响应式 span 计算器 + * + * 返回一个函数,用于计算指定断点下的 span 值 + * + * @param defaultSpan 默认的 span 值 + * @returns span 计算函数 + * + * @example + * ```ts + * const getColSpan = createResponsiveSpanCalculator(6) + * getColSpan(undefined, 'xs') // 24 + * getColSpan(8, 'md') // 8 + * getColSpan(12, 'lg') // 12 + * ``` + */ +export function createResponsiveSpanCalculator(defaultSpan: number) { + return (itemSpan: number | undefined, breakpoint: ResponsiveBreakpoint): number => { + return calculateResponsiveSpan(itemSpan, defaultSpan, breakpoint) + } +} diff --git a/admin/src/utils/form/validator.ts b/admin/src/utils/form/validator.ts new file mode 100644 index 0000000..3670763 --- /dev/null +++ b/admin/src/utils/form/validator.ts @@ -0,0 +1,316 @@ +/** + * 表单验证工具模块 + * + * 提供全面的表单字段验证功能 + * + * ## 主要功能 + * + * - 手机号码验证(中国大陆格式) + * - 固定电话验证(支持区号格式) + * - 用户账号验证(字母开头,支持数字和下划线) + * - 密码强度验证(普通密码、强密码) + * - 密码强度评估(弱、中、强) + * - IPv4 地址验证 + * - 邮箱地址验证(RFC 5322 标准) + * - URL 地址验证 + * - 身份证号码验证(18位,含校验码验证) + * - 银行卡号验证(Luhn 算法) + * - 字符串空格处理 + * + * ## 验证规则 + * + * - 手机号:1开头,第二位3-9,共11位 + * - 账号:字母开头,5-20位,支持字母数字下划线 + * - 普通密码:6-20位,必须包含字母和数字 + * - 强密码:8-20位,必须包含大小写字母、数字和特殊字符 + * - 身份证:18位,含出生日期和校验码验证 + * - 银行卡:13-19位,通过 Luhn 算法验证 + * + * @module utils/validation/formValidator + * @author Art Design Pro Team + */ + +/** + * 密码强度级别枚举 + */ +export enum PasswordStrength { + WEAK = '弱', + MEDIUM = '中', + STRONG = '强' +} + +/** + * 去除字符串首尾空格 + * @param value 待处理的字符串 + * @returns 返回去除首尾空格后的字符串 + */ +export function trimSpaces(value: string): string { + if (typeof value !== 'string') { + return '' + } + return value.trim() +} + +/** + * 验证手机号码(中国大陆) + * @param value 手机号码字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validatePhone(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + // 中国大陆手机号码:1开头,第二位为3-9,共11位数字 + const phoneRegex = /^1[3-9]\d{9}$/ + return phoneRegex.test(value.trim()) +} + +/** + * 验证固定电话号码(中国大陆) + * @param value 电话号码字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateTelPhone(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + // 支持格式:区号-号码,如:010-12345678、0755-1234567 + const telRegex = /^0\d{2,3}-?\d{7,8}$/ + return telRegex.test(value.trim().replace(/\s+/g, '')) +} + +/** + * 验证用户账号 + * @param value 账号字符串 + * @returns 返回验证结果,true表示格式正确 + * @description 规则:字母开头,5-20位,支持字母、数字、下划线 + */ +export function validateAccount(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + // 字母开头,5-20位,支持字母、数字、下划线 + const accountRegex = /^[a-zA-Z][a-zA-Z0-9_]{4,19}$/ + return accountRegex.test(value.trim()) +} + +/** + * 验证密码 + * @param value 密码字符串 + * @returns 返回验证结果,true表示格式正确 + * @description 规则:6-20位,必须包含字母和数字 + */ +export function validatePassword(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // 长度检查 + if (trimmedValue.length < 6 || trimmedValue.length > 20) { + return false + } + + // 必须包含字母和数字 + const hasLetter = /[a-zA-Z]/.test(trimmedValue) + const hasNumber = /\d/.test(trimmedValue) + + return hasLetter && hasNumber +} + +/** + * 验证强密码 + * @param value 密码字符串 + * @returns 返回验证结果,true表示格式正确 + * @description 规则:8-20位,必须包含大写字母、小写字母、数字和特殊字符 + */ +export function validateStrongPassword(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // 长度检查 + if (trimmedValue.length < 8 || trimmedValue.length > 20) { + return false + } + + // 必须包含:大写字母、小写字母、数字、特殊字符 + const hasUpperCase = /[A-Z]/.test(trimmedValue) + const hasLowerCase = /[a-z]/.test(trimmedValue) + const hasNumber = /\d/.test(trimmedValue) + const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(trimmedValue) + + return hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar +} + +/** + * 获取密码强度 + * @param value 密码字符串 + * @returns 返回密码强度:弱、中、强 + * @description 弱:纯数字/纯字母/纯特殊字符;中:两种组合;强:三种或以上组合 + */ +export function getPasswordStrength(value: string): PasswordStrength { + if (!value || typeof value !== 'string') { + return PasswordStrength.WEAK + } + + const trimmedValue = value.trim() + + if (trimmedValue.length < 6) { + return PasswordStrength.WEAK + } + + const hasUpperCase = /[A-Z]/.test(trimmedValue) + const hasLowerCase = /[a-z]/.test(trimmedValue) + const hasNumber = /\d/.test(trimmedValue) + const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(trimmedValue) + + const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(Boolean).length + + if (typeCount >= 3) { + return PasswordStrength.STRONG + } else if (typeCount >= 2) { + return PasswordStrength.MEDIUM + } else { + return PasswordStrength.WEAK + } +} + +/** + * 验证IPv4地址 + * @param value IP地址字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateIPv4Address(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + const ipRegex = /^((25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(25[0-5]|2[0-4]\d|[01]?\d{1,2})$/ + + if (!ipRegex.test(trimmedValue)) { + return false + } + + // 额外检查每个段是否在有效范围内 + const segments = trimmedValue.split('.') + return segments.every((segment) => { + const num = parseInt(segment, 10) + return num >= 0 && num <= 255 + }) +} + +/** + * 验证邮箱地址 + * @param value 邮箱地址字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateEmail(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // RFC 5322 标准的简化版邮箱正则 + const emailRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + + return emailRegex.test(trimmedValue) && trimmedValue.length <= 254 +} + +/** + * 验证URL地址 + * @param value URL字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateURL(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + try { + new URL(value.trim()) + return true + } catch { + return false + } +} + +/** + * 验证身份证号码(中国大陆) + * @param value 身份证号码字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateChineseIDCard(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // 18位身份证号码正则 + const idCardRegex = + /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/ + + if (!idCardRegex.test(trimmedValue)) { + return false + } + + // 验证校验码 + const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] + + let sum = 0 + for (let i = 0; i < 17; i++) { + sum += parseInt(trimmedValue[i]) * weights[i] + } + + const checkCode = checkCodes[sum % 11] + return trimmedValue[17].toUpperCase() === checkCode +} + +/** + * 验证银行卡号 + * @param value 银行卡号字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateBankCard(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim().replace(/\s+/g, '') + + // 银行卡号通常为13-19位数字 + if (!/^\d{13,19}$/.test(trimmedValue)) { + return false + } + + // Luhn算法验证 + let sum = 0 + let shouldDouble = false + + for (let i = trimmedValue.length - 1; i >= 0; i--) { + let digit = parseInt(trimmedValue[i]) + + if (shouldDouble) { + digit *= 2 + if (digit > 9) { + digit = (digit % 10) + 1 + } + } + + sum += digit + shouldDouble = !shouldDouble + } + + return sum % 10 === 0 +} diff --git a/admin/src/utils/http/error.ts b/admin/src/utils/http/error.ts new file mode 100644 index 0000000..0f2c1ae --- /dev/null +++ b/admin/src/utils/http/error.ts @@ -0,0 +1,182 @@ +/** + * HTTP 错误处理模块 + * + * 提供统一的 HTTP 请求错误处理机制 + * + * ## 主要功能 + * + * - 自定义 HttpError 错误类,封装错误信息、状态码、时间戳等 + * - 错误拦截和转换,将 Axios 错误转换为标准的 HttpError + * - 错误消息国际化处理,根据状态码返回对应的多语言错误提示 + * - 错误日志记录,便于问题追踪和调试 + * - 错误和成功消息的统一展示 + * - 类型守卫函数,用于判断错误类型 + * + * ## 使用场景 + * + * - HTTP 请求拦截器中统一处理错误 + * - 业务代码中捕获和处理特定错误 + * - 错误日志收集和上报 + * + * @module utils/http/error + * @author Art Design Pro Team + */ +import { AxiosError } from 'axios' +import { ApiStatus } from './status' +import { $t } from '@/locales' + +// 错误响应接口 +export interface ErrorResponse { + /** 错误状态码 */ + code: number + /** 错误消息 */ + msg: string + /** 错误附加数据 */ + data?: unknown +} + +// 错误日志数据接口 +export interface ErrorLogData { + /** 错误状态码 */ + code: number + /** 错误消息 */ + message: string + /** 错误附加数据 */ + data?: unknown + /** 错误发生时间戳 */ + timestamp: string + /** 请求 URL */ + url?: string + /** 请求方法 */ + method?: string + /** 错误堆栈信息 */ + stack?: string +} + +// 自定义 HttpError 类 +export class HttpError extends Error { + public readonly code: number + public readonly data?: unknown + public readonly timestamp: string + public readonly url?: string + public readonly method?: string + + constructor( + message: string, + code: number, + options?: { + data?: unknown + url?: string + method?: string + } + ) { + super(message) + this.name = 'HttpError' + this.code = code + this.data = options?.data + this.timestamp = new Date().toISOString() + this.url = options?.url + this.method = options?.method + } + + public toLogData(): ErrorLogData { + return { + code: this.code, + message: this.message, + data: this.data, + timestamp: this.timestamp, + url: this.url, + method: this.method, + stack: this.stack + } + } +} + +/** + * 获取错误消息 + * @param status 错误状态码 + * @returns 错误消息 + */ +const getErrorMessage = (status: number): string => { + const errorMap: Record = { + [ApiStatus.unauthorized]: 'httpMsg.unauthorized', + [ApiStatus.forbidden]: 'httpMsg.forbidden', + [ApiStatus.notFound]: 'httpMsg.notFound', + [ApiStatus.methodNotAllowed]: 'httpMsg.methodNotAllowed', + [ApiStatus.requestTimeout]: 'httpMsg.requestTimeout', + [ApiStatus.internalServerError]: 'httpMsg.internalServerError', + [ApiStatus.badGateway]: 'httpMsg.badGateway', + [ApiStatus.serviceUnavailable]: 'httpMsg.serviceUnavailable', + [ApiStatus.gatewayTimeout]: 'httpMsg.gatewayTimeout' + } + + return $t(errorMap[status] || 'httpMsg.internalServerError') +} + +/** + * 处理错误 + * @param error 错误对象 + * @returns 错误对象 + */ +export function handleError(error: AxiosError): never { + // 处理取消的请求 + if (error.code === 'ERR_CANCELED') { + console.warn('Request cancelled:', error.message) + throw new HttpError($t('httpMsg.requestCancelled'), ApiStatus.error) + } + + const statusCode = error.response?.status + const errorMessage = error.response?.data?.msg || error.message + const requestConfig = error.config + + // 处理网络错误 + if (!error.response) { + throw new HttpError($t('httpMsg.networkError'), ApiStatus.error, { + url: requestConfig?.url, + method: requestConfig?.method?.toUpperCase() + }) + } + + // 处理 HTTP 状态码错误 + const message = statusCode + ? getErrorMessage(statusCode) + : errorMessage || $t('httpMsg.requestFailed') + throw new HttpError(message, statusCode || ApiStatus.error, { + data: error.response.data, + url: requestConfig?.url, + method: requestConfig?.method?.toUpperCase() + }) +} + +/** + * 显示错误消息 + * @param error 错误对象 + * @param showMessage 是否显示错误消息 + */ +export function showError(error: HttpError, showMessage: boolean = true): void { + if (showMessage) { + ElMessage.error(error.message) + } + // 记录错误日志 + console.error('[HTTP Error]', error.toLogData()) +} + +/** + * 显示成功消息 + * @param message 成功消息 + * @param showMessage 是否显示消息 + */ +export function showSuccess(message: string, showMessage: boolean = true): void { + if (showMessage) { + ElMessage.success(message) + } +} + +/** + * 判断是否为 HttpError 类型 + * @param error 错误对象 + * @returns 是否为 HttpError 类型 + */ +export const isHttpError = (error: unknown): error is HttpError => { + return error instanceof HttpError +} diff --git a/admin/src/utils/http/index.ts b/admin/src/utils/http/index.ts new file mode 100644 index 0000000..0e0002a --- /dev/null +++ b/admin/src/utils/http/index.ts @@ -0,0 +1,214 @@ +/** + * HTTP 请求封装模块 + * 基于 Axios 封装的 HTTP 请求工具,提供统一的请求/响应处理 + * + * ## 主要功能 + * + * - 请求/响应拦截器(自动添加 Token、统一错误处理) + * - 401 未授权自动登出(带防抖机制) + * - 请求失败自动重试(可配置) + * - 统一的成功/错误消息提示 + * - 支持 GET/POST/PUT/DELETE 等常用方法 + * + * @module utils/http + * @author Art Design Pro Team + */ + +import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' +import { useUserStore } from '@/store/modules/user' +import { ApiStatus } from './status' +import { HttpError, handleError, showError, showSuccess } from './error' +import { $t } from '@/locales' +import { BaseResponse } from '@/types' + +/** 请求配置常量 */ +const REQUEST_TIMEOUT = 15000 +const LOGOUT_DELAY = 500 +const MAX_RETRIES = 0 +const RETRY_DELAY = 1000 +const UNAUTHORIZED_DEBOUNCE_TIME = 3000 + +/** 401防抖状态 */ +let isUnauthorizedErrorShown = false +let unauthorizedTimer: NodeJS.Timeout | null = null + +/** 扩展 AxiosRequestConfig */ +interface ExtendedAxiosRequestConfig extends AxiosRequestConfig { + showErrorMessage?: boolean + showSuccessMessage?: boolean +} + +const { VITE_API_URL, VITE_WITH_CREDENTIALS } = import.meta.env + +/** Axios实例 */ +const axiosInstance = axios.create({ + timeout: REQUEST_TIMEOUT, + baseURL: VITE_API_URL, + withCredentials: VITE_WITH_CREDENTIALS === 'true', + validateStatus: (status) => status >= 200 && status < 300, + transformResponse: [ + (data, headers) => { + const contentType = headers['content-type'] + if (contentType?.includes('application/json')) { + try { + return JSON.parse(data) + } catch { + return data + } + } + return data + } + ] +}) + +/** 请求拦截器 */ +axiosInstance.interceptors.request.use( + (request: InternalAxiosRequestConfig) => { + const { accessToken } = useUserStore() + if (accessToken) request.headers.set('Authorization', accessToken) + + if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) { + request.headers.set('Content-Type', 'application/json') + request.data = JSON.stringify(request.data) + } + + return request + }, + (error) => { + showError(createHttpError($t('httpMsg.requestConfigError'), ApiStatus.error)) + return Promise.reject(error) + } +) + +/** 响应拦截器 */ +axiosInstance.interceptors.response.use( + (response: AxiosResponse) => { + const { code, msg } = response.data + if (code === ApiStatus.success) return response + if (code === ApiStatus.unauthorized) handleUnauthorizedError(msg) + throw createHttpError(msg || $t('httpMsg.requestFailed'), code) + }, + (error) => { + if (error.response?.status === ApiStatus.unauthorized) handleUnauthorizedError() + return Promise.reject(handleError(error)) + } +) + +/** 统一创建HttpError */ +function createHttpError(message: string, code: number) { + return new HttpError(message, code) +} + +/** 处理401错误(带防抖) */ +function handleUnauthorizedError(message?: string): never { + const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized) + + if (!isUnauthorizedErrorShown) { + isUnauthorizedErrorShown = true + logOut() + + unauthorizedTimer = setTimeout(resetUnauthorizedError, UNAUTHORIZED_DEBOUNCE_TIME) + + showError(error, true) + throw error + } + + throw error +} + +/** 重置401防抖状态 */ +function resetUnauthorizedError() { + isUnauthorizedErrorShown = false + if (unauthorizedTimer) clearTimeout(unauthorizedTimer) + unauthorizedTimer = null +} + +/** 退出登录函数 */ +function logOut() { + setTimeout(() => { + useUserStore().logOut() + }, LOGOUT_DELAY) +} + +/** 是否需要重试 */ +function shouldRetry(statusCode: number) { + return [ + ApiStatus.requestTimeout, + ApiStatus.internalServerError, + ApiStatus.badGateway, + ApiStatus.serviceUnavailable, + ApiStatus.gatewayTimeout + ].includes(statusCode) +} + +/** 请求重试逻辑 */ +async function retryRequest( + config: ExtendedAxiosRequestConfig, + retries: number = MAX_RETRIES +): Promise { + try { + return await request(config) + } catch (error) { + if (retries > 0 && error instanceof HttpError && shouldRetry(error.code)) { + await delay(RETRY_DELAY) + return retryRequest(config, retries - 1) + } + throw error + } +} + +/** 延迟函数 */ +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +/** 请求函数 */ +async function request(config: ExtendedAxiosRequestConfig): Promise { + // POST | PUT 参数自动填充 + if ( + ['POST', 'PUT'].includes(config.method?.toUpperCase() || '') && + config.params && + !config.data + ) { + config.data = config.params + config.params = undefined + } + + try { + const res = await axiosInstance.request>(config) + + // 显示成功消息 + if (config.showSuccessMessage && res.data.msg) { + showSuccess(res.data.msg) + } + + return res.data.data as T + } catch (error) { + if (error instanceof HttpError && error.code !== ApiStatus.unauthorized) { + const showMsg = config.showErrorMessage !== false + showError(error, showMsg) + } + return Promise.reject(error) + } +} + +/** API方法集合 */ +const api = { + get(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'GET' }) + }, + post(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'POST' }) + }, + put(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'PUT' }) + }, + del(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'DELETE' }) + }, + request(config: ExtendedAxiosRequestConfig) { + return retryRequest(config) + } +} + +export default api diff --git a/admin/src/utils/http/status.ts b/admin/src/utils/http/status.ts new file mode 100644 index 0000000..989bb37 --- /dev/null +++ b/admin/src/utils/http/status.ts @@ -0,0 +1,18 @@ +/** + * 接口状态码 + */ +export enum ApiStatus { + success = 200, // 成功 + error = 400, // 错误 + unauthorized = 401, // 未授权 + forbidden = 403, // 禁止访问 + notFound = 404, // 未找到 + methodNotAllowed = 405, // 方法不允许 + requestTimeout = 408, // 请求超时 + internalServerError = 500, // 服务器错误 + notImplemented = 501, // 未实现 + badGateway = 502, // 网关错误 + serviceUnavailable = 503, // 服务不可用 + gatewayTimeout = 504, // 网关超时 + httpVersionNotSupported = 505 // HTTP版本不支持 +} diff --git a/admin/src/utils/index.ts b/admin/src/utils/index.ts new file mode 100644 index 0000000..a89a6b0 --- /dev/null +++ b/admin/src/utils/index.ts @@ -0,0 +1,31 @@ +/** + * Utils 工具函数统一导出 + * 提供向后兼容性和便捷导入 + * + * @module utils/index + * @author Art Design Pro Team + */ + +// UI 相关 +export * from './ui' + +// 路由相关 +export * from './router' + +// 路由导航相关 +export * from './navigation' + +// 系统管理相关 +export * from './sys' + +// 存储相关 +export * from './storage' + +// HTTP 相关 +export * from './http' + +// 表单相关 +export * from './form' + +// socket 相关 +export * from './socket' diff --git a/admin/src/utils/navigation/index.ts b/admin/src/utils/navigation/index.ts new file mode 100644 index 0000000..0b84e78 --- /dev/null +++ b/admin/src/utils/navigation/index.ts @@ -0,0 +1,10 @@ +/** + * 路由和导航相关工具函数统一导出 + * + * @module utils/navigation/index + * @author Art Design Pro Team + */ + +export * from './jump' +export * from './worktab' +export * from './route' diff --git a/admin/src/utils/navigation/jump.ts b/admin/src/utils/navigation/jump.ts new file mode 100644 index 0000000..320edb5 --- /dev/null +++ b/admin/src/utils/navigation/jump.ts @@ -0,0 +1,68 @@ +/** + * 导航跳转工具模块 + * + * 提供统一的页面跳转和导航功能 + * + * ## 主要功能 + * + * - 外部链接打开(新窗口) + * - 菜单项跳转处理(支持内部路由和外部链接) + * - iframe 页面跳转支持 + * - 递归查找并跳转到第一个可见的子菜单 + * - 智能判断跳转目标类型(外部链接/内部路由) + * + * @module utils/navigation/jump + * @author Art Design Pro Team + */ +import { AppRouteRecord } from '@/types/router' +import { router } from '@/router' +import { isNavigableMenuItem } from './route' + +// 打开外部链接 +export const openExternalLink = (link: string) => { + window.open(link, '_blank') +} + +/** + * 菜单跳转 + * @param item 菜单项 + * @param jumpToFirst 是否跳转到第一个子菜单 + * @returns + */ +export const handleMenuJump = (item: AppRouteRecord, jumpToFirst: boolean = false) => { + // 处理外部链接 + const { link, isIframe } = item.meta + if (link && !isIframe) { + return openExternalLink(link) + } + + // 如果不需要跳转到第一个子菜单,或者没有子菜单,直接跳转当前路径 + if (!jumpToFirst || !item.children?.length) { + return router.push(item.path) + } + + // 递归查找第一个可导航的叶子节点菜单 + const findFirstLeafMenu = (items: AppRouteRecord[]): AppRouteRecord | undefined => { + for (const child of items) { + if (isNavigableMenuItem(child)) { + return child.children?.length ? findFirstLeafMenu(child.children) || child : child + } + } + return undefined + } + + const firstChild = findFirstLeafMenu(item.children) + + // 如果子菜单都不可见,则回退到父级页面自身。 + if (!firstChild) { + return router.push(item.path) + } + + // 如果第一个子菜单是外部链接则打开新窗口 + if (firstChild.meta?.link) { + return openExternalLink(firstChild.meta.link) + } + + // 跳转到子菜单路径 + router.push(firstChild.path) +} diff --git a/admin/src/utils/navigation/route.ts b/admin/src/utils/navigation/route.ts new file mode 100644 index 0000000..518bcb9 --- /dev/null +++ b/admin/src/utils/navigation/route.ts @@ -0,0 +1,85 @@ +/** + * 路由工具模块 + * + * 提供路由处理和菜单路径相关的工具函数 + * + * ## 主要功能 + * + * - iframe 路由检测,判断是否为外部嵌入页面 + * - 菜单项有效性验证,过滤隐藏和无效菜单 + * - 路径标准化处理,统一路径格式 + * - 递归查找菜单树中第一个有效路径 + * - 支持多级嵌套菜单的路径解析 + * + * ## 使用场景 + * + * - 系统初始化时获取默认跳转路径 + * - 菜单权限过滤后获取首个可访问页面 + * - 路由重定向逻辑处理 + * - iframe 页面特殊处理 + * + * @module utils/navigation/route + * @author Art Design Pro Team + */ + +import { AppRouteRecord } from '@/types' + +// 检查是否为 iframe 路由 +export function isIframe(url: string): boolean { + return url.startsWith('/outside/iframe/') +} + +/** + * 判断菜单项是否可作为默认导航落点 + * 隐藏的全屏页面虽然不展示在菜单中,但仍然可能是合法首页。 + */ +export const isNavigableMenuItem = (menuItem: AppRouteRecord): boolean => { + if (!menuItem.path || !menuItem.path.trim()) { + return false + } + + if (!menuItem.meta?.isHide) { + return true + } + + return menuItem.meta?.isFullPage === true +} + +/** + * 标准化路径格式 + * @param path 路径 + * @returns 标准化后的路径 + */ +const normalizePath = (path: string): string => { + return path.startsWith('/') ? path : `/${path}` +} + +/** + * 递归获取菜单的第一个有效路径 + * @param menuList 菜单列表 + * @returns 第一个有效路径,如果没有找到则返回空字符串 + */ +export const getFirstMenuPath = (menuList: AppRouteRecord[]): string => { + if (!Array.isArray(menuList) || menuList.length === 0) { + return '' + } + + for (const menuItem of menuList) { + if (!isNavigableMenuItem(menuItem)) { + continue + } + + // 如果有子菜单,优先查找子菜单 + if (menuItem.children?.length) { + const childPath = getFirstMenuPath(menuItem.children) + if (childPath) { + return childPath + } + } + + // 返回当前菜单项的标准化路径 + return normalizePath(menuItem.path!) + } + + return '' +} diff --git a/admin/src/utils/navigation/worktab.ts b/admin/src/utils/navigation/worktab.ts new file mode 100644 index 0000000..6db6a77 --- /dev/null +++ b/admin/src/utils/navigation/worktab.ts @@ -0,0 +1,67 @@ +/** + * 工作标签页管理模块 + * + * 提供工作标签页(Worktab)的自动管理功能 + * + * ## 主要功能 + * + * - 根据路由导航自动创建和更新工作标签页 + * - iframe 页面标签页特殊处理 + * - 标签页信息提取(标题、路径、缓存状态等) + * - 固定标签页支持 + * - 根据系统设置控制标签页显示 + * - 首页标签页特殊处理 + * + * ## 使用场景 + * + * - 路由守卫中自动创建标签页 + * - 页面切换时更新标签页状态 + * - 多标签页导航系统 + * + * @module utils/navigation/worktab + * @author Art Design Pro Team + */ +import { useWorktabStore } from '@/store/modules/worktab' +import { RouteLocationNormalized } from 'vue-router' +import { isIframe } from './route' +import { useSettingStore } from '@/store/modules/setting' +import { IframeRouteManager } from '@/router/core' +import { useCommon } from '@/hooks/core/useCommon' + +/** + * 根据当前路由信息设置工作标签页(worktab) + * @param to 当前路由对象 + */ +export const setWorktab = (to: RouteLocationNormalized): void => { + const worktabStore = useWorktabStore() + const { meta, path, name, params, query } = to + if (!meta.isHideTab) { + // 如果是 iframe 页面,则特殊处理工作标签页 + if (isIframe(path)) { + const iframeRoute = IframeRouteManager.getInstance().findByPath(to.path) + + if (iframeRoute?.meta) { + worktabStore.openTab({ + title: iframeRoute.meta.title, + icon: meta.icon as string, + path, + name: name as string, + keepAlive: meta.keepAlive as boolean, + params, + query + }) + } + } else if (useSettingStore().showWorkTab || path === useCommon().homePath.value) { + worktabStore.openTab({ + title: meta.title as string, + icon: meta.icon as string, + path, + name: name as string, + keepAlive: meta.keepAlive as boolean, + params, + query, + fixedTab: meta.fixedTab as boolean + }) + } + } +} diff --git a/admin/src/utils/router.ts b/admin/src/utils/router.ts new file mode 100644 index 0000000..8c838ff --- /dev/null +++ b/admin/src/utils/router.ts @@ -0,0 +1,61 @@ +/** + * 路由工具函数 + * + * 提供路由相关的工具函数 + * + * @module utils/router + */ +import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router' +import AppConfig from '@/config' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import i18n, { $t } from '@/locales' + +/** 扩展的路由配置类型 */ +export type AppRouteRecordRaw = RouteRecordRaw & { + hidden?: boolean +} + +/** 顶部进度条配置 */ +export const configureNProgress = () => { + NProgress.configure({ + easing: 'ease', + speed: 600, + showSpinner: false, + parent: 'body' + }) +} + +/** + * 设置页面标题,根据路由元信息和系统信息拼接标题 + * @param to 当前路由对象 + */ +export const setPageTitle = (to: RouteLocationNormalized): void => { + const { title } = to.meta + if (title) { + setTimeout(() => { + document.title = `${formatMenuTitle(String(title))} - ${AppConfig.systemInfo.name}` + }, 150) + } +} + +/** + * 格式化菜单标题 + * @param title 菜单标题,可以是 i18n 的 key,也可以是字符串 + * @returns 格式化后的菜单标题 + */ +export const formatMenuTitle = (title: string): string => { + if (title) { + if (title.startsWith('menus.')) { + // 使用 te() 方法检查翻译键值是否存在,避免控制台警告 + if (i18n.global.te(title)) { + return $t(title) + } else { + // 如果翻译不存在,返回键值的最后部分作为fallback + return title.split('.').pop() || title + } + } + return title + } + return '' +} diff --git a/admin/src/utils/socket/index.ts b/admin/src/utils/socket/index.ts new file mode 100644 index 0000000..ac80da1 --- /dev/null +++ b/admin/src/utils/socket/index.ts @@ -0,0 +1,423 @@ +interface WebSocketOptions { + url?: string + messageHandler: (event: MessageEvent) => void + reconnectInterval?: number // 重连间隔(ms) + heartbeatInterval?: number // 心跳检测间隔(ms) + pingInterval?: number // 发送ping间隔(ms) + reconnectTimeout?: number // 重连超时时间(ms) + maxReconnectAttempts?: number // 最大重连次数 + connectionTimeout?: number // 连接建立超时时间(ms) +} + +export default class WebSocketClient { + private static instance: WebSocketClient | null = null + private ws: WebSocket | null = null + private url: string + private messageHandler: (event: MessageEvent) => void + private reconnectInterval: number + private heartbeatInterval: number + private pingInterval: number + private reconnectTimeout: number + private maxReconnectAttempts: number + private connectionTimeout: number + private reconnectAttempts: number = 0 // 当前重连次数 + + // 消息队列 - 缓存连接建立前的消息 + private messageQueue: Array = [] + + // 定时器 + private detectionTimer: NodeJS.Timeout | null = null + private timeoutTimer: NodeJS.Timeout | null = null + private reconnectTimer: NodeJS.Timeout | null = null + private pingTimer: NodeJS.Timeout | null = null + private connectionTimer: NodeJS.Timeout | null = null // 连接超时定时器 + + // 状态标识 + private isConnected: boolean = false + private isConnecting: boolean = false // 是否正在连接中 + private stopReconnect: boolean = false + private isReconnecting: boolean = false + + private constructor(options: WebSocketOptions) { + this.url = options.url || (process.env.VUE_APP_LOGIN_WEBSOCKET as string) + this.messageHandler = options.messageHandler + this.reconnectInterval = options.reconnectInterval ?? 20 * 1000 // 默认20秒 + this.heartbeatInterval = options.heartbeatInterval ?? 5 * 1000 // 默认5秒 + this.pingInterval = options.pingInterval ?? 10 * 1000 // 默认10秒 + this.reconnectTimeout = options.reconnectTimeout ?? 30 * 1000 // 默认30秒 + this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10 // 默认最多重连10次 + this.connectionTimeout = options.connectionTimeout ?? 10 * 1000 // 连接超时10秒 + } + + // 单例模式获取实例 + static getInstance(options: WebSocketOptions): WebSocketClient { + if (!WebSocketClient.instance) { + WebSocketClient.instance = new WebSocketClient(options) + } else { + // 更新消息处理器 + WebSocketClient.instance.messageHandler = options.messageHandler + // 如果提供了新的URL,则更新并重新连接 + if (options.url && WebSocketClient.instance.url !== options.url) { + WebSocketClient.instance.url = options.url + WebSocketClient.instance.reconnectAttempts = 0 + WebSocketClient.instance.init() + } + } + return WebSocketClient.instance + } + + // 初始化连接 + init(): void { + this.connect(true) + } + + private connect(resetReconnectAttempts: boolean = false): void { + // 如果正在连接中,不重复连接 + if (this.isConnecting) { + console.log('正在建立WebSocket连接中...') + return + } + + // 如果已连接,不重复连接 + if (this.ws?.readyState === WebSocket.OPEN) { + console.warn('WebSocket连接已存在') + this.flushMessageQueue() // 确保队列中的消息被发送 + return + } + + try { + this.isConnecting = true + this.stopReconnect = false + if (resetReconnectAttempts) { + this.reconnectAttempts = 0 + this.isReconnecting = false + this.clearTimer('reconnectTimer') + } + this.ws = new WebSocket(this.url) + + // 设置连接超时检测 + this.clearTimer('connectionTimer') + this.connectionTimer = setTimeout(() => { + console.error(`WebSocket连接超时 (${this.connectionTimeout}ms):${this.url}`) + this.handleConnectionTimeout() + }, this.connectionTimeout) + + this.ws.onopen = (event) => this.handleOpen(event) + this.ws.onmessage = (event) => this.handleMessage(event) + this.ws.onclose = (event) => this.handleClose(event) + this.ws.onerror = (event) => this.handleError(event) + } catch (error) { + console.error('WebSocket初始化失败:', error) + this.isConnecting = false + this.reconnect() + } + } + + // 处理连接超时 + private handleConnectionTimeout(): void { + if (this.ws?.readyState !== WebSocket.OPEN) { + console.error('WebSocket连接超时,强制关闭连接') + this.ws?.close(1000, 'Connection timeout') + this.isConnecting = false + this.reconnect() + } + } + + // 关闭连接 + close(force?: boolean): void { + this.clearAllTimers() + this.stopReconnect = true + this.isReconnecting = false + this.isConnecting = false + + if (this.ws) { + // 1000 表示正常关闭 + this.ws.close(force ? 1001 : 1000, force ? 'Force closed' : 'Normal close') + this.ws = null + } + + this.isConnected = false + } + + // 发送消息 - 增加消息队列 + send(data: string | ArrayBufferLike | Blob | ArrayBufferView, immediate: boolean = false): void { + // 如果要求立即发送且未连接,则直接报错 + if (immediate && (!this.ws || this.ws.readyState !== WebSocket.OPEN)) { + console.error('WebSocket未连接,无法立即发送消息') + return + } + + // 如果未连接且不要求立即发送,则加入消息队列 + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + console.log('WebSocket未连接,消息已加入队列等待发送') + this.messageQueue.push(data) + // 如果未在重连中,则尝试重连 + if (!this.isConnecting && !this.stopReconnect) { + this.init() + } + return + } + + try { + this.ws.send(data) + } catch (error) { + console.error('WebSocket发送消息失败:', error) + // 发送失败时将消息加入队列,等待重连后重试 + this.messageQueue.push(data) + this.reconnect() + } + } + + // 发送队列中的消息 + private flushMessageQueue(): void { + if (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) { + console.log(`发送队列中的${this.messageQueue.length}条消息`) + while (this.messageQueue.length > 0) { + const data = this.messageQueue.shift() + if (data) { + try { + this.ws?.send(data) + } catch (error) { + console.error('发送队列消息失败:', error) + // 如果发送失败,将消息放回队列头部 + if (data) this.messageQueue.unshift(data) + break + } + } + } + } + } + + // 处理连接打开 + private handleOpen(event: Event): void { + console.log('WebSocket连接成功', event) + this.clearTimer('connectionTimer') // 清除连接超时定时器 + this.isConnected = true + this.isConnecting = false + this.isReconnecting = false + this.stopReconnect = false + this.reconnectAttempts = 0 // 重置重连次数 + this.startHeartbeat() + this.startPing() + this.flushMessageQueue() // 发送队列中的消息 + } + + // 处理收到的消息 + private handleMessage(event: MessageEvent): void { + console.log('收到WebSocket消息:', event) + this.resetHeartbeat() + this.messageHandler(event) + } + + // 处理连接关闭 + private handleClose(event: CloseEvent): void { + console.log( + `WebSocket断开: 代码=${event.code}, 原因=${event.reason}, 干净关闭=${event.wasClean}` + ) + + // 1000 是正常关闭代码 + const isNormalClose = event.code === 1000 + + this.isConnected = false + this.isConnecting = false + this.clearConnectionTimers() + this.ws = null + + if (!this.stopReconnect && !isNormalClose) { + this.reconnect() + } + } + + // 处理错误 - 增加详细错误信息 + private handleError(event: Event): void { + console.error('WebSocket连接错误:') + console.error('错误事件:', event) + console.error( + '当前连接状态:', + this.ws?.readyState ? this.getReadyStateText(this.ws.readyState) : '未初始化' + ) + + this.isConnected = false + this.isConnecting = false + + // 只有在未停止重连的情况下才尝试重连 + if (!this.stopReconnect) { + this.reconnect() + } + } + + private closeCurrentSocketForReconnect(): void { + this.clearConnectionTimers() + this.isConnected = false + this.isConnecting = false + + if (this.ws) { + this.ws.onopen = null + this.ws.onmessage = null + this.ws.onclose = null + this.ws.onerror = null + + if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) { + this.ws.close(1001, 'Reconnect') + } + + this.ws = null + } + } + + // 转换连接状态为文本描述 + private getReadyStateText(state: number): string { + switch (state) { + case WebSocket.CONNECTING: + return 'CONNECTING (0) - 正在连接' + case WebSocket.OPEN: + return 'OPEN (1) - 已连接' + case WebSocket.CLOSING: + return 'CLOSING (2) - 正在关闭' + case WebSocket.CLOSED: + return 'CLOSED (3) - 已关闭' + default: + return `未知状态 (${state})` + } + } + + // 开始心跳检测 + private startHeartbeat(): void { + this.clearTimer('detectionTimer') + this.clearTimer('timeoutTimer') + + this.detectionTimer = setTimeout(() => { + this.isConnected = this.ws?.readyState === WebSocket.OPEN + + if (!this.isConnected) { + console.warn('WebSocket心跳检测失败,尝试重连') + this.reconnect() + + this.timeoutTimer = setTimeout(() => { + console.warn('WebSocket重连超时') + this.close() + }, this.reconnectTimeout) + } + }, this.heartbeatInterval) + } + + // 重置心跳检测 + private resetHeartbeat(): void { + this.clearTimer('detectionTimer') + this.clearTimer('timeoutTimer') + this.startHeartbeat() + } + + // 开始发送ping消息 + private startPing(): void { + this.clearTimer('pingTimer') + + this.pingTimer = setInterval(() => { + if (this.ws?.readyState !== WebSocket.OPEN) { + console.warn('WebSocket未连接,停止发送ping') + this.clearTimer('pingTimer') + this.reconnect() + return + } + + try { + this.ws.send('ping') + console.log('发送ping消息') + } catch (error) { + console.error('发送ping消息失败:', error) + this.clearTimer('pingTimer') + this.reconnect() + } + }, this.pingInterval) + } + + // 重连 - 增加重连次数限制 + private reconnect(): void { + if (this.stopReconnect || this.isConnecting || this.reconnectInterval <= 0) { + return + } + + // 检查是否超过最大重连次数 + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error(`已达到最大重连次数(${this.maxReconnectAttempts}),停止重连`) + this.close(true) + return + } + + this.reconnectAttempts++ + this.isReconnecting = true + this.closeCurrentSocketForReconnect() + + const delay = this.calculateReconnectDelay() + console.log( + `将在${delay / 1000}秒后尝试重新连接(第${this.reconnectAttempts}/${this.maxReconnectAttempts}次)` + ) + + this.clearTimer('reconnectTimer') + this.reconnectTimer = setTimeout(() => { + console.log(`尝试重新连接WebSocket(第${this.reconnectAttempts}次)`) + this.connect(false) + }, delay) + } + + // 计算重连延迟 - 指数退避策略 + private calculateReconnectDelay(): number { + // 基础延迟 + 随机值,避免多个客户端同时重连 + const jitter = Math.random() * 1000 // 0-1秒的随机延迟 + const baseDelay = Math.min( + this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1), + this.reconnectInterval * 5 + ) + return baseDelay + jitter + } + + // 清除指定定时器 + private clearTimer( + timerName: + | 'detectionTimer' + | 'timeoutTimer' + | 'reconnectTimer' + | 'pingTimer' + | 'connectionTimer' + ): void { + if (this[timerName]) { + clearTimeout(this[timerName] as NodeJS.Timeout) + this[timerName] = null + } + } + + // 清除所有定时器 + private clearAllTimers(): void { + this.clearConnectionTimers() + this.clearTimer('reconnectTimer') + } + + private clearConnectionTimers(): void { + this.clearTimer('detectionTimer') + this.clearTimer('timeoutTimer') + this.clearTimer('pingTimer') + this.clearTimer('connectionTimer') + } + + // 获取当前连接状态 + get isWebSocketConnected(): boolean { + return this.isConnected + } + + // 获取当前连接状态文本 + get connectionStatusText(): string { + if (this.isConnecting) return '正在连接' + if (this.isConnected) return '已连接' + if (this.isReconnecting && this.reconnectAttempts > 0) + return `重连中(${this.reconnectAttempts}/${this.maxReconnectAttempts})` + return '已断开' + } + + // 销毁实例 + static destroyInstance(): void { + if (WebSocketClient.instance) { + WebSocketClient.instance.close() + WebSocketClient.instance = null + } + } +} diff --git a/admin/src/utils/storage/index.ts b/admin/src/utils/storage/index.ts new file mode 100644 index 0000000..a4366f0 --- /dev/null +++ b/admin/src/utils/storage/index.ts @@ -0,0 +1,7 @@ +/** + * 存储相关工具函数统一导出 + */ + +export * from './storage' +export * from './storage-config' +export * from './storage-key-manager' diff --git a/admin/src/utils/storage/storage-config.ts b/admin/src/utils/storage/storage-config.ts new file mode 100644 index 0000000..c53b489 --- /dev/null +++ b/admin/src/utils/storage/storage-config.ts @@ -0,0 +1,125 @@ +/** + * 存储配置管理模块 + * + * 提供统一的本地存储配置和工具方法 + * + * ## 主要功能 + * + * - 版本化存储键管理,支持多版本数据隔离 + * - 存储键名生成和解析(带版本前缀) + * - 版本号提取和验证 + * - 存储键匹配的正则表达式生成 + * - 旧版本存储键兼容处理 + * - 升级和登出延迟配置 + * - 主题存储键配置 + * + * ## 使用场景 + * + * - Pinia Store 持久化存储 + * - 应用版本升级时的数据迁移 + * - 多版本数据清理 + * - 存储键的统一管理和规范 + * + * 存储键格式:sys-v{version}-{storeId} + * 例如:sys-v1.0.0-user, sys-v1.0.0-setting + * + * @module utils/storage/storage-config + * @author Art Design Pro Team + */ +export class StorageConfig { + /** 当前应用版本 */ + static readonly CURRENT_VERSION = __APP_VERSION__ + + /** 存储键前缀 */ + static readonly STORAGE_PREFIX = 'sys-v' + + /** 版本键名 */ + static readonly VERSION_KEY = 'sys-version' + + /** 主题键名(index.html中使用了,如果修改,需要同步修改) */ + static readonly THEME_KEY = 'sys-theme' + + /** 上次登录用户ID键名(用于判断是否为同一用户登录) */ + static readonly LAST_USER_ID_KEY = 'sys-last-user-id' + + /** 响应式布局切换时暂存桌面端菜单类型 */ + static readonly RESPONSIVE_MENU_TYPE_KEY = 'sys-responsive-menu-type' + + /** 跳过升级检查的版本 */ + static readonly SKIP_UPGRADE_VERSION = '1.0.0' + + /** 升级处理延迟时间(毫秒) */ + static readonly UPGRADE_DELAY = 1000 + + /** 登出延迟时间(毫秒) */ + static readonly LOGOUT_DELAY = 1000 + + /** + * 生成版本化的存储键名 + * @param storeId 存储ID + * @param version 版本号,默认使用当前版本 + */ + static generateStorageKey(storeId: string, version: string = this.CURRENT_VERSION): string { + return `${this.STORAGE_PREFIX}${version}-${storeId}` + } + + /** + * 生成旧版本的存储键名(不带分隔符) + * @param version 版本号,默认使用当前版本 + */ + static generateLegacyKey(version: string = this.CURRENT_VERSION): string { + return `${this.STORAGE_PREFIX}${version}` + } + + /** + * 创建存储键匹配的正则表达式 + * @param storeId 存储ID + */ + static createKeyPattern(storeId: string): RegExp { + return new RegExp(`^${this.STORAGE_PREFIX}[^-]+-${storeId}$`) + } + + /** + * 创建当前版本存储键匹配的正则表达式 + */ + static createCurrentVersionPattern(): RegExp { + return new RegExp(`^${this.STORAGE_PREFIX}${this.CURRENT_VERSION}-`) + } + + /** + * 创建任意版本存储键匹配的正则表达式 + */ + static createVersionPattern(): RegExp { + return new RegExp(`^${this.STORAGE_PREFIX}`) + } + + /** + * 检查是否为当前版本的键 + */ + static isCurrentVersionKey(key: string): boolean { + return key.startsWith(`${this.STORAGE_PREFIX}${this.CURRENT_VERSION}`) + } + + /** + * 检查是否为版本化的键 + */ + static isVersionedKey(key: string): boolean { + return key.startsWith(this.STORAGE_PREFIX) + } + + /** + * 从存储键中提取版本号 + */ + static extractVersionFromKey(key: string): string | null { + const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}([^-]+)`)) + return match ? match[1] : null + } + + /** + * 从存储键中提取存储ID + */ + static extractStoreIdFromKey(key: string): string | null { + const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}[^-]+-(.+)$`)) + return match ? match[1] : null + } +} diff --git a/admin/src/utils/storage/storage-key-manager.ts b/admin/src/utils/storage/storage-key-manager.ts new file mode 100644 index 0000000..ba14f65 --- /dev/null +++ b/admin/src/utils/storage/storage-key-manager.ts @@ -0,0 +1,97 @@ +/** + * 存储键名管理器模块 + * + * 提供智能的版本化存储键管理和数据迁移功能 + * + * ## 主要功能 + * + * - 自动生成当前版本的存储键名 + * - 检测当前版本数据是否存在 + * - 查找其他版本的同名存储数据 + * - 自动将旧版本数据迁移到当前版本 + * - 数据迁移日志记录 + * - 迁移失败的错误处理 + * + * ## 使用场景 + * + * - Pinia Store 持久化插件中获取存储键 + * - 应用版本升级时自动迁移用户数据 + * - 避免版本升级导致的数据丢失 + * - 实现平滑的版本过渡 + * + * ## 工作流程 + * + * 1. 优先使用当前版本的存储键 + * 2. 如果当前版本无数据,查找其他版本的同名数据 + * 3. 找到旧版本数据后自动迁移到当前版本 + * 4. 返回当前版本的存储键供使用 + * + * @module utils/storage/storage-key-manager + * @author Art Design Pro Team + */ +import { StorageConfig } from '@/utils/storage' + +/** + * 存储键名管理器 + * 负责处理版本化的存储键名生成和数据迁移 + */ +export class StorageKeyManager { + /** + * 获取当前版本的存储键名 + */ + private getCurrentVersionKey(storeId: string): string { + return StorageConfig.generateStorageKey(storeId) + } + + /** + * 检查当前版本的数据是否存在 + */ + private hasCurrentVersionData(key: string): boolean { + return localStorage.getItem(key) !== null + } + + /** + * 查找其他版本的同名存储键 + */ + private findExistingKey(storeId: string): string | null { + const storageKeys = Object.keys(localStorage) + const pattern = StorageConfig.createKeyPattern(storeId) + + return storageKeys.find((key) => pattern.test(key) && localStorage.getItem(key)) || null + } + + /** + * 将数据从旧版本迁移到当前版本 + */ + private migrateData(fromKey: string, toKey: string): void { + try { + const existingData = localStorage.getItem(fromKey) + if (existingData) { + localStorage.setItem(toKey, existingData) + console.info(`[Storage] 已迁移数据: ${fromKey} → ${toKey}`) + } + } catch (error) { + console.warn(`[Storage] 数据迁移失败: ${fromKey}`, error) + } + } + + /** + * 获取持久化存储的键名(支持自动数据迁移) + */ + getStorageKey(storeId: string): string { + const currentKey = this.getCurrentVersionKey(storeId) + + // 优先使用当前版本的数据 + if (this.hasCurrentVersionData(currentKey)) { + return currentKey + } + + // 查找并迁移其他版本的数据 + const existingKey = this.findExistingKey(storeId) + if (existingKey) { + this.migrateData(existingKey, currentKey) + } + + return currentKey + } +} diff --git a/admin/src/utils/storage/storage.ts b/admin/src/utils/storage/storage.ts new file mode 100644 index 0000000..67b9e9e --- /dev/null +++ b/admin/src/utils/storage/storage.ts @@ -0,0 +1,250 @@ +/** + * 存储兼容性管理模块 + * + * 提供完整的本地存储兼容性检查和数据验证功能 + * + * 主要功能 + * + * - 多版本存储数据检测和验证 + * - 新旧存储格式兼容处理 + * - 存储数据完整性校验 + * - 存储异常自动恢复(清理+登出) + * - 登录状态验证 + * - 存储为空检测 + * - 版本号管理 + * + * ## 使用场景 + * + * - 应用启动时检查存储数据有效性 + * - 路由守卫中验证登录状态 + * - 版本升级时的数据兼容性检查 + * - 存储异常时的自动恢复 + * - 防止因存储数据损坏导致的系统异常 + * + * ## 工作流程 + * + * 1. 优先检查当前版本的存储数据 + * 2. 检查其他版本的存储数据 + * 3. 兼容旧格式的存储数据 + * 4. 验证数据完整性 + * 5. 异常时提示用户并执行登出 + * + * @module utils/storage/storage + * @author Art Design Pro Team + */ +import { router } from '@/router' +import { useUserStore } from '@/store/modules/user' +import { StorageConfig } from '@/utils/storage/storage-config' + +/** + * 存储兼容性管理器 + * 负责处理不同版本间的存储兼容性检查和数据验证 + */ +class StorageCompatibilityManager { + /** + * 获取系统版本号 + */ + getSystemVersion(): string | null { + return localStorage.getItem(StorageConfig.VERSION_KEY) + } + + /** + * 获取系统存储数据(兼容旧格式) + */ + getSystemStorage(): any { + const version = this.getSystemVersion() || StorageConfig.CURRENT_VERSION + const legacyKey = StorageConfig.generateLegacyKey(version) + const data = localStorage.getItem(legacyKey) + return data ? JSON.parse(data) : null + } + + /** + * 检查当前版本是否有存储数据 + */ + private hasCurrentVersionStorage(): boolean { + const storageKeys = Object.keys(localStorage) + const currentVersionPattern = StorageConfig.createCurrentVersionPattern() + + return storageKeys.some( + (key) => currentVersionPattern.test(key) && localStorage.getItem(key) !== null + ) + } + + /** + * 检查是否存在任何版本的存储数据 + */ + private hasAnyVersionStorage(): boolean { + const storageKeys = Object.keys(localStorage) + const versionPattern = StorageConfig.createVersionPattern() + + return storageKeys.some((key) => versionPattern.test(key) && localStorage.getItem(key) !== null) + } + + /** + * 获取旧格式的本地存储数据 + */ + private getLegacyStorageData(): Record { + try { + const systemStorage = this.getSystemStorage() + return systemStorage || {} + } catch (error) { + console.warn('[Storage] 解析旧格式存储数据失败:', error) + return {} + } + } + + /** + * 显示存储错误消息 + */ + private showStorageError(): void { + ElMessage({ + type: 'error', + offset: 40, + duration: 5000, + message: '系统检测到本地数据异常,请重新登录系统恢复使用!' + }) + } + + /** + * 执行系统登出 + */ + private performSystemLogout(): void { + setTimeout(() => { + try { + localStorage.clear() + useUserStore().logOut() + router.push({ name: 'Login' }) + console.info('[Storage] 已执行系统登出') + } catch (error) { + console.error('[Storage] 系统登出失败:', error) + } + }, StorageConfig.LOGOUT_DELAY) + } + + /** + * 处理存储异常 + */ + private handleStorageError(): void { + this.showStorageError() + this.performSystemLogout() + } + + /** + * 验证存储数据完整性 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ + validateStorageData(requireAuth: boolean = false): boolean { + try { + // 优先检查新版本存储结构 + if (this.hasCurrentVersionStorage()) { + // console.debug('[Storage] 发现当前版本存储数据') + return true + } + + // 检查是否有任何版本的存储数据 + if (this.hasAnyVersionStorage()) { + // console.debug('[Storage] 发现其他版本存储数据,可能需要迁移') + return true + } + + // 检查旧版本存储结构 + const legacyData = this.getLegacyStorageData() + if (Object.keys(legacyData).length === 0) { + // 只有在需要验证登录状态时才执行登出操作 + if (requireAuth) { + console.warn('[Storage] 未发现任何存储数据,需要重新登录') + this.performSystemLogout() + return false + } + // 首次访问或访问静态路由,不需要登出 + // console.debug('[Storage] 未发现存储数据,首次访问或访问静态路由') + return true + } + + console.debug('[Storage] 发现旧版本存储数据') + return true + } catch (error) { + console.error('[Storage] 存储数据验证失败:', error) + // 只有在需要验证登录状态时才处理错误 + if (requireAuth) { + this.handleStorageError() + return false + } + return true + } + } + + /** + * 检查存储是否为空 + */ + isStorageEmpty(): boolean { + // 检查新版本存储结构 + if (this.hasCurrentVersionStorage()) { + return false + } + + // 检查是否有任何版本的存储数据 + if (this.hasAnyVersionStorage()) { + return false + } + + // 检查旧版本存储结构 + const legacyData = this.getLegacyStorageData() + return Object.keys(legacyData).length === 0 + } + + /** + * 检查存储兼容性 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ + checkCompatibility(requireAuth: boolean = false): boolean { + try { + const isValid = this.validateStorageData(requireAuth) + const isEmpty = this.isStorageEmpty() + + if (isValid || isEmpty) { + // console.debug('[Storage] 存储兼容性检查通过') + return true + } + + console.warn('[Storage] 存储兼容性检查失败') + return false + } catch (error) { + console.error('[Storage] 兼容性检查异常:', error) + return false + } + } +} + +// 创建存储兼容性管理器实例 +const storageManager = new StorageCompatibilityManager() + +/** + * 获取系统存储数据 + */ +export function getSystemStorage(): any { + return storageManager.getSystemStorage() +} + +/** + * 获取系统版本号 + */ +export function getSysVersion(): string | null { + return storageManager.getSystemVersion() +} + +/** + * 验证本地存储数据 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ +export function validateStorageData(requireAuth: boolean = false): boolean { + return storageManager.validateStorageData(requireAuth) +} + +/** + * 检查存储兼容性 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ +export function checkStorageCompatibility(requireAuth: boolean = false): boolean { + return storageManager.checkCompatibility(requireAuth) +} diff --git a/admin/src/utils/sys/console.ts b/admin/src/utils/sys/console.ts new file mode 100644 index 0000000..d2a1344 --- /dev/null +++ b/admin/src/utils/sys/console.ts @@ -0,0 +1,9 @@ +// 控制台欢迎信息(all-kefu 品牌位,替换模板自推广内容) +const asciiArt = ` +\x1b[32m欢迎使用 all-kefu 管理后台! +\x1b[0m +\x1b[36m多租户 AI 客服平台控制台(基于 art-design-pro 模板构建) +\x1b[0m +` + +console.log(asciiArt) diff --git a/admin/src/utils/sys/error-handle.ts b/admin/src/utils/sys/error-handle.ts new file mode 100644 index 0000000..bc966c9 --- /dev/null +++ b/admin/src/utils/sys/error-handle.ts @@ -0,0 +1,143 @@ +/** + * 全局错误处理模块 + * + * 提供统一的错误捕获和处理机制 + * + * ## 主要功能 + * + * - Vue 运行时错误捕获(组件错误、生命周期错误等) + * - 全局脚本错误捕获(语法错误、运行时错误等) + * - Promise 未捕获错误处理(unhandledrejection) + * - 静态资源加载错误监控(图片、脚本、样式等) + * - 错误日志记录和上报 + * - 统一的错误处理入口 + * + * ## 使用场景 + * - 应用启动时安装全局错误处理器 + * - 捕获和记录所有类型的错误 + * - 错误上报到监控平台 + * - 提升应用稳定性和可维护性 + * - 问题排查和调试 + * + * ## 错误类型 + * + * - VueError: Vue 组件相关错误 + * - ScriptError: JavaScript 脚本错误 + * - PromiseError: Promise 未捕获的 rejection + * - ResourceError: 静态资源加载失败 + * + * @module utils/sys/error-handle + * @author Art Design Pro Team + */ +import type { App } from 'vue' + +const IGNORABLE_SCRIPT_ERRORS = [ + 'ResizeObserver loop completed with undelivered notifications.', + 'ResizeObserver loop limit exceeded' +] + +function normalizeErrorMessage(message: Event | string): string { + if (typeof message === 'string') { + return message + } + + if ('message' in message && typeof message.message === 'string') { + return message.message + } + + return '' +} + +function isIgnorableScriptError(message: Event | string, source?: string): boolean { + const normalizedMessage = normalizeErrorMessage(message) + + if (!normalizedMessage) { + return false + } + + if (IGNORABLE_SCRIPT_ERRORS.some((item) => normalizedMessage.includes(item))) { + // 浏览器/扩展在布局抖动时常见的 ResizeObserver 噪声,不作为真实异常处理 + return true + } + + // 浏览器扩展注入脚本偶发的跨域 Script error 也没有排查价值 + if (normalizedMessage === 'Script error.' && source === '') { + return true + } + + return false +} + +/** + * Vue 运行时错误处理 + */ +export function vueErrorHandler(err: unknown, instance: any, info: string) { + console.error('[VueError]', err, info, instance) + // 这里可以上报到服务端,比如: + // reportError({ type: 'vue', err, info }) +} + +/** + * 全局脚本错误处理 + */ +export function scriptErrorHandler( + message: Event | string, + source?: string, + lineno?: number, + colno?: number, + error?: Error +): boolean { + if (isIgnorableScriptError(message, source)) { + return true + } + + console.error('[ScriptError]', { message, source, lineno, colno, error }) + // reportError({ type: 'script', message, source, lineno, colno, error }) + return true // 阻止默认控制台报错,可根据需求改 +} + +/** + * Promise 未捕获错误处理 + */ +export function registerPromiseErrorHandler() { + window.addEventListener('unhandledrejection', (event) => { + console.error('[PromiseError]', event.reason) + // reportError({ type: 'promise', reason: event.reason }) + }) +} + +/** + * 资源加载错误处理 (img, script, css...) + */ +export function registerResourceErrorHandler() { + window.addEventListener( + 'error', + (event: Event) => { + const target = event.target as HTMLElement + if ( + target && + (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK') + ) { + console.error('[ResourceError]', { + tagName: target.tagName, + src: + (target as HTMLImageElement).src || + (target as HTMLScriptElement).src || + (target as HTMLLinkElement).href + }) + // reportError({ type: 'resource', target }) + } + }, + true // 捕获阶段才能监听到资源错误 + ) +} + +/** + * 安装统一错误处理 + */ +export function setupErrorHandle(app: App) { + app.config.errorHandler = vueErrorHandler + window.onerror = scriptErrorHandler + registerPromiseErrorHandler() + registerResourceErrorHandler() +} diff --git a/admin/src/utils/sys/index.ts b/admin/src/utils/sys/index.ts new file mode 100644 index 0000000..cc30e31 --- /dev/null +++ b/admin/src/utils/sys/index.ts @@ -0,0 +1,5 @@ +/** + * 系统管理相关工具函数统一导出 + */ + +export { default as mittBus } from './mittBus' diff --git a/admin/src/utils/sys/mittBus.ts b/admin/src/utils/sys/mittBus.ts new file mode 100644 index 0000000..22f0108 --- /dev/null +++ b/admin/src/utils/sys/mittBus.ts @@ -0,0 +1,63 @@ +/** + * 全局事件总线模块 + * + * 基于 mitt 库实现的类型安全的事件总线 + * + * ## 主要功能 + * + * - 跨组件通信(发布/订阅模式) + * - 类型安全的事件定义和调用 + * - 全局事件管理(烟花效果、设置面板、搜索对话框等) + * - 解耦组件间的直接依赖 + * + * ## 使用场景 + * + * - 跨层级组件通信 + * - 全局功能触发(设置、搜索、聊天、锁屏等) + * - 特效触发(烟花效果) + * - 避免 props 层层传递 + * + * ## 用法示例 + * + * ```typescript + * // 订阅事件 + * mittBus.on('openSetting', () => { ... }) + * + * // 发布事件 + * mittBus.emit('openSetting') + * + * // 带参数的事件 + * mittBus.emit('triggerFireworks', 'image-url') + * ``` + * + * ## 已定义的事件 + * + * - triggerFireworks: 触发烟花效果(可选图片URL) + * - openSetting: 打开设置面板 + * - openSearchDialog: 打开搜索对话框 + * - openChat: 打开聊天窗口 + * - openLockScreen: 打开锁屏 + * + * @module utils/sys/mittBus + * @author Art Design Pro Team + */ +import mitt, { type Emitter } from 'mitt' + +// 定义事件类型映射 +type Events = { + // 烟花效果事件 - 可选的图片URL参数 + triggerFireworks: string | undefined + // 打开设置面板事件 - 无参数 + openSetting: void + // 打开搜索对话框事件 - 无参数 + openSearchDialog: void + // 打开聊天窗口事件 - 无参数 + openChat: void + // 打开锁屏事件 - 无参数 + openLockScreen: void +} + +// 创建类型安全的事件总线实例 +const mittBus: Emitter = mitt() + +export default mittBus diff --git a/admin/src/utils/table/tableCache.ts b/admin/src/utils/table/tableCache.ts new file mode 100644 index 0000000..045a7ce --- /dev/null +++ b/admin/src/utils/table/tableCache.ts @@ -0,0 +1,266 @@ +/** + * 表格缓存管理模块 + * + * 提供高性能的表格数据缓存机制 + * + * ## 主要功能 + * + * - 基于参数的智能缓存键生成(使用 ohash) + * - LRU(最近最少使用)缓存淘汰策略 + * - 缓存过期时间管理 + * - 缓存大小限制和自动清理 + * - 基于标签的缓存分组管理 + * - 多种缓存失效策略(清空所有、清空当前、清空分页等) + * - 缓存访问统计和命中率分析 + * - 缓存大小估算 + * + * ## 使用场景 + * + * - 表格数据的分页缓存 + * - 减少重复的 API 请求 + * - 提升表格切换和返回的响应速度 + * - 搜索条件变化时的智能缓存管理 + * - 数据更新后的缓存失效处理 + * + * ## 缓存策略 + * + * - CLEAR_ALL: 清空所有缓存(适用于全局数据更新) + * - CLEAR_CURRENT: 仅清空当前查询条件的缓存(适用于单条数据更新) + * - CLEAR_PAGINATION: 清空所有分页缓存但保留不同搜索条件(适用于批量操作) + * - KEEP_ALL: 不清除缓存(适用于只读操作) + * + * @module utils/table/tableCache + * @author Art Design Pro Team + */ +import { hash } from 'ohash' + +// 缓存失效策略枚举 +export enum CacheInvalidationStrategy { + /** 清空所有缓存 */ + CLEAR_ALL = 'clear_all', + /** 仅清空当前查询条件的缓存 */ + CLEAR_CURRENT = 'clear_current', + /** 清空所有分页缓存(保留不同搜索条件的缓存) */ + CLEAR_PAGINATION = 'clear_pagination', + /** 不清除缓存 */ + KEEP_ALL = 'keep_all' +} + +// 通用 API 响应接口(兼容不同的后端响应格式) +export interface ApiResponse { + records?: T[] + data?: T[] + total?: number + current?: number + size?: number + [key: string]: unknown +} + +// 缓存存储接口 +export interface CacheItem { + data: T[] + response: ApiResponse + timestamp: number + params: string + // 缓存标签,用于分组管理 + tags: Set + // 访问次数(用于 LRU 算法) + accessCount: number + // 最后访问时间 + lastAccessTime: number +} + +// 增强的缓存管理类 +export class TableCache { + private cache = new Map>() + private cacheTime: number + private maxSize: number + private enableLog: boolean + + constructor(cacheTime = 5 * 60 * 1000, maxSize = 50, enableLog = false) { + // 默认5分钟,最多50条缓存 + this.cacheTime = cacheTime + this.maxSize = maxSize + this.enableLog = enableLog + } + + // 内部日志工具 + private log(message: string, ...args: any[]) { + if (this.enableLog) { + console.log(`[TableCache] ${message}`, ...args) + } + } + + // 生成稳定的缓存键 + private generateKey(params: unknown): string { + return hash(params) + } + + // 🔧 优化:增强类型安全性 + private generateTags(params: Record): Set { + const tags = new Set() + + // 添加搜索条件标签 + const searchKeys = Object.keys(params).filter( + (key) => + !['current', 'size', 'total'].includes(key) && + params[key] !== undefined && + params[key] !== '' && + params[key] !== null + ) + + if (searchKeys.length > 0) { + const searchTag = searchKeys.map((key) => `${key}:${String(params[key])}`).join('|') + tags.add(`search:${searchTag}`) + } else { + tags.add('search:default') + } + + // 添加分页标签 + tags.add(`pagination:${params.size || 10}`) + // 添加通用分页标签,用于清理所有分页缓存 + tags.add('pagination') + + return tags + } + + // 🔧 优化:LRU 缓存清理 + private evictLRU(): void { + if (this.cache.size <= this.maxSize) return + + // 找到最少使用的缓存项 + let lruKey = '' + let minAccessCount = Infinity + let oldestTime = Infinity + + for (const [key, item] of this.cache.entries()) { + if ( + item.accessCount < minAccessCount || + (item.accessCount === minAccessCount && item.lastAccessTime < oldestTime) + ) { + lruKey = key + minAccessCount = item.accessCount + oldestTime = item.lastAccessTime + } + } + + if (lruKey) { + this.cache.delete(lruKey) + this.log(`LRU 清理缓存: ${lruKey}`) + } + } + + // 设置缓存 + set(params: unknown, data: T[], response: ApiResponse): void { + const key = this.generateKey(params) + const tags = this.generateTags(params as Record) + const now = Date.now() + + // 检查是否需要清理 + this.evictLRU() + + this.cache.set(key, { + data, + response, + timestamp: now, + params: key, + tags, + accessCount: 1, + lastAccessTime: now + }) + } + + // 获取缓存 + get(params: unknown): CacheItem | null { + const key = this.generateKey(params) + const item = this.cache.get(key) + + if (!item) return null + + // 检查是否过期 + if (Date.now() - item.timestamp > this.cacheTime) { + this.cache.delete(key) + return null + } + + // 更新访问统计 + item.accessCount++ + item.lastAccessTime = Date.now() + + return item + } + + // 根据标签清除缓存 + clearByTags(tags: string[]): number { + let clearedCount = 0 + + for (const [key, item] of this.cache.entries()) { + // 检查是否包含任意一个标签 + const hasMatchingTag = tags.some((tag) => + Array.from(item.tags).some((itemTag) => itemTag.includes(tag)) + ) + + if (hasMatchingTag) { + this.cache.delete(key) + clearedCount++ + } + } + + return clearedCount + } + + // 清除当前搜索条件的缓存 + clearCurrentSearch(params: unknown): number { + const key = this.generateKey(params) + const deleted = this.cache.delete(key) + return deleted ? 1 : 0 + } + + // 清除分页缓存 + clearPagination(): number { + return this.clearByTags(['pagination']) + } + + // 清空所有缓存 + clear(): void { + this.cache.clear() + } + + // 获取缓存统计信息 + getStats(): { total: number; size: string; hitRate: string } { + const total = this.cache.size + let totalSize = 0 + let totalAccess = 0 + + for (const item of this.cache.values()) { + // 粗略估算大小(JSON字符串长度) + totalSize += JSON.stringify(item.data).length + totalAccess += item.accessCount + } + + // 转换为人类可读的大小 + const sizeInKB = (totalSize / 1024).toFixed(2) + const avgHits = total > 0 ? (totalAccess / total).toFixed(1) : '0' + + return { + total, + size: `${sizeInKB}KB`, + hitRate: `${avgHits} avg hits` + } + } + + // 清理过期缓存 + cleanupExpired(): number { + let cleanedCount = 0 + const now = Date.now() + + for (const [key, item] of this.cache.entries()) { + if (now - item.timestamp > this.cacheTime) { + this.cache.delete(key) + cleanedCount++ + } + } + + return cleanedCount + } +} diff --git a/admin/src/utils/table/tableConfig.ts b/admin/src/utils/table/tableConfig.ts new file mode 100644 index 0000000..e464c89 --- /dev/null +++ b/admin/src/utils/table/tableConfig.ts @@ -0,0 +1,55 @@ +/** + * 表格全局配置模块 + * + * 提供表格与后端接口的字段映射配置 + * + * ## 主要功能 + * + * - 响应数据字段自动识别和映射 + * - 支持多种常见的后端响应格式 + * - 请求参数字段映射配置 + * - 可扩展的字段配置机制 + * + * ## 使用场景 + * + * - 适配不同后端的分页接口格式 + * - 统一前端表格组件的数据处理 + * - 减少重复的数据转换代码 + * - 支持多个后端服务的接口对接 + * + * ## 配置说明 + * + * - recordFields: 列表数据字段名(按优先级顺序查找) + * - totalFields: 总条数字段名 + * - currentFields: 当前页码字段名 + * - sizeFields: 每页大小字段名 + * - paginationKey: 前端发送请求时使用的分页参数名 + * + * ## 扩展方式 + * + * 如果后端使用其他字段名,可以在对应数组中添加新的字段名 + * 例如:recordFields: ['list', 'data', 'records', 'items', 'yourCustomField'] + * + * @module utils/table/tableConfig + * @author Art Design Pro Team + */ +export const tableConfig = { + // 响应数据字段映射配置,系统会从接口返回数据中按顺序查找这些字段 + // 列表数据 + recordFields: ['list', 'data', 'records', 'items', 'result', 'rows'], + // 总条数 + totalFields: ['total', 'count'], + // 当前页码 + currentFields: ['current', 'page', 'pageNum'], + // 每页大小 + sizeFields: ['size', 'pageSize', 'limit'], + + // 请求参数映射配置,前端发送请求时使用的分页参数名 + // useTable 组合式函数传递分页参数的时候 用 current 跟 size + paginationKey: { + // 当前页码 + current: 'current', + // 每页大小 + size: 'size' + } +} diff --git a/admin/src/utils/table/tableUtils.ts b/admin/src/utils/table/tableUtils.ts new file mode 100644 index 0000000..3ca9db1 --- /dev/null +++ b/admin/src/utils/table/tableUtils.ts @@ -0,0 +1,297 @@ +/** + * 表格工具函数模块 + * + * 提供表格数据处理和请求管理的核心工具函数 + * + * ## 主要功能 + * + * - 多格式 API 响应自动适配和标准化 + * - 表格数据提取和转换 + * - 分页信息自动更新和校验 + * - 智能防抖函数(支持取消和立即执行) + * - 统一的错误处理机制 + * - 嵌套数据结构解析 + * + * ## 使用场景 + * + * - useTable 组合式函数的底层工具 + * - 适配各种后端接口响应格式 + * - 表格数据的标准化处理 + * - 请求防抖和性能优化 + * - 错误统一处理和日志记录 + * + * ## 支持的响应格式 + * + * 1. 直接数组: [item1, item2, ...] + * 2. 标准对象: { records: [], total: 100 } + * 3. 嵌套data: { data: { list: [], total: 100 } } + * 4. 多种字段名: list/data/records/items/result/rows + * + * ## 核心功能 + * + * - defaultResponseAdapter: 智能识别和转换响应格式 + * - extractTableData: 提取表格数据数组 + * - updatePaginationFromResponse: 更新分页信息 + * - createSmartDebounce: 创建可控的防抖函数 + * - createErrorHandler: 生成错误处理器 + * + * @module utils/table/tableUtils + * @author Art Design Pro Team + */ + +import type { ApiResponse } from './tableCache' +import { tableConfig } from './tableConfig' + +// 请求参数基础接口,扩展分页参数 +export interface BaseRequestParams extends Api.Common.PaginationParams { + [key: string]: unknown +} + +// 错误处理接口 +export interface TableError { + code: string + message: string + details?: unknown +} + +// 辅助函数:从对象中提取记录数组 +function extractRecords(obj: Record, fields: string[]): T[] { + for (const field of fields) { + if (field in obj && Array.isArray(obj[field])) { + return obj[field] as T[] + } + } + return [] +} + +// 辅助函数:从对象中提取总数 +function extractTotal(obj: Record, records: unknown[], fields: string[]): number { + for (const field of fields) { + if (field in obj && typeof obj[field] === 'number') { + return obj[field] as number + } + } + return records.length +} + +// 辅助函数:提取分页参数 +function extractPagination( + obj: Record, + data?: Record +): Pick, 'current' | 'size'> | undefined { + const result: Partial, 'current' | 'size'>> = {} + const sources = [obj, data ?? {}] + + const currentFields = tableConfig.currentFields + for (const src of sources) { + for (const field of currentFields) { + if (field in src && typeof src[field] === 'number') { + result.current = src[field] as number + break + } + } + if (result.current !== undefined) break + } + + const sizeFields = tableConfig.sizeFields + for (const src of sources) { + for (const field of sizeFields) { + if (field in src && typeof src[field] === 'number') { + result.size = src[field] as number + break + } + } + if (result.size !== undefined) break + } + + if (result.current === undefined && result.size === undefined) return undefined + return result +} + +/** + * 默认响应适配器 - 支持多种常见的API响应格式 + */ +export const defaultResponseAdapter = (response: unknown): ApiResponse => { + // 定义支持的字段 + const recordFields = tableConfig.recordFields + + if (!response) { + return { records: [], total: 0 } + } + + if (Array.isArray(response)) { + return { records: response, total: response.length } + } + + if (typeof response !== 'object') { + console.warn( + '[tableUtils] 无法识别的响应格式,支持的格式包括: 数组、包含' + + recordFields.join('/') + + '字段的对象、嵌套data对象。当前格式:', + response + ) + return { records: [], total: 0 } + } + + const res = response as Record + let records: T[] = [] + let total = 0 + let pagination: Pick, 'current' | 'size'> | undefined + + // 处理标准格式或直接列表 + records = extractRecords(res, recordFields) + total = extractTotal(res, records, tableConfig.totalFields) + pagination = extractPagination(res) + + // 如果没有找到,检查嵌套data + if (records.length === 0 && 'data' in res && typeof res.data === 'object') { + const data = res.data as Record + records = extractRecords(data, ['list', 'records', 'items']) + total = extractTotal(data, records, tableConfig.totalFields) + pagination = extractPagination(res, data) + + if (Array.isArray(res.data)) { + records = res.data as T[] + total = records.length + } + } + + if (!recordFields.some((field) => field in res) && records.length === 0) { + console.warn('[tableUtils] 无法识别的响应格式') + console.warn('支持的字段包括: ' + recordFields.join('、'), response) + console.warn('扩展字段请到 utils/table/tableConfig 文件配置') + } + + const result: ApiResponse = { records, total } + if (pagination) { + Object.assign(result, pagination) + } + return result +} + +/** + * 从标准化的API响应中提取表格数据 + */ +export const extractTableData = (response: ApiResponse): T[] => { + const data = response.records || response.data || [] + return Array.isArray(data) ? data : [] +} + +/** + * 根据API响应更新分页信息 + */ +export const updatePaginationFromResponse = ( + pagination: Api.Common.PaginationParams, + response: ApiResponse +): void => { + pagination.total = response.total ?? pagination.total ?? 0 + + if (response.current !== undefined) { + pagination.current = response.current + } + + const maxPage = Math.max(1, Math.ceil(pagination.total / (pagination.size || 1))) + if (pagination.current > maxPage) { + pagination.current = maxPage + } +} + +/** + * 创建智能防抖函数 - 支持取消和立即执行 + */ +export const createSmartDebounce = Promise>( + fn: T, + delay: number +): T & { cancel: () => void; flush: () => Promise } => { + let timeoutId: NodeJS.Timeout | null = null + let lastArgs: Parameters | null = null + let lastResolve: ((value: any) => void) | null = null + let lastReject: ((reason: any) => void) | null = null + + const debouncedFn = (...args: Parameters): Promise => { + return new Promise((resolve, reject) => { + if (timeoutId) clearTimeout(timeoutId) + lastArgs = args + lastResolve = resolve + lastReject = reject + timeoutId = setTimeout(async () => { + try { + const result = await fn(...args) + resolve(result) + } catch (error) { + reject(error) + } finally { + timeoutId = null + lastArgs = null + lastResolve = null + lastReject = null + } + }, delay) + }) + } + + debouncedFn.cancel = () => { + if (timeoutId) clearTimeout(timeoutId) + timeoutId = null + lastArgs = null + lastResolve = null + lastReject = null + } + + debouncedFn.flush = async () => { + if (timeoutId && lastArgs && lastResolve && lastReject) { + clearTimeout(timeoutId) + timeoutId = null + const args = lastArgs + const resolve = lastResolve + const reject = lastReject + lastArgs = null + lastResolve = null + lastReject = null + try { + const result = await fn(...args) + resolve(result) + return result + } catch (error) { + reject(error) + throw error + } + } + return Promise.resolve() + } + + return debouncedFn as any +} + +/** + * 生成错误处理函数 + */ +export const createErrorHandler = ( + onError?: (error: TableError) => void, + enableLog: boolean = false +) => { + const logger = { + error: (message: string, ...args: any[]) => { + if (enableLog) console.error(`[useTable] ${message}`, ...args) + } + } + + return (err: unknown, context: string): TableError => { + const tableError: TableError = { + code: 'UNKNOWN_ERROR', + message: '未知错误', + details: err + } + + if (err instanceof Error) { + tableError.message = err.message + tableError.code = err.name + } else if (typeof err === 'string') { + tableError.message = err + } + + logger.error(`${context}:`, err) + onError?.(tableError) + return tableError + } +} diff --git a/admin/src/utils/ui/animation.ts b/admin/src/utils/ui/animation.ts new file mode 100644 index 0000000..5efd02a --- /dev/null +++ b/admin/src/utils/ui/animation.ts @@ -0,0 +1,80 @@ +/** + * 主题动画工具模块 + * + * 提供主题切换的视觉动画效果 + * + * ## 主要功能 + * + * - 基于鼠标点击位置的圆形扩散动画 + * - View Transition API 支持(现代浏览器) + * - 降级处理(不支持动画的浏览器) + * - 暗黑主题切换过渡效果 + * - 页面刷新时的主题过渡优化 + * + * ## 使用场景 + * + * - 明暗主题切换 + * - 提升用户体验的视觉反馈 + * - 页面刷新时的平滑过渡 + * + * ## 技术实现 + * + * - 使用 CSS 变量存储点击位置和半径 + * - 利用 View Transition API 实现流畅动画 + * - 通过 CSS class 控制过渡效果 + * - 自动计算最大扩散半径 + * + * @module utils/theme/animation + * @author Art Design Pro Team + */ +import { useCommon } from '@/hooks/core/useCommon' +import { useTheme } from '@/hooks/core/useTheme' +import { SystemThemeEnum } from '@/enums/appEnum' +import { useSettingStore } from '@/store/modules/setting' +const { LIGHT, DARK } = SystemThemeEnum + +/** + * 主题切换动画 + * @param e 鼠标点击事件 + */ +export const themeAnimation = (e: any) => { + const x = e.clientX + const y = e.clientY + // 计算鼠标点击位置距离视窗的最大圆半径 + const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)) + + // 设置CSS变量 + document.documentElement.style.setProperty('--x', x + 'px') + document.documentElement.style.setProperty('--y', y + 'px') + document.documentElement.style.setProperty('--r', endRadius + 'px') + + if (document.startViewTransition) { + document.startViewTransition(() => toggleTheme()) + } else { + toggleTheme() + } +} + +/** + * 切换主题 + */ +const toggleTheme = () => { + useTheme().switchThemeStyles(useSettingStore().systemThemeType === LIGHT ? DARK : LIGHT) + useCommon().refresh() +} + +/** + * 切换主题过渡效果 + * @param enable 是否启用过渡效果 + */ +export const toggleTransition = (enable: boolean) => { + const body = document.body + + if (enable) { + body.classList.add('theme-change') + } else { + setTimeout(() => { + body.classList.remove('theme-change') + }, 300) + } +} diff --git a/admin/src/utils/ui/colors.ts b/admin/src/utils/ui/colors.ts new file mode 100644 index 0000000..b4f6b77 --- /dev/null +++ b/admin/src/utils/ui/colors.ts @@ -0,0 +1,273 @@ +/** + * 颜色处理工具模块 + * + * 提供完整的颜色格式转换和处理功能 + * + * ## 主要功能 + * + * - Hex 与 RGB/RGBA 格式互转 + * - 颜色混合计算 + * - 颜色变浅/变深处理 + * - Element Plus 主题色自动生成 + * - 颜色格式验证 + * - CSS 变量读取 + * - 暗黑模式颜色适配 + * + * ## 使用场景 + * + * - 主题色动态切换 + * - Element Plus 组件主题定制 + * - 颜色渐变生成 + * - 明暗主题颜色计算 + * - 颜色格式标准化 + * + * ## 核心功能 + * + * - hexToRgba: Hex 转 RGBA(支持透明度) + * - hexToRgb: Hex 转 RGB 数组 + * - rgbToHex: RGB 转 Hex + * - colourBlend: 两种颜色混合 + * - getLightColor: 生成变浅的颜色 + * - getDarkColor: 生成变深的颜色 + * - handleElementThemeColor: 处理 Element Plus 主题色 + * - setElementThemeColor: 设置完整的主题色系统 + * + * ## 支持格式 + * + * - Hex: #FFF, #FFFFFF + * - RGB: rgb(255, 255, 255) + * - RGBA: rgba(255, 255, 255, 0.5) + * + * @module utils/ui/colors + * @author Art Design Pro Team + */ +import { useSettingStore } from '@/store/modules/setting' + +/** + * 颜色转换结果接口 + */ +interface RgbaResult { + red: number + green: number + blue: number + rgba: string +} + +/** + * 获取CSS变量值(别名函数) + * @param name CSS变量名 + * @returns CSS变量值 + */ +export function getCssVar(name: string): string { + return getComputedStyle(document.documentElement).getPropertyValue(name) +} + +/** + * 验证hex颜色格式 + * @param hex hex颜色值 + * @returns 是否为有效的hex颜色 + */ +function isValidHexColor(hex: string): boolean { + const cleanHex = hex.trim().replace(/^#/, '') + return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex) +} + +/** + * 验证RGB颜色值 + * @param r 红色值 + * @param g 绿色值 + * @param b 蓝色值 + * @returns 是否为有效的RGB值 + */ +function isValidRgbValue(r: number, g: number, b: number): boolean { + const isValid = (value: number) => Number.isInteger(value) && value >= 0 && value <= 255 + return isValid(r) && isValid(g) && isValid(b) +} + +/** + * 将hex颜色转换为RGBA + * @param hex hex颜色值 (支持 #FFF 或 #FFFFFF 格式) + * @param opacity 透明度 (0-1) + * @returns 包含RGB值和RGBA字符串的对象 + */ +export function hexToRgba(hex: string, opacity: number): RgbaResult { + if (!isValidHexColor(hex)) { + throw new Error('Invalid hex color format') + } + + // 移除可能存在的 # 前缀并转换为大写 + let cleanHex = hex.trim().replace(/^#/, '').toUpperCase() + + // 如果是缩写形式(如 FFF),转换为完整形式 + if (cleanHex.length === 3) { + cleanHex = cleanHex + .split('') + .map((char) => char.repeat(2)) + .join('') + } + + // 解析 RGB 值 + const [red, green, blue] = cleanHex.match(/\w\w/g)!.map((x) => parseInt(x, 16)) + + // 确保 opacity 在有效范围内 + const validOpacity = Math.max(0, Math.min(1, opacity)) + + // 构建 RGBA 字符串 + const rgba = `rgba(${red}, ${green}, ${blue}, ${validOpacity.toFixed(2)})` + + return { red, green, blue, rgba } +} + +/** + * 将hex颜色转换为RGB数组 + * @param hexColor hex颜色值 + * @returns RGB数组 [r, g, b] + */ +export function hexToRgb(hexColor: string): number[] { + if (!isValidHexColor(hexColor)) { + ElMessage.warning('输入错误的hex颜色值') + throw new Error('Invalid hex color format') + } + + const cleanHex = hexColor.replace(/^#/, '') + let hex = cleanHex + + // 处理缩写形式 + if (hex.length === 3) { + hex = hex + .split('') + .map((char) => char.repeat(2)) + .join('') + } + + const hexPairs = hex.match(/../g) + if (!hexPairs) { + throw new Error('Invalid hex color format') + } + + return hexPairs.map((hexPair) => parseInt(hexPair, 16)) +} + +/** + * 将RGB颜色转换为hex + * @param r 红色值 (0-255) + * @param g 绿色值 (0-255) + * @param b 蓝色值 (0-255) + * @returns hex颜色值 + */ +export function rgbToHex(r: number, g: number, b: number): string { + if (!isValidRgbValue(r, g, b)) { + ElMessage.warning('输入错误的RGB颜色值') + throw new Error('Invalid RGB color values') + } + + const toHex = (value: number) => { + const hex = value.toString(16) + return hex.length === 1 ? `0${hex}` : hex + } + + return `#${toHex(r)}${toHex(g)}${toHex(b)}` +} + +/** + * 颜色混合 + * @param color1 第一个颜色 + * @param color2 第二个颜色 + * @param ratio 混合比例 (0-1) + * @returns 混合后的颜色 + */ +export function colourBlend(color1: string, color2: string, ratio: number): string { + const validRatio = Math.max(0, Math.min(1, Number(ratio))) + + const rgb1 = hexToRgb(color1) + const rgb2 = hexToRgb(color2) + + const blendedRgb = rgb1.map((value1, index) => { + const value2 = rgb2[index] + return Math.round(value1 * (1 - validRatio) + value2 * validRatio) + }) + + return rgbToHex(blendedRgb[0], blendedRgb[1], blendedRgb[2]) +} + +/** + * 获取变浅的颜色 + * @param color 原始颜色 + * @param level 变浅程度 (0-1) + * @param isDark 是否为暗色主题 + * @returns 变浅后的颜色 + */ +export function getLightColor(color: string, level: number, isDark: boolean = false): string { + if (!isValidHexColor(color)) { + ElMessage.warning('输入错误的hex颜色值') + throw new Error('Invalid hex color format') + } + + if (isDark) { + return getDarkColor(color, level) + } + + const rgb = hexToRgb(color) + const lightRgb = rgb.map((value) => Math.floor((255 - value) * level + value)) + + return rgbToHex(lightRgb[0], lightRgb[1], lightRgb[2]) +} + +/** + * 获取变深的颜色 + * @param color 原始颜色 + * @param level 变深程度 (0-1) + * @returns 变深后的颜色 + */ +export function getDarkColor(color: string, level: number): string { + if (!isValidHexColor(color)) { + ElMessage.warning('输入错误的hex颜色值') + throw new Error('Invalid hex color format') + } + + const rgb = hexToRgb(color) + const darkRgb = rgb.map((value) => Math.floor(value * (1 - level))) + + return rgbToHex(darkRgb[0], darkRgb[1], darkRgb[2]) +} + +/** + * 处理 Element Plus 主题颜色 + * @param theme 主题颜色 + * @param isDark 是否为暗色主题 + */ +export function handleElementThemeColor(theme: string, isDark: boolean = false): void { + document.documentElement.style.setProperty('--el-color-primary', theme) + + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty( + `--el-color-primary-light-${i}`, + getLightColor(theme, i / 10, isDark) + ) + } + + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty( + `--el-color-primary-dark-${i}`, + getDarkColor(theme, i / 10) + ) + } +} + +/** + * 设置 Element Plus 主题颜色 + * @param color 主题颜色 + */ +export function setElementThemeColor(color: string): void { + const mixColor = '#ffffff' + const elStyle = document.documentElement.style + + elStyle.setProperty('--el-color-primary', color) + handleElementThemeColor(color, useSettingStore().isDark) + + // 生成更淡一点的颜色 + for (let i = 1; i < 16; i++) { + const itemColor = colourBlend(color, mixColor, i / 16) + elStyle.setProperty(`--el-color-primary-custom-${i}`, itemColor) + } +} diff --git a/admin/src/utils/ui/emojo.ts b/admin/src/utils/ui/emojo.ts new file mode 100644 index 0000000..cabad7d --- /dev/null +++ b/admin/src/utils/ui/emojo.ts @@ -0,0 +1,24 @@ +/** + * 表情 + * 用于在消息提示的时候显示对应的表情 + * + * 用法 + * ElMessage.success(`${EmojiText[200]} 图片上传成功`) + * ElMessage.error(`${EmojiText[400]} 图片上传失败`) + * ElMessage.error(`${EmojiText[500]} 图片上传失败`) + * + * @module utils/ui/emojo + * @author Art Design Pro Team + */ + +// macos 用户 按 shift + 6 可以唤出更多表情…… +const EmojiText: { [key: string]: string } = { + '0': 'O_O', // 空 + '200': '^_^', // 成功 + '400': 'T_T', // 错误请求 + '500': 'X_X' // 服务器内部错误,无法完成请求 +} + +// const EmojiIcon = ['🟢', '🔴', '🟡 ', '🚀', '✨', '💡', '🛠️', '🔥', '🎉', '🌟', '🌈'] + +export default EmojiText diff --git a/admin/src/utils/ui/iconify-loader.ts b/admin/src/utils/ui/iconify-loader.ts new file mode 100644 index 0000000..035de16 --- /dev/null +++ b/admin/src/utils/ui/iconify-loader.ts @@ -0,0 +1,31 @@ +/** + * 离线图标加载器 + * + * 用于在内网环境下支持 Iconify 图标的离线加载。 + * 通过预加载图标集数据,避免运行时从 CDN 获取图标。 + * + * 使用方式: + * 1. 安装所需图标集:pnpm add -D @iconify-json/[icon-set-name] + * 2. 在此文件中导入并注册图标集 + * 3. 在组件中使用: + * + * @module utils/ui/iconify-loader + * @author Art Design Pro Team + */ + +// import { addCollection } from '@iconify/vue' + +// // 导入离线图标数据 + +// // 系统必要图标库 +// import riIcons from '@iconify-json/ri/icons.json' + +// // 演示图标库(可选,生产环境可移除) +// import svgSpinners from '@iconify-json/svg-spinners/icons.json' +// import lineMd from '@iconify-json/line-md/icons.json' + +// // 注册离线图标集 + +// addCollection(riIcons) +// addCollection(svgSpinners) +// addCollection(lineMd) diff --git a/admin/src/utils/ui/index.ts b/admin/src/utils/ui/index.ts new file mode 100644 index 0000000..9ca1049 --- /dev/null +++ b/admin/src/utils/ui/index.ts @@ -0,0 +1,11 @@ +/** + * UI 相关工具函数统一导出 + * + * @module utils/ui/index + * @author Art Design Pro Team + */ + +export * from './colors' +export * from './loading' +export * from './tabs' +export * from './emojo' diff --git a/admin/src/utils/ui/loading.ts b/admin/src/utils/ui/loading.ts new file mode 100644 index 0000000..6580e02 --- /dev/null +++ b/admin/src/utils/ui/loading.ts @@ -0,0 +1,84 @@ +/** + * 全局 Loading 加载管理模块 + * + * 提供统一的全屏加载动画管理 + * + * ## 主要功能 + * + * - 全屏 Loading 显示和隐藏 + * - 自动适配明暗主题背景色 + * - 自定义 SVG 加载动画 + * - 单例模式防止重复创建 + * - 锁定页面交互 + * + * ## 使用场景 + * + * - 页面初始化加载 + * - 大量数据请求 + * - 路由切换过渡 + * - 异步操作等待 + * + * ## 特性 + * + * - 自动检测当前主题并应用对应背景色 + * - 使用自定义 SVG 动画(四点旋转) + * - 单例模式确保同时只有一个 Loading + * - 提供便捷的显示/隐藏方法 + * + * @module utils/ui/loading + * @author Art Design Pro Team + */ +import { fourDotsSpinnerSvg } from '@/assets/svg/loading' + +/** + * 获取当前主题对应的loading背景色 + * @returns 背景色字符串 + */ +const getLoadingBackground = (): string => { + const isDark = document.documentElement.classList.contains('dark') + return isDark ? 'rgba(7, 7, 7, 0.85)' : '#fff' +} + +const DEFAULT_LOADING_CONFIG = { + lock: true, + get background() { + return getLoadingBackground() + }, + svg: fourDotsSpinnerSvg, + svgViewBox: '0 0 40 40', + customClass: 'art-loading-fix' +} as const + +interface LoadingInstance { + close: () => void +} + +let loadingInstance: LoadingInstance | null = null + +export const loadingService = { + /** + * 显示 loading + * @returns 关闭 loading 的函数 + */ + showLoading(): () => void { + if (!loadingInstance) { + // 每次显示时获取最新的配置,确保背景色与当前主题同步 + const config = { + ...DEFAULT_LOADING_CONFIG, + background: getLoadingBackground() + } + loadingInstance = ElLoading.service(config) + } + return () => this.hideLoading() + }, + + /** + * 隐藏 loading + */ + hideLoading(): void { + if (loadingInstance) { + loadingInstance.close() + loadingInstance = null + } + } +} diff --git a/admin/src/utils/ui/tabs.ts b/admin/src/utils/ui/tabs.ts new file mode 100644 index 0000000..5f53ea5 --- /dev/null +++ b/admin/src/utils/ui/tabs.ts @@ -0,0 +1,60 @@ +/** + * 标签页布局配置模块 + * + * 提供不同标签页样式的高度和间距配置 + * + * ## 主要功能 + * + * - 多种标签页样式配置(默认、卡片、谷歌风格) + * - 标签页打开/关闭状态的高度管理 + * - 顶部间距自动计算 + * - 配置获取和默认值处理 + * + * ## 使用场景 + * + * - 工作标签页(Worktab)布局计算 + * - 页面内容区域高度调整 + * - 标签页显示/隐藏时的动画 + * - 响应式布局适配 + * + * ## 配置项说明 + * + * - openTop: 标签页显示时,内容区域距离顶部的距离 + * - closeTop: 标签页隐藏时,内容区域距离顶部的距离 + * - openHeight: 标签页显示时的总高度(包含标签栏) + * - closeHeight: 标签页隐藏时的总高度(仅头部) + * + * ## 支持的样式 + * + * - tab-default: 默认标签页样式 + * - tab-card: 卡片式标签页 + * - tab-google: 谷歌浏览器风格标签页 + * + * @module utils/ui/tabs + * @author Art Design Pro Team + */ +export const TAB_CONFIG = { + 'tab-default': { + openTop: 106, + closeTop: 60, + openHeight: 121, + closeHeight: 75 + }, + 'tab-card': { + openTop: 122, + closeTop: 78, + openHeight: 139, + closeHeight: 95 + }, + 'tab-google': { + openTop: 122, + closeTop: 78, + openHeight: 139, + closeHeight: 95 + } +} + +// 获取当前 tab 样式配置,设置默认值 +export const getTabConfig = (style: string) => { + return TAB_CONFIG[style as keyof typeof TAB_CONFIG] || TAB_CONFIG['tab-card'] // 默认使用 tab-card 配置 +} diff --git a/admin/src/views/auth/force-change-password/index.vue b/admin/src/views/auth/force-change-password/index.vue new file mode 100644 index 0000000..aa9910d --- /dev/null +++ b/admin/src/views/auth/force-change-password/index.vue @@ -0,0 +1,95 @@ + + + + + + diff --git a/admin/src/views/auth/login/index.vue b/admin/src/views/auth/login/index.vue new file mode 100644 index 0000000..f0eaa62 --- /dev/null +++ b/admin/src/views/auth/login/index.vue @@ -0,0 +1,221 @@ + + + + + + + + diff --git a/admin/src/views/auth/login/style.css b/admin/src/views/auth/login/style.css new file mode 100644 index 0000000..bd8c3a9 --- /dev/null +++ b/admin/src/views/auth/login/style.css @@ -0,0 +1,38 @@ +@reference '@styles/core/tailwind.css'; + +/* 授权页右侧区域 */ +.auth-right-wrap { + @apply absolute inset-0 w-[440px] h-[650px] py-[5px] m-auto overflow-hidden + max-sm:px-7 max-sm:w-full + animate-[slideInRight_0.6s_cubic-bezier(0.25,0.46,0.45,0.94)_forwards] + max-md:animate-none; + + .form { + @apply h-full py-[40px]; + } + + .title { + @apply text-g-900 text-4xl font-semibold max-md:text-3xl max-sm:pt-10; + } + + .sub-title { + @apply mt-[10px] text-g-600 text-sm; + } + + .custom-height { + @apply !h-[40px]; + } +} + +/* 滑入动画 */ +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(30px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} diff --git a/admin/src/views/config/center/index.vue b/admin/src/views/config/center/index.vue new file mode 100644 index 0000000..0814069 --- /dev/null +++ b/admin/src/views/config/center/index.vue @@ -0,0 +1,25 @@ + + + + + + diff --git a/admin/src/views/conversations/list/index.vue b/admin/src/views/conversations/list/index.vue new file mode 100644 index 0000000..6795d15 --- /dev/null +++ b/admin/src/views/conversations/list/index.vue @@ -0,0 +1,25 @@ + + + + + + diff --git a/admin/src/views/dashboard/console/index.vue b/admin/src/views/dashboard/console/index.vue new file mode 100644 index 0000000..43b3368 --- /dev/null +++ b/admin/src/views/dashboard/console/index.vue @@ -0,0 +1,25 @@ + + + + + + diff --git a/admin/src/views/exception/403/index.vue b/admin/src/views/exception/403/index.vue new file mode 100644 index 0000000..2756c42 --- /dev/null +++ b/admin/src/views/exception/403/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/admin/src/views/exception/404/index.vue b/admin/src/views/exception/404/index.vue new file mode 100644 index 0000000..6b64f45 --- /dev/null +++ b/admin/src/views/exception/404/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/admin/src/views/exception/500/index.vue b/admin/src/views/exception/500/index.vue new file mode 100644 index 0000000..1b26377 --- /dev/null +++ b/admin/src/views/exception/500/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/admin/src/views/index/index.vue b/admin/src/views/index/index.vue new file mode 100644 index 0000000..415a436 --- /dev/null +++ b/admin/src/views/index/index.vue @@ -0,0 +1,29 @@ + + + + + + diff --git a/admin/src/views/index/style.scss b/admin/src/views/index/style.scss new file mode 100644 index 0000000..c89f354 --- /dev/null +++ b/admin/src/views/index/style.scss @@ -0,0 +1,93 @@ +.app-layout { + display: flex; + width: 100%; + min-height: 100vh; + background: var(--default-bg-color); + + #app-sidebar { + flex-shrink: 0; + } + + #app-main { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + height: 100vh; + overflow: auto; + + #app-header { + position: sticky; + top: 0; + z-index: 50; + flex-shrink: 0; + width: 100%; + } + + #app-content { + flex: 1; + + :deep(.layout-content) { + box-sizing: border-box; + width: calc(100% - 40px); + margin: auto; + + // 子页面默认 style + .page-content { + position: relative; + box-sizing: border-box; + padding: 20px; + overflow: hidden; + background: var(--default-box-color); + border-radius: calc(var(--custom-radius) / 2 + 2px) !important; + } + } + } + } +} + +@media only screen and (width <= 1180px) { + .app-layout { + #app-main { + height: 100dvh; + } + } +} + +@media only screen and (width <= 800px) { + .app-layout { + position: relative; + + #app-sidebar { + position: fixed; + top: 0; + left: 0; + z-index: 300; + height: 100vh; + } + + #app-main { + width: 100%; + height: auto; + overflow: visible; + + #app-content { + :deep(.layout-content) { + width: calc(100% - 40px); + } + } + } + } +} + +@media only screen and (width <= 640px) { + .app-layout { + #app-main { + #app-content { + :deep(.layout-content) { + width: calc(100% - 30px); + } + } + } + } +} diff --git a/admin/src/views/outside/Iframe.vue b/admin/src/views/outside/Iframe.vue new file mode 100644 index 0000000..33ea0dc --- /dev/null +++ b/admin/src/views/outside/Iframe.vue @@ -0,0 +1,42 @@ + + + diff --git a/admin/src/views/settings/platform/index.vue b/admin/src/views/settings/platform/index.vue new file mode 100644 index 0000000..6268b9c --- /dev/null +++ b/admin/src/views/settings/platform/index.vue @@ -0,0 +1,25 @@ + + + + + + diff --git a/admin/src/views/stats/overview/index.vue b/admin/src/views/stats/overview/index.vue new file mode 100644 index 0000000..01ede20 --- /dev/null +++ b/admin/src/views/stats/overview/index.vue @@ -0,0 +1,25 @@ + + + + + + diff --git a/admin/src/views/tenants/list/index.vue b/admin/src/views/tenants/list/index.vue new file mode 100644 index 0000000..91f2993 --- /dev/null +++ b/admin/src/views/tenants/list/index.vue @@ -0,0 +1,25 @@ + + + + + + diff --git a/admin/tsconfig.json b/admin/tsconfig.json new file mode 100644 index 0000000..4331962 --- /dev/null +++ b/admin/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "types": ["vite/client", "node", "element-plus/global"], + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@views/*": ["src/views/*"], + "@imgs/*": ["src/assets/images/*"], + "@icons/*": ["src/assets/icons/*"], + "@utils/*": ["src/utils/*"], + "@stores/*": ["src/store/*"], + "@plugins/*": ["src/plugins/*"], + "@styles/*": ["src/assets/styles/*"] + } + }, + "include": ["src/**/*", "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "exclude": ["node_modules", "dist", "**/*.js"] +} diff --git a/admin/vite.config.ts b/admin/vite.config.ts new file mode 100644 index 0000000..a5c299c --- /dev/null +++ b/admin/vite.config.ts @@ -0,0 +1,175 @@ +import { defineConfig, loadEnv } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' +import { fileURLToPath } from 'url' +import vueDevTools from 'vite-plugin-vue-devtools' +import viteCompression from 'vite-plugin-compression' +import Components from 'unplugin-vue-components/vite' +import AutoImport from 'unplugin-auto-import/vite' +import ElementPlus from 'unplugin-element-plus/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import tailwindcss from '@tailwindcss/vite' +// import { visualizer } from 'rollup-plugin-visualizer' + +export default ({ mode }: { mode: string }) => { + const root = process.cwd() + const env = loadEnv(mode, root) + const { + VITE_VERSION, + VITE_PORT, + VITE_BASE_URL, + VITE_API_URL, + VITE_API_PROXY_URL, + VITE_TENANT_PREVIEW_HOST + } = env + + console.log(`🚀 API_URL = ${VITE_API_URL}`) + console.log(`🚀 VERSION = ${VITE_VERSION}`) + + // 代理策略(后端 TenantHostMiddleware 事实源 app/platform/host_middleware.py: + // 仅 /health 精确与 /api/platform/ 前缀豁免租户 Host 解析,其余按 Host 找租户): + // - /api/platform/*:豁免前缀,changeOrigin 直转即可(Host=127.0.0.1:8018 无碍); + // - /api/public/*:品牌/头像公开端点依赖租户 Host——用 headers.Host 改写成 + // VITE_TENANT_PREVIEW_HOST(种子租户域名),changeOrigin 必须为 false + // 否则 Host 会被改回目标地址使解析 404; + // - /static/*:静态资源同样过租户解析,与 /api/public 同策略。 + // 注意对象键序:更长的 /api/public 前缀必须排在 /api 之前(http-proxy 按序首配命中)。 + const tenantHostProxy = { + target: VITE_API_PROXY_URL, + changeOrigin: false, + headers: { Host: VITE_TENANT_PREVIEW_HOST } + } + + return defineConfig({ + define: { + __APP_VERSION__: JSON.stringify(VITE_VERSION) + }, + base: VITE_BASE_URL, + server: { + port: Number(VITE_PORT), + proxy: { + '/api/public': { ...tenantHostProxy }, + '/api': { + target: VITE_API_PROXY_URL, + changeOrigin: true + }, + '/static': { ...tenantHostProxy } + }, + host: true + }, + // 路径别名 + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@views': resolvePath('src/views'), + '@imgs': resolvePath('src/assets/images'), + '@icons': resolvePath('src/assets/icons'), + '@utils': resolvePath('src/utils'), + '@stores': resolvePath('src/store'), + '@styles': resolvePath('src/assets/styles') + } + }, + build: { + target: 'esnext', + outDir: 'dist', + chunkSizeWarningLimit: 2000, + minify: 'terser', + terserOptions: { + compress: { + // 生产环境去除 console + drop_console: true, + // 生产环境去除 debugger + drop_debugger: true + } + }, + dynamicImportVarsOptions: { + warnOnError: true, + exclude: [], + include: ['src/views/**/*.vue'] + } + }, + plugins: [ + vue(), + tailwindcss(), + // 自动按需导入 API + AutoImport({ + imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'], + dts: 'src/types/import/auto-imports.d.ts', + resolvers: [ElementPlusResolver()], + eslintrc: { + enabled: true, + filepath: './.auto-import.json', + globalsPropValue: true + } + }), + // 自动按需导入组件 + Components({ + dts: 'src/types/import/components.d.ts', + resolvers: [ElementPlusResolver()] + }), + // 按需定制主题配置 + ElementPlus({ + useSource: true + }), + // 压缩 + viteCompression({ + verbose: false, // 是否在控制台输出压缩结果 + disable: false, // 是否禁用 + algorithm: 'gzip', // 压缩算法 + ext: '.gz', // 压缩后的文件名后缀 + threshold: 10240, // 只有大小大于该值的资源会被处理 10240B = 10KB + deleteOriginFile: false // 压缩后是否删除原文件 + }), + vueDevTools() + // 打包分析 + // visualizer({ + // open: true, + // gzipSize: true, + // brotliSize: true, + // filename: 'dist/stats.html' // 分析图生成的文件名及路径 + // }), + ], + // 依赖预构建:避免运行时重复请求与转换,提升首次加载速度 + optimizeDeps: { + include: [ + 'echarts/core', + 'echarts/charts', + 'echarts/components', + 'echarts/renderers', + 'crypto-js', + 'element-plus/es', + 'element-plus/es/components/*/style/css', + 'element-plus/es/components/*/style/index' + ] + }, + css: { + preprocessorOptions: { + // sass variable and mixin + scss: { + additionalData: ` + @use "@styles/core/el-light.scss" as *; + @use "@styles/core/mixin.scss" as *; + ` + } + }, + postcss: { + plugins: [ + { + postcssPlugin: 'internal:charset-removal', + AtRule: { + charset: (atRule) => { + if (atRule.name === 'charset') { + atRule.remove() + } + } + } + } + ] + } + } + }) +} + +function resolvePath(paths: string) { + return path.resolve(__dirname, paths) +} From 6afa31ec85376106b07abb8300076bbed1dd1108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 15:03:30 +0800 Subject: [PATCH 03/15] =?UTF-8?q?fix:=20=E8=84=9A=E6=89=8B=E6=9E=B6?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E4=BF=AE=E5=A4=8D=EF=BC=88dts=E5=85=A5?= =?UTF-8?q?=E5=BA=93=E4=BF=9D=E9=A6=96=E8=B7=91/esnext=E6=B3=A8=E9=87=8A/?= =?UTF-8?q?=E6=A8=A1=E6=9D=BFchangelog=E6=B8=85=E7=90=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auto-imports.d.ts / components.d.ts 出 .gitignore 并入库(已重新生成):新克隆/CI 首跑 pnpm build 直接可用,Phase 4 部署依赖 - vite.config.ts build.target=esnext 补注释:Windows 本机 es2015 曾触发 esbuild 0xC0000409 原生崩溃,改后稳定;管理后台仅面向现代浏览器 - 删除模板自带 CHANGELOG.md / CHANGELOG.zh-CN.md(约 650 行 x2,与本项目无关) Co-Authored-By: Claude Code --- admin/.gitignore | 5 +- admin/CHANGELOG.md | 650 ---------------------- admin/CHANGELOG.zh-CN.md | 654 ----------------------- admin/src/types/import/auto-imports.d.ts | 319 +++++++++++ admin/src/types/import/components.d.ts | 114 ++++ admin/vite.config.ts | 3 + 6 files changed, 439 insertions(+), 1306 deletions(-) delete mode 100644 admin/CHANGELOG.md delete mode 100644 admin/CHANGELOG.zh-CN.md create mode 100644 admin/src/types/import/auto-imports.d.ts create mode 100644 admin/src/types/import/components.d.ts diff --git a/admin/.gitignore b/admin/.gitignore index e48d2e9..08f9677 100644 --- a/admin/.gitignore +++ b/admin/.gitignore @@ -6,6 +6,7 @@ dist-ssr .cursorrules # Auto-generated files -src/types/import/auto-imports.d.ts -src/types/import/components.d.ts +# 注:auto-imports.d.ts / components.d.ts 由 unplugin 生成但已入库, +# 保证新克隆/CI 首跑 pnpm build 直接可用(vue-tsc 需要它们); +# 本地重新构建会自动更新这两个文件。 .auto-import.json diff --git a/admin/CHANGELOG.md b/admin/CHANGELOG.md deleted file mode 100644 index 05d5d90..0000000 --- a/admin/CHANGELOG.md +++ /dev/null @@ -1,650 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Released] - -### v3.0.1 - 2025-11-15 - -#### Fixed - -- Fixed repeated API calls issue when accessing non-existent interfaces during route registration -- Fixed one-click cleanup script build failure issue - -#### Optimized - -- Enhanced route configuration validation mechanism, automatically detects and prompts path configuration errors for non-first-level menus -- Optimized top progress bar ghosting issue -- Optimized vite pre-build configuration -- Unified border radius and border consistency -- Redesigned lock screen page, improved visual experience -- Optimized visual experience issue caused by logout menu disappearing prematurely - -#### Added - -- ArtForm and ArtSearchBar component labels support custom render functions, enabling complex labels with tooltips -- Menu management form key fields added Tooltip hints, lowering user configuration threshold -- Iconify added offline icon loading mode -- Logout added redirect property for redirecting to corresponding page after re-login -- Logout and re-entry preserves user tabs, identified by userId -- Dual menu added collapse button -- Enhanced menu icons -- Multi-tab pages added icon display -- Added WebSocket connection example -- Lock screen password error triggers input box shake animation - -#### ⚠️ Important Notice - -> This version requires re-login - ---- - -### v3.0.0 - 2025-11-09 🎉 - -#### Refactored - -- Style system refactored: Sass fully migrated to Tailwind CSS, improving development efficiency and style consistency -- Icon solution upgraded: Iconfont replaced with Iconify, supporting richer icon libraries and on-demand loading -- Route registration refactored: Comprehensive refactoring of route registration system with object-oriented design, improving code maintainability, testability and extensibility -- Refactored ArtTextScroll component, improving performance and maintainability -- Refactored color system, unified UI visual specifications, improving interface consistency - -#### Optimized - -- Build optimization: Complete package size reduced by 1.3 MB, significantly improving loading performance -- Architecture optimization: Optimized directory structure with clearer responsibility division, reducing user learning curve and onboarding difficulty -- Comment optimization: Unified module comment standards, improved component descriptions, feature explanations and usage examples, reducing user understanding cost and onboarding difficulty -- Performance improvement: Optimized core code logic, improving system runtime efficiency -- Menu optimization: Refined menu styles, optimized interaction experience and visual presentation -- Responsive optimization: Optimized ArtForm and ArtSearchBar grid layout, adapting to various screen sizes -- Configuration management optimization: Added setting.ts configuration file, supporting one-click copy and reset of system default settings - -#### Fixed - -- Fixed ArtForm and ArtSearchBar custom component rendering issues - -#### Added - -- ArtForm and ArtSearchBar added render property, supporting custom component rendering -- Festival feature enhancement: Fireworks configuration supports cross-date range settings and custom playback counts - -#### Upgraded - -- Dependency updates: Upgraded core dependencies to latest stable versions - -#### ⚠️ Important Notice - -> This upgrade involves fundamental refactoring of the style system (Sass → Tailwind CSS) and icon library (Iconfont → Iconify), which is a breaking change. It is recommended to use v3.0 for new projects. Upgrading existing projects is not recommended. - ---- - -### v2.6.1 - 2025-10-19 - -#### Fixed - -- Fixed repeated API calls issue when accessing invalid addresses for user info and menu endpoints -- Fixed ElButton circle mode styles -- Fixed ElSelect keyboard selection issue -- Fixed static routes with parameters redirecting to login page issue - -#### Optimized - -- Upgraded some dependencies to be compatible with tailwindcss -- Optimized external link menu click selection state -- Added theme color switching feature to authorization pages - ---- - -### v2.6.0 - 2025-10-16 - -#### Added - -- Extended registration and password reset page top component support - -#### Optimized - -- Optimized streamlined version menu data structure, improved data consistency -- Optimized local development environment network request proxy configuration -- Optimized ElTree component default styles -- Added VsCode recommended plugin related configuration -- Optimized ElDropdown component click trigger mode interaction styles -- Optimized menu filtering logic -- Optimized page switching animation, improved loading speed -- Optimized dark mode text color - -#### Fixed - -- Fixed static route custom home page path first visit redirecting to login page issue -- Fixed logout briefly jumping to 500 page issue -- Fixed v2.5.9 version home route redirect configuration failure issue -- Fixed v2.5.9 auto-import mechanism causing build exception - -#### ⚠️ Important Notice - -> This version requires re-login - ---- - -### v2.5.9 - 2025-10-12 - -#### Code Optimized - -- Optimized views file directory, file names, and code -- Added global configuration for useTable pagination request fields (tableConfig.ts) -- Optimized routing configuration to modular structure -- Menu API now uses apifox mock data (set VITE_ACCESS_MODE to backend mode in .env) - ---- - -### v2.5.8 - 2025-09-29 - -#### Upgraded - -- Upgraded core libraries including vue, vite, element-plus - -#### Fixed - -- Fixed rich text editor fullscreen top bar z-index issue -- Fixed table column sorting component text overflow issue -- Fixed statistics card condition judgment -- Fixed infinite redirect when root path / and HOME_PAGE_PATH are both / - -#### Optimized - -- Optimized el-tag styles -- Optimized top progress bar color -- Optimized custom theme configuration -- Optimized ElementPlus custom theme issues - -#### ⚠️ Important Notice - -> Due to dependency upgrades, Node version needs to be upgraded to v20.19.0 or above - ---- - -### v2.5.7 - 2025-09-14 - -#### Added - -- Added ArtForm component - -#### Fixed - -- Fixed theme switching flicker issue in new Chrome versions - -#### Optimized - -- Optimized form label height alignment issue -- First screen startup performance optimization - ---- - -### v2.5.6 - 2025-09-06 - -#### Added - -- ArtTableHeader added search button to control top search bar visibility -- Added permission demonstration examples -- Added global error handling framework - -#### Optimized - -- useTable type inference optimization, no need to manually pass types for type hints -- useTable removeColumn supports multiple data deletion -- useTable auto-recognizes response body with custom configuration (src/utils/table/tableConfig.ts) -- useTable empty data browser warning optimization -- API request code optimization, api.d.ts type optimization -- Optimized ArtTable top button wrapping adaptive issue -- ArtTable pagination component selection style optimization -- ArtTable empty state height fills by default -- ArtButtonMore component added icon and color configuration -- ArtSearchBar label doesn't occupy space when empty -- Table operation column drag prohibits fixed column dragging -- Role management page API integration and code optimization -- Menu management page optimization -- Optimized settings center scroll following issue -- First-level route external link component validation logic optimization -- Optimized map bottom-right drag issue -- Optimized dark mode page refresh white background issue -- Optimized left menu collapse button spacing issue -- Mobile displays left menu logo -- Network request added showSuccessMessage to configure success message display -- Global components use async loading strategy to improve first screen loading performance - -#### Fixed - -- Fixed bug where batch deleting entire page data doesn't return to previous page -- Fixed dynamic route parameter issues -- Fixed dynamic route configuration first-level route iframe page fullscreen issue - ---- - -### v2.5.5 - 2025-08-17 - -#### Added - -- Added ArtSearchBar component examples -- useTable added excludeParams to exclude certain parameters from requests - -#### Refactored - -- Refactored ArtSearchBar component, supports more components and form validation - -#### Optimized - -- useTable column configuration: supports dynamic update capability -- Optimized color picker border radius -- Unified el-radio and el-checkbox sizes -- art-stats-card added decimal places and separator configuration -- Route configuration example optimization -- Advanced table added custom data fetching example -- Optimized path alias type issues -- Local development CORS configuration optimization -- useTable property and method naming optimization -- Login page UI upgrade -- 403, 404, 500 page UI upgrade - -#### Fixed - -- Fixed multiple rich text editor icon inconsistency issue -- Fixed useTable deleting last entire page data not returning to previous page issue -- Fixed echarts chart data initialization and update browser error -- Network request supports logout on HTTP status code 401 -- Optimized network request logout multiple prompt issue - -#### Removed - -- Removed art-chart-empty component - ---- - -### v2.5.4 - 2025-07-27 - -#### Added - -- Network request headers support custom configuration -- Expand row supports formatter rendering - -#### Optimized - -- Route registration added component validation - -#### Fixed - -- Fixed user info API timing issue causing route registration menu rendering error -- Fixed dynamic route validation issue causing iframe not to display -- Fixed reset file syntax error -- Fixed ArtTable data type error -- Fixed map scroll wheel zoom issue - ---- - -### v2.5.3 - 2025-07-20 - -#### Added - -- Added table left-right layout example - -#### Refactored - -- ArtTable component refactored - -#### Upgraded - -- Element Plus upgraded to v2.10.2 - -#### Optimized - -- Optimized useTable pagination parameter issue -- Optimized network request example: initialization parameters, pagination carrying parameters -- Optimized search date range parameter handling -- Optimized el-date-picker component border radius -- Optimized el-select component hover style -- Search component and pagination component height reduced -- Optimized login page slider animation interval -- Optimized menu without submenu display issue - -#### Fixed - -- Fixed ArtTable switching page size executing two requests bug - ---- - -### v2.5.2 - 2025-07-13 - -#### Added - -- Added one-click cleanup script for quick development environment preparation -- useTable hooks support custom pagination field name mapping -- Horizontal menu, mixed menu, double column menu support badge display -- Quick entry supports configuration file mode -- Top bar function supports configuration file mode -- Support custom home page path -- Route supports redirect and other attributes configuration - -#### Optimized - -- Global event bus mittBus type safety optimization -- Optimized mobile settings container width style -- Optimized login page verification slider text centering effect - -#### Fixed - -- Fixed table no data header not displaying issue -- Fixed v2.5.0 top progress bar not displaying issue -- Fixed left menu mask abnormal display issue -- Fixed hiding all submenus still showing parent menu issue -- Fixed stylelint causing login page slider style abnormality -- Fixed old mobile device loading positioning issue - ---- - -### v2.5.1 - 2025-07-08 - -#### Optimized - -- el-card and el-table background color consistent with system - -#### Fixed - -- Fixed first login system loading closing early bug -- Fixed v2.5.0 version causing fullscreen page style z-index too low bug -- Fixed v2.5.0 version causing table expand row collapse bug - ---- - -### v2.5.0 - 2025-07-06 🎉 - -#### Added - -- Added useTable hooks table encapsulation, supports data fetching, transformation, response adaptation, intelligent caching (based on LRU algorithm), error handling, column configuration and slots, pagination control, refresh strategies and other core functions, comprehensively improving development efficiency and user experience - -#### Refactored - -- Refactored ArtTable, ArtTableHeader, ArtNotification components - -#### Upgraded - -- Echarts version upgraded to 5.6.0 - -#### Removed - -- Removed CountTo plugin, replaced with ArtCountTo component - -#### Fixed - -- Fixed menu management search directly modifying pinia data issue -- Fixed route guard loading flicker issue - -#### ⚠️ Important Notice - -> Recommended upgrade for more efficient and intelligent table development experience - ---- - -### v2.4.2.9 - 2025-07-02 - -#### Refactored - -- Menu layout and top navigation code refactored - -#### Optimized - -- Optimized mobile menu scroll user experience -- Optimized top menu style issues -- Top menu width adaptive, can display more content, mixed menu supports mouse scroll -- asyncRoutes route configuration auth_mark field changed to authMark -- Removed duplicate components.d.ts file, components.d.ts and auto-imports.d.ts ignored from commits -- Optimized i18n language file loading method, changed from async to sync mode -- Optimized el-pagination size inconsistency issue - -#### Fixed - -- Fixed mobile lock screen page some browsers unable to unlock bug - ---- - -### v2.4.2.8 - 2025-06-26 - -#### Fixed - -- Fixed v2.4.2.7 version accessing / path showing 404 issue - ---- - -### v2.4.2.7 - 2025-06-25 - -#### Added - -- Route supports fullscreen mode configuration -- Route supports auto-jump to first valid route of menu -- Dynamic route added removeAllDynamicRoutes method for completely clearing all dynamic routes -- Permission custom directive optimization, added role permission directive v-roles for controlling element visibility -- Added tab operation examples - -#### Optimized - -- CORS request carrying cookie configuration from environment variables, disabled by default -- Some optimizations for SEO and accessibility - -#### Fixed - -- Fixed login page drag component ArtDragVerify width and color abnormal bug -- Fixed iframe page mixed mode and double column mode abnormal bug -- Optimized lock screen page being penetrated by el-loading bug - ---- - -### v2.4.2.6 - 2025-06-23 - -#### Refactored - -- Refactored form-related components in components/core/forms folder, improved maintainability and consistency -- Refactored ArtBreadcrumb breadcrumb navigation component, optimized logic structure and styles -- Refactored ArtFireworksEffect fireworks effect component, significantly improved rendering performance and animation smoothness - -#### Optimized - -- Optimized ArtChatWindow and ArtFastEnter component code, improved readability and performance - -#### Added - -- README documentation added official website link for easy access to project documentation - ---- - -### v2.4.2.5 - 2025-06-22 - -#### Refactored - -- Refactored chart components, optimized code structure and maintainability -- Fine-tuned chart animations and theme color schemes, improved visual consistency - ---- - -### v2.4.2.4 - 2025-06-18 - -#### Refactored - -- ArtMenuRight component refactored -- Components under components/core/cards refactored, code optimized - -#### Optimized - -- ArtWatermark added type annotations - ---- - -### v2.4.2.3 - 2025-06-18 - -#### Refactored - -- ArtResultPage component refactored - -#### Optimized - -- ArtTextScroll component code optimization -- ArtException component added type hints -- ArtCutterImg component style optimization, added type definitions -- ArtVideoPlayer component added type definitions - ---- - -### v2.4.2.2 - 2025-06-16 - -#### Refactored - -- Back to top component refactored -- Icon selector component refactored - -#### Changed - -- System Logo component property changes - ---- - -### v2.4.2.1 - 2025-06-16 - -#### Refactored - -- Banner component refactored and optimized - -#### Fixed - -- Fixed mixed menu first menu nested menu jump bug - ---- - -### v2.4.2 - 2025-06-14 - -#### Refactored - -- Refactored network request module, enhanced error handling, type safety and multi-language support - -#### Added - -- Route configuration added activePath active menu path property - -#### Optimized - -- Guide and column settings multi-language improvement -- Removed invalid code from user list and menu management pages -- Updated technical support link - -#### Fixed - -- Fixed mobile search bar unable to scroll, iPad page scroll abnormal issue -- Fixed el-dialog enabling draggable property causing custom animation failure issue -- Fixed v2.3.0 local storage refactoring causing login, registration page multi-language settings unable to persist issue -- Fixed table fixed column not working bug -- Fixed infinite redirect when root path / and HOME_PAGE_PATH are both / - -#### ⚠️ Important Notice - -> This version requires re-login - ---- - -### v2.4.1.1 - 2025-06-07 - -#### Optimized - -- Optimized role management page code -- el-dialog visual effect optimization, supports line configuration -- System theme mode changed from Light to follow system mode - -#### Fixed - -- Fixed menu management collapse bug -- Fixed table empty data height infinitely growing bug - ---- - -### v2.4.1 - 2025-06-07 - -#### Added - -- Prohibit using developer tools to crack lock screen when locked - -#### Optimized - -- Improved menu operation responsiveness -- Page entrance animation time reduced by 0.04s -- Echarts chart performance optimization, added visible area initialization, memory leak protection, debounce handling - -#### Fixed - -- Fixed Echarts chart component not displaying in dialog bug - ---- - -### v2.4.0 - 2025-06-06 🎉 - -#### Refactored - -- Global TypeScript type system refactored, improved type accuracy and maintainability -- Refactored utils toolkit, unified utility method structure, enhanced readability and reusability - -#### Added - -- utils added form validation and Cookie operation related utility functions -- Page components added defineOptions for explicit component naming -- HTTP request added token expiration auto-handling logic, improved security and user experience - -#### Optimized - -- Deleted unused utility modules and invalid resources, streamlined project size -- Optimized views page structure, removed redundant page files -- Exception page multi-language support, improved internationalization experience -- Image resources uniformly converted to webp format, overall resource size reduced by about 50% -- Build output reduced by about 1MB, improved loading efficiency - -#### ⚠️ Important Notice - -> This version requires re-login - ---- - -## Version Guidelines - -### Version Number Rules - -This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html): - -- **Major**: Incompatible API changes -- **Minor**: Backwards-compatible functionality additions -- **Patch**: Backwards-compatible bug fixes - -### Change Types - -- **Added**: New features -- **Changed**: Changes to existing functionality -- **Deprecated**: Soon-to-be removed features -- **Removed**: Removed features -- **Fixed**: Bug fixes -- **Security**: Security-related fixes -- **Optimized**: Performance or experience optimizations -- **Refactored**: Code refactoring -- **Upgraded**: Dependency upgrades - -### Important Notice Markers - -- 🎉 **Major Update**: Versions with important new features -- ⚠️ **Breaking Changes**: Updates requiring special attention for incompatibility -- 🔒 **Security Update**: Versions fixing security vulnerabilities - ---- - -## Contributing - -If you want to contribute to the project, please check [CONTRIBUTING.md](./CONTRIBUTING.md). - -## License - -[MIT License](./LICENSE) - ---- - -**Note**: This changelog starts recording from version v2.4.0. For earlier version history, please check Git commit records. diff --git a/admin/CHANGELOG.zh-CN.md b/admin/CHANGELOG.zh-CN.md deleted file mode 100644 index d1b43f2..0000000 --- a/admin/CHANGELOG.zh-CN.md +++ /dev/null @@ -1,654 +0,0 @@ -# 更新日志 - -所有重要的项目变更都将记录在此文件中。 - -格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 - -## [已发布] - -### v3.0.1 - 2025-11-15 - -#### 修复 - -- 修复路由注册时不存在接口的重复请求问题 -- 修复一键精简脚本打包失败的问题 - -#### 优化 - -- 完善路由配置验证机制,自动检测并提示非一级菜单的路径配置错误 -- 优化顶部进度条残影问题 -- 优化 vite 预构建配置 -- 优化圆角、边框统一性 -- 重新设计锁屏页面,提升视觉体验 -- 优化退出登录菜单提前消失造成的视觉体验不好问题 - -#### 新增 - -- ArtForm 和 ArtSearchBar 组件的 label 支持自定义渲染函数,可实现带 tooltip 等复杂标签 -- 菜单管理表单关键字段新增 Tooltip 提示,降低用户配置门槛 -- iconify 新增离线图标加载模式 -- 退出登录新增 redirect 属性,用于重新登录后跳转到对应页面 -- 退出登录重新进入系统保留用户标签页,根据 userId 进行身份识别 -- 双列菜单新增折叠按钮 -- 完善菜单图标 -- 多标签页增加图标显示 -- 新增 WebSocket 连接示例 -- 锁屏密码错误时增加输入框抖动动效 - -#### ⚠️ 重要提示 - -> 此版本需要重新登录 - ---- - -### v3.0.0 - 2025-11-09 🎉 - -#### 重构 - -- 样式系统重构:Sass 全面迁移至 Tailwind CSS,提升开发效率与样式一致性 -- 图标方案升级:Iconfont 替换为 Iconify,支持更丰富的图标库与按需加载 -- 路由注册重构:全面重构路由注册系统,引入面向对象设计,提高代码的可维护性、可测试性和扩展能力 -- 重构 ArtTextScroll 组件,提升性能与可维护性 -- 重构颜色体系,统一 UI 视觉规范,提升界面一致性 - -#### 优化 - -- 构建优化:完整包体积减少 1.3 MB,显著提升加载性能 -- 架构优化:优化目录结构,职责划分更清晰,降低用户学习成本与上手难度 -- 注释优化:统一模块注释规范,完善每一个组件介绍、功能说明与使用示例,降低用户理解成本与上手难度 -- 性能提升:优化核心代码逻辑,提升系统运行效率 -- 菜单优化:细化菜单样式,优化交互体验与视觉呈现 -- 响应式优化:优化 ArtForm、ArtSearchBar 栅格布局,适配多种屏幕尺寸 -- 配置管理优化:新增 setting.ts 配置文件,支持一键复制与重置系统默认设置 - -#### 修复 - -- 修复 ArtForm、ArtSearchBar 自定义组件渲染异常 - -#### 新增 - -- ArtForm、ArtSearchBar 新增 render 属性,支持自定义组件渲染 -- 节日功能增强:礼花配置支持跨日期范围设置与自定义播放次数 - -#### 升级 - -- 依赖更新:升级核心依赖至最新稳定版本 - -#### ⚠️ 重要提示 - -> 本次升级涉及样式系统(Sass → Tailwind CSS)与图标库(Iconfont → Iconify)的底层重构,属于破坏性更新。建议新项目直接使用 v3.0,旧版本项目不建议升级。 - ---- - -### v2.6.1 - 2025-10-19 - -#### 修复 - -- 修复获取用户信息、获取菜单接口访问无效地址重复调用问题 -- 修复 ElButton circle 模式样式 -- 修复 ElSelect 无法通过键盘选择问题 -- 修复带参数静态路由跳转登录页面问题 - -#### 优化 - -- 升级部分依赖兼容 tailwindcss -- 优化外部链接菜单点击选中状态 -- 授权页增加主题色切换功能 - ---- - -### v2.6.0 - 2025-10-16 - -#### 新增 - -- 扩展注册、密码重置页面顶部组件支持 - -#### 优化 - -- 优化精简版本菜单数据结构,提升数据一致性 -- 优化本地开发环境网络请求代理配置 -- 优化 ElTree 组件默认样式 -- 新增 VsCode 推荐插件相关配置 -- 优化 ElDropdown 组件点击触发模式下的交互样式 -- 优化菜单过滤逻辑 -- 优化页面切换动画,提升加载速度 -- 优化暗黑模式文字颜色 - -#### 修复 - -- 修复静态路由自定义首页路径首次访问跳转登录页问题 -- 修复退出登录时短暂跳转至 500 页的问题 -- 修复 v2.5.9 版本首页路由跳转配置失效问题 -- 修复 v2.5.9 自动导包机制导致的构建异常 - -#### ⚠️ 重要提示 - -> 此版本需要重新登录 - ---- - -### v2.5.9 - 2025-10-12 - -#### 代码优化 - -- views 文件目录、文件名、代码优化 -- useTable 分页请求字段增加全局配置 tableConfig.ts -- 优化路由配置为模块化结构 -- 获取菜单接口使用 apifox mock 数据(需在 .env 中将 VITE_ACCESS_MODE 设为 backend 模式) - ---- - -### v2.5.8 - 2025-09-29 - -#### 升级 - -- vue、vite、element-plus 等核心库升级 - -#### 修复 - -- 修复富文本编辑器全屏顶栏层级问题 -- 修复表格列排序组件文字溢出问题 -- 修复统计卡片条件判断 -- 修复根路径 / 与 HOME_PAGE_PATH 同为 / 时出现的无限重定向 - -#### 优化 - -- 优化 el-tag 样式 -- 优化顶部进度条颜色 -- 优化自定义主题配置 -- 优化 ElementPlus 自定义主题问题 - -#### ⚠️ 重要提示 - -> 由于项目依赖升级,node 版本需要升级到 v20.19.0 或以上 - ---- - -### v2.5.7 - 2025-09-14 - -#### 新增 - -- 新增 ArtForm 组件 - -#### 修复 - -- 修复新版本谷歌浏览器切换主题闪烁问题 - -#### 优化 - -- 优化表单 label 高度没有对齐问题 -- 首屏启动性能优化 - ---- - -### v2.5.6 - 2025-09-06 - -#### 新增 - -- ArtTableHeader 新增搜索按钮,用于控制顶部搜索栏的显示与隐藏 -- 新增权限演示示例 -- 添加全局错误处理基础框架 - -#### 优化 - -- useTable 类型推导优化,不需要手动传递类型即可实现类型提示 -- useTable removeColumn 支持多数据删除 -- useTable 自动识别响应体支持自定义配置 (src/utils/table/tableConfig.ts) -- useTable 空数据浏览器警告优化 -- api 接口请求代码优化、api.d.ts 类型优化 -- 优化 ArtTable 顶部按钮换行无法自适应问题(示例:功能示例 / 左右布局表格) -- ArtTable 分页组件选中样式优化 -- ArtTable 空状态高度默认撑满 -- ArtButtonMore 组件新增图标、颜色配置 -- ArtSearchBar label 为空时不占空间 -- 表格操作栏拖拽禁止固定列拖拽 -- 角色管理页面接口对接、代码优化 -- 菜单管理页面优化 -- 优化设置中心滚动页面跟随滚动问题 -- 一级路由是外链时,component 校验逻辑优化 -- 优化地图右下角拖动问题 -- 优化暗黑模式刷新页面白色背景问题 -- 优化左侧菜单折叠按钮间距问题 -- 移动端显示左侧菜单 logo -- 网络请求新增 showSuccessMessage,用于配置是否显示成功消息 -- 全局组件采用异步加载策略,提升首屏加载性能 - -#### 修复 - -- 修复批量删除整页数据没有返回上一页的bug -- 修复动态路由参数导致的问题 -- 修复动态路由配置一级路由是 iframe 页面时,全屏问题 - ---- - -### v2.5.5 - 2025-08-17 - -#### 新增 - -- 新增 ArtSearchBar 组件示例 -- useTable 新增 excludeParams,用于排除某些参数不参与请求 - -#### 重构 - -- 重构 ArtSearchBar 组件,支持更多组件、表单校验等能力 - -#### 优化 - -- useTable 列配置:支持动态更新能力 -- 优化颜色选择器圆角 -- el-radio、el-checkbox 统一大小 -- art-stats-card 新增小数位、分隔符配置 -- 路由配置示例优化 -- 高级表格新增自定义获取数据示例(等待其他请求完成后执行 useTable 数据获取) -- 优化路径别名类型问题 -- 本地开发跨域配置优化 -- useTable 属性、方法命名优化 -- 登录页UI升级 -- 403、404、500 页面UI升级 - -#### 修复 - -- 修复多个富文本编辑器图标不统一问题 -- 修复 useTable 删除最后一整页数据没有返回上一页的问题 -- 修复 echarts 图表数据初始化、更新数据浏览器报错 -- 网络请求支持 http 状态码为 401 时退出登录 -- 优化网络请求退出登录多次提示问题 - -#### 移除 - -- 删除 art-chart-empty 组件 - ---- - -### v2.5.4 - 2025-07-27 - -#### 新增 - -- 网络请求 headers 支持自定义配置 -- 展开行支持 formatter 渲染 - -#### 优化 - -- 路由注册新增 component 校验 - -#### 修复 - -- 修复获取用户信息接口时序问题导致路由注册菜单渲染错误bug -- 修复动态路由校验问题导致的 iframe 不显示bug -- 修复 reset 文件语法错误 -- 修复 ArtTable 数据类型错误 -- 修复地图滚轮滚动放大问题 - ---- - -### v2.5.3 - 2025-07-20 - -#### 新增 - -- 新增表格左右布局示例 - -#### 重构 - -- ArtTable 组件重构 - -#### 升级 - -- Element Plus 升级到 v2.10.2 - -#### 优化 - -- 优化 useTable 分页参数问题 -- 优化网络请求示例:初始化参数、分页携带参数问题 -- 优化搜索日期范围参数处理 -- 优化 el-date-picker 组件圆角问题 -- 优化 el-select 组件 hover 样式 -- 搜索组件、分页组件高度降低 -- 优化登录页面滑块动画间隔时长 -- 优化菜单没有子菜单显示的问题 - -#### 修复 - -- 修复 ArtTable 切换分页大小时执行两次请求bug - ---- - -### v2.5.2 - 2025-07-13 - -#### 新增 - -- 新增一键精简脚本,快速准备开发环境 -- useTable hooks 支持分页字段名自定义映射 -- 水平菜单、混合菜单、双列菜单支持徽章显示 -- 快速入口支持配置文件模式 -- 顶栏功能支持配置文件模式 -- 支持自定义首页路径 -- 路由支持配置 redirect 等属性 - -#### 优化 - -- 全局事件总线 mittBus 类型安全优化 -- 优化移动端设置中容器宽度样式 -- 优化登录页验证滑块文字居中效果 - -#### 修复 - -- 修复表格无数据时表头不显示问题 -- 修复 v2.5.0 顶部进度条不显示问题 -- 修复左侧菜单遮罩异常显示问题 -- 修复隐藏所有子菜单时仍显示父级菜单的问题 -- 修复 stylelint 导致的登录页滑块样式异常 -- 修复老旧移动端设备 loading 定位问题 - ---- - -### v2.5.1 - 2025-07-08 - -#### 优化 - -- el-card、el-table 背景色跟系统保持一致 - -#### 修复 - -- 修复首次登录系统时 loading 提前关闭bug -- 修复 v2.5.0 版本引起的全屏页样式层级过低bug -- 修复 v2.5.0 版本引起的表格展开行折叠bug - ---- - -### v2.5.0 - 2025-07-06 🎉 - -#### 新增 - -- 新增 useTable hooks 表格封装,支持数据获取、转换、响应适配、智能缓存(基于 LRU 算法)、错误处理、列配置与插槽、分页控制、刷新策略等核心功能,全面提升开发效率与用户体验 - -#### 重构 - -- 重构 ArtTable、ArtTableHeader、ArtNotification 组件 - -#### 升级 - -- Echarts 版本升级到 5.6.0 - -#### 移除 - -- 移除 CountTo 插件,替换为 ArtCountTo 组件 - -#### 修复 - -- 修复菜单管理搜索直接修改 pinia 数据的问题 -- 修复路由守卫 loading 闪烁问题 - -#### ⚠️ 重要提示 - -> 建议升级,带来更高效、更智能的表格开发体验 - ---- - -### v2.4.2.9 - 2025-07-02 - -#### 重构 - -- 菜单布局、顶部导航代码重构 - -#### 优化 - -- 优化移动端菜单滚动用户体验 -- 优化顶部菜单样式问题 -- 顶部菜单宽度自适应,可显示更多内容,混合菜单支持鼠标滚动 -- asyncRoutes 路由配置 auth_mark 字段改为 authMark -- 去除重复的 components.d.ts 文件,components.d.ts、auto-imports.d.ts 忽略提交 -- 优化国际化语言文件加载方式,异步改成同步模式 -- 优化 el-pagination 大小不一致问题 - -#### 修复 - -- 修复移动端锁屏页部分浏览器无法解锁bug - ---- - -### v2.4.2.8 - 2025-06-26 - -#### 修复 - -- 修复 v2.4.2.7 版本访问 / 路径时显示 404 的问题 - ---- - -### v2.4.2.7 - 2025-06-25 - -#### 新增 - -- 路由支持配置全屏模式 -- 路由支持自动跳转到菜单的第一个有效路由 -- 动态路由新增 removeAllDynamicRoutes 方法,可用于彻底清除所有动态路由 -- 权限自定义指令优化、新增角色权限指令 v-roles、可用于控制元素的显示与隐藏 -- 新增标签页操作示例 - -#### 优化 - -- 跨域请求携带 cookie 配置从环境变量中获取,默认关闭 -- 针对SEO、可访问性做一些优化 - -#### 修复 - -- 修复登录页面拖拽组件 ArtDragVerify 宽度、颜色异常bug -- 修复 iframe 页面混合模式、双列模式异常bug -- 优化锁屏页面被 el-loading 穿透bug - ---- - -### v2.4.2.6 - 2025-06-23 - -#### 重构 - -- 重构 components/core/forms 文件夹下的表单相关组件,提升可维护性与一致性 -- 重构 ArtBreadcrumb 面包屑导航组件,优化逻辑结构与样式 -- 重构 ArtFireworksEffect 烟花效果组件,显著提升渲染性能与动画流畅度 - -#### 优化 - -- 优化 ArtChatWindow 与 ArtFastEnter 组件代码,提升可读性与性能 - -#### 新增 - -- README 文档新增官方网站链接,便于用户查看项目文档 - ---- - -### v2.4.2.5 - 2025-06-22 - -#### 重构 - -- 重构图表组件,优化代码结构与可维护性 -- 精细调整图表动画与主题配色方案,提升视觉一致性 - ---- - -### v2.4.2.4 - 2025-06-18 - -#### 重构 - -- ArtMenuRight 组件重构 -- components/core/cards 下面的组件重构,代码优化 - -#### 优化 - -- ArtWatermark 增加类型注释 - ---- - -### v2.4.2.3 - 2025-06-18 - -#### 重构 - -- ArtResultPage 组件重构 - -#### 优化 - -- ArtTextScroll 组件代码优化 -- ArtException 组件增加类型提示 -- ArtCutterImg 组件样式优化、增加类型定义 -- ArtVideoPlayer 组件增加类型定义 - ---- - -### v2.4.2.2 - 2025-06-16 - -#### 重构 - -- 返回顶部组件重构 -- 图标选择器组件重构 - -#### 修改 - -- 系统Logo组件属性变更 - ---- - -### v2.4.2.1 - 2025-06-16 - -#### 重构 - -- 横幅组件重构以及优化 - -#### 修复 - -- 修复混合菜单下第一个菜单是嵌套菜单跳转bug - ---- - -### v2.4.2 - 2025-06-14 - -#### 重构 - -- 重构网络请求模块,增强错误处理、类型安全与多语言支持 - -#### 新增 - -- 路由配置新增 activePath 激活菜单路径属性 - -#### 优化 - -- 引导、列设置多语言完善 -- 去除用户列表、菜单管理页面无效代码 -- 更新技术支持链接 - -#### 修复 - -- 修复移动端搜索栏无法滚动、iPad端页面滚动异常问题 -- 修复 el-dialog 启用 draggable 属性后,自定义动画失效的问题 -- 修复 2.3.0 版本本地存储重构后,导致登录、注册等页面多语言设置无法持久化的问题 -- 修复表格固定列不起作用bug -- 修复根路径 / 与 HOME_PAGE_PATH 同为 / 时出现的无限重定向 - -#### ⚠️ 重要提示 - -> 此版本需要重新登录 - ---- - -### v2.4.1.1 - 2025-06-07 - -#### 优化 - -- 优化角色管理页面代码 -- el-dialog视觉效果优化,支持配置线条 -- 系统主题模式从Light改成跟随系统模式 - -#### 修复 - -- 修复菜单管理折叠 bug -- 修复表格数据为空高度无限变大bug - ---- - -### v2.4.1 - 2025-06-07 - -#### 新增 - -- 锁屏状态下禁止使用开发者工具破解锁屏 - -#### 优化 - -- 提升菜单操作跟手感 -- 页面入场动画时间减少0.04s -- Echarts 图表性能优化,新增可视区域初始化、内存泄漏防护、防抖处理 - -#### 修复 - -- 修复 Echarts 图表组件在弹窗中不显示的 bug - ---- - -### v2.4.0 - 2025-06-06 🎉 - -#### 重构 - -- 全局 TypeScript 类型体系重构,提升类型准确性与可维护性 -- 重构 utils 工具包,统一工具方法结构,增强可读性与复用性 - -#### 新增 - -- utils 新增表单验证与 Cookie 操作相关工具函数 -- 页面组件增加 defineOptions,明确组件命名 -- HTTP 请求增加 token 过期自动处理逻辑,提升安全性与用户体验 - -#### 优化 - -- 删除未使用的工具模块与无效资源,精简项目体积 -- 优化 views 页面结构,移除冗余页面文件 -- 异常页面多语言支持, 提升国际化体验 -- 图片资源统一转换为 webp 格式,整体资源体积减少约 50% -- 打包产物减少约 1MB,提高加载效率 - -#### ⚠️ 重要提示 - -> 此版本需要重新登录 - ---- - -## 版本说明 - -### 版本号规则 - -本项目遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) 规范: - -- **主版本号(Major)**:当你做了不兼容的 API 修改 -- **次版本号(Minor)**:当你做了向下兼容的功能性新增 -- **修订号(Patch)**:当你做了向下兼容的问题修正 - -### 变更类型 - -- **新增(Added)**:新功能 -- **修改(Changed)**:对现有功能的变更 -- **弃用(Deprecated)**:即将移除的功能 -- **移除(Removed)**:已移除的功能 -- **修复(Fixed)**:Bug 修复 -- **安全(Security)**:安全相关的修复 -- **优化(Optimized)**:性能优化或体验优化 -- **重构(Refactored)**:代码重构 -- **升级(Upgraded)**:依赖升级 - -### 重要提示标记 - -- 🎉 **重大更新**:包含重要新特性的版本 -- ⚠️ **破坏性变更**:需要特别注意的不兼容更新 -- 🔒 **安全更新**:修复安全漏洞的版本 - ---- - -## 贡献指南 - -如果你想为项目做出贡献,请查看 [CONTRIBUTING.md](./CONTRIBUTING.md)。 - -## 许可证 - -[MIT License](./LICENSE) - ---- - -**注意**:本更新日志从 v2.4.0 版本开始记录。更早的版本历史请查看 Git 提交记录。 - ---- - -[English Version](./CHANGELOG.en.md) diff --git a/admin/src/types/import/auto-imports.d.ts b/admin/src/types/import/auto-imports.d.ts new file mode 100644 index 0000000..a3d6466 --- /dev/null +++ b/admin/src/types/import/auto-imports.d.ts @@ -0,0 +1,319 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + const EffectScope: typeof import('vue')['EffectScope'] + const ElLoading: typeof import('element-plus/es')['ElLoading'] + const ElMessage: typeof import('element-plus/es')['ElMessage'] + const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] + const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] + const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] + const computed: typeof import('vue')['computed'] + const computedAsync: typeof import('@vueuse/core')['computedAsync'] + const computedEager: typeof import('@vueuse/core')['computedEager'] + const computedInject: typeof import('@vueuse/core')['computedInject'] + const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] + const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] + const controlledRef: typeof import('@vueuse/core')['controlledRef'] + const createApp: typeof import('vue')['createApp'] + const createEventHook: typeof import('@vueuse/core')['createEventHook'] + const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] + const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] + const createPinia: typeof import('pinia')['createPinia'] + const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] + const createRef: typeof import('@vueuse/core')['createRef'] + const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] + const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] + const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] + const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] + const customRef: typeof import('vue')['customRef'] + const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] + const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] + const defineComponent: typeof import('vue')['defineComponent'] + const defineStore: typeof import('pinia')['defineStore'] + const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] + const effectScope: typeof import('vue')['effectScope'] + const extendRef: typeof import('@vueuse/core')['extendRef'] + const getActivePinia: typeof import('pinia')['getActivePinia'] + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] + const getCurrentScope: typeof import('vue')['getCurrentScope'] + const getCurrentWatcher: typeof import('vue')['getCurrentWatcher'] + const h: typeof import('vue')['h'] + const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] + const inject: typeof import('vue')['inject'] + const injectLocal: typeof import('@vueuse/core')['injectLocal'] + const isDefined: typeof import('@vueuse/core')['isDefined'] + const isProxy: typeof import('vue')['isProxy'] + const isReactive: typeof import('vue')['isReactive'] + const isReadonly: typeof import('vue')['isReadonly'] + const isRef: typeof import('vue')['isRef'] + const isShallow: typeof import('vue')['isShallow'] + const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] + const mapActions: typeof import('pinia')['mapActions'] + const mapGetters: typeof import('pinia')['mapGetters'] + const mapState: typeof import('pinia')['mapState'] + const mapStores: typeof import('pinia')['mapStores'] + const mapWritableState: typeof import('pinia')['mapWritableState'] + const markRaw: typeof import('vue')['markRaw'] + const nextTick: typeof import('vue')['nextTick'] + const onActivated: typeof import('vue')['onActivated'] + const onBeforeMount: typeof import('vue')['onBeforeMount'] + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] + const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] + const onDeactivated: typeof import('vue')['onDeactivated'] + const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval'] + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] + const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] + const onLongPress: typeof import('@vueuse/core')['onLongPress'] + const onMounted: typeof import('vue')['onMounted'] + const onRenderTracked: typeof import('vue')['onRenderTracked'] + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] + const onScopeDispose: typeof import('vue')['onScopeDispose'] + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] + const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] + const onUnmounted: typeof import('vue')['onUnmounted'] + const onUpdated: typeof import('vue')['onUpdated'] + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] + const provide: typeof import('vue')['provide'] + const provideLocal: typeof import('@vueuse/core')['provideLocal'] + const reactify: typeof import('@vueuse/core')['reactify'] + const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] + const reactive: typeof import('vue')['reactive'] + const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] + const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] + const reactivePick: typeof import('@vueuse/core')['reactivePick'] + const readonly: typeof import('vue')['readonly'] + const ref: typeof import('vue')['ref'] + const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] + const refDebounced: typeof import('@vueuse/core')['refDebounced'] + const refDefault: typeof import('@vueuse/core')['refDefault'] + const refThrottled: typeof import('@vueuse/core')['refThrottled'] + const refWithControl: typeof import('@vueuse/core')['refWithControl'] + const resolveComponent: typeof import('vue')['resolveComponent'] + const resolveRef: typeof import('@vueuse/core')['resolveRef'] + const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] + const setActivePinia: typeof import('pinia')['setActivePinia'] + const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] + const shallowReactive: typeof import('vue')['shallowReactive'] + const shallowReadonly: typeof import('vue')['shallowReadonly'] + const shallowRef: typeof import('vue')['shallowRef'] + const storeToRefs: typeof import('pinia')['storeToRefs'] + const syncRef: typeof import('@vueuse/core')['syncRef'] + const syncRefs: typeof import('@vueuse/core')['syncRefs'] + const templateRef: typeof import('@vueuse/core')['templateRef'] + const throttledRef: typeof import('@vueuse/core')['throttledRef'] + const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] + const toRaw: typeof import('vue')['toRaw'] + const toReactive: typeof import('@vueuse/core')['toReactive'] + const toRef: typeof import('vue')['toRef'] + const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('vue')['toValue'] + const triggerRef: typeof import('vue')['triggerRef'] + const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] + const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] + const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] + const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] + const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] + const unref: typeof import('vue')['unref'] + const unrefElement: typeof import('@vueuse/core')['unrefElement'] + const until: typeof import('@vueuse/core')['until'] + const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] + const useAnimate: typeof import('@vueuse/core')['useAnimate'] + const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] + const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] + const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] + const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] + const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] + const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] + const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] + const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] + const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] + const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] + const useArraySome: typeof import('@vueuse/core')['useArraySome'] + const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] + const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] + const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] + const useAttrs: typeof import('vue')['useAttrs'] + const useBase64: typeof import('@vueuse/core')['useBase64'] + const useBattery: typeof import('@vueuse/core')['useBattery'] + const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] + const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] + const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] + const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] + const useCached: typeof import('@vueuse/core')['useCached'] + const useClipboard: typeof import('@vueuse/core')['useClipboard'] + const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] + const useCloned: typeof import('@vueuse/core')['useCloned'] + const useColorMode: typeof import('@vueuse/core')['useColorMode'] + const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] + const useCountdown: typeof import('@vueuse/core')['useCountdown'] + const useCounter: typeof import('@vueuse/core')['useCounter'] + const useCssModule: typeof import('vue')['useCssModule'] + const useCssVar: typeof import('@vueuse/core')['useCssVar'] + const useCssVars: typeof import('vue')['useCssVars'] + const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] + const useCycleList: typeof import('@vueuse/core')['useCycleList'] + const useDark: typeof import('@vueuse/core')['useDark'] + const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] + const useDebounce: typeof import('@vueuse/core')['useDebounce'] + const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] + const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] + const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] + const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] + const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] + const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] + const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] + const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] + const useDraggable: typeof import('@vueuse/core')['useDraggable'] + const useDropZone: typeof import('@vueuse/core')['useDropZone'] + const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] + const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] + const useElementHover: typeof import('@vueuse/core')['useElementHover'] + const useElementSize: typeof import('@vueuse/core')['useElementSize'] + const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] + const useEventBus: typeof import('@vueuse/core')['useEventBus'] + const useEventListener: typeof import('@vueuse/core')['useEventListener'] + const useEventSource: typeof import('@vueuse/core')['useEventSource'] + const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] + const useFavicon: typeof import('@vueuse/core')['useFavicon'] + const useFetch: typeof import('@vueuse/core')['useFetch'] + const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] + const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] + const useFocus: typeof import('@vueuse/core')['useFocus'] + const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] + const useFps: typeof import('@vueuse/core')['useFps'] + const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] + const useGamepad: typeof import('@vueuse/core')['useGamepad'] + const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] + const useId: typeof import('vue')['useId'] + const useIdle: typeof import('@vueuse/core')['useIdle'] + const useImage: typeof import('@vueuse/core')['useImage'] + const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] + const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] + const useInterval: typeof import('@vueuse/core')['useInterval'] + const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] + const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] + const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] + const useLink: typeof import('vue-router')['useLink'] + const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] + const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] + const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] + const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] + const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] + const useMemoize: typeof import('@vueuse/core')['useMemoize'] + const useMemory: typeof import('@vueuse/core')['useMemory'] + const useModel: typeof import('vue')['useModel'] + const useMounted: typeof import('@vueuse/core')['useMounted'] + const useMouse: typeof import('@vueuse/core')['useMouse'] + const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] + const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] + const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] + const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] + const useNetwork: typeof import('@vueuse/core')['useNetwork'] + const useNow: typeof import('@vueuse/core')['useNow'] + const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] + const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] + const useOnline: typeof import('@vueuse/core')['useOnline'] + const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] + const useParallax: typeof import('@vueuse/core')['useParallax'] + const useParentElement: typeof import('@vueuse/core')['useParentElement'] + const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] + const usePermission: typeof import('@vueuse/core')['usePermission'] + const usePointer: typeof import('@vueuse/core')['usePointer'] + const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] + const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] + const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] + const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] + const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] + const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] + const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] + const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency'] + const usePrevious: typeof import('@vueuse/core')['usePrevious'] + const useRafFn: typeof import('@vueuse/core')['useRafFn'] + const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] + const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] + const useRoute: typeof import('vue-router')['useRoute'] + const useRouter: typeof import('vue-router')['useRouter'] + const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth'] + const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] + const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] + const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] + const useScroll: typeof import('@vueuse/core')['useScroll'] + const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] + const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] + const useShare: typeof import('@vueuse/core')['useShare'] + const useSlots: typeof import('vue')['useSlots'] + const useSorted: typeof import('@vueuse/core')['useSorted'] + const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] + const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] + const useStepper: typeof import('@vueuse/core')['useStepper'] + const useStorage: typeof import('@vueuse/core')['useStorage'] + const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] + const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] + const useSupported: typeof import('@vueuse/core')['useSupported'] + const useSwipe: typeof import('@vueuse/core')['useSwipe'] + const useTemplateRef: typeof import('vue')['useTemplateRef'] + const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] + const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] + const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] + const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] + const useThrottle: typeof import('@vueuse/core')['useThrottle'] + const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] + const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] + const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] + const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl'] + const useTimeout: typeof import('@vueuse/core')['useTimeout'] + const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] + const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] + const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] + const useTitle: typeof import('@vueuse/core')['useTitle'] + const useToNumber: typeof import('@vueuse/core')['useToNumber'] + const useToString: typeof import('@vueuse/core')['useToString'] + const useToggle: typeof import('@vueuse/core')['useToggle'] + const useTransition: typeof import('@vueuse/core')['useTransition'] + const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] + const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] + const useVModel: typeof import('@vueuse/core')['useVModel'] + const useVModels: typeof import('@vueuse/core')['useVModels'] + const useVibrate: typeof import('@vueuse/core')['useVibrate'] + const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] + const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] + const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] + const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] + const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] + const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] + const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] + const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] + const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] + const watch: typeof import('vue')['watch'] + const watchArray: typeof import('@vueuse/core')['watchArray'] + const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] + const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] + const watchDeep: typeof import('@vueuse/core')['watchDeep'] + const watchEffect: typeof import('vue')['watchEffect'] + const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] + const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] + const watchOnce: typeof import('@vueuse/core')['watchOnce'] + const watchPausable: typeof import('@vueuse/core')['watchPausable'] + const watchPostEffect: typeof import('vue')['watchPostEffect'] + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] + const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] + const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] + const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] + const whenever: typeof import('@vueuse/core')['whenever'] +} +// for type re-export +declare global { + // @ts-ignore + export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + import('vue') +} diff --git a/admin/src/types/import/components.d.ts b/admin/src/types/import/components.d.ts new file mode 100644 index 0000000..a1af2ef --- /dev/null +++ b/admin/src/types/import/components.d.ts @@ -0,0 +1,114 @@ +/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + ArtBackToTop: typeof import('./../../components/core/base/art-back-to-top/index.vue')['default'] + ArtBarChart: typeof import('./../../components/core/charts/art-bar-chart/index.vue')['default'] + ArtBarChartCard: typeof import('./../../components/core/cards/art-bar-chart-card/index.vue')['default'] + ArtBasicBanner: typeof import('./../../components/core/banners/art-basic-banner/index.vue')['default'] + ArtBreadcrumb: typeof import('./../../components/core/layouts/art-breadcrumb/index.vue')['default'] + ArtButtonMore: typeof import('./../../components/core/forms/art-button-more/index.vue')['default'] + ArtButtonTable: typeof import('./../../components/core/forms/art-button-table/index.vue')['default'] + ArtCardBanner: typeof import('./../../components/core/banners/art-card-banner/index.vue')['default'] + ArtChatWindow: typeof import('./../../components/core/layouts/art-chat-window/index.vue')['default'] + ArtCountTo: typeof import('./../../components/core/text-effect/art-count-to/index.vue')['default'] + ArtDataListCard: typeof import('./../../components/core/cards/art-data-list-card/index.vue')['default'] + ArtDonutChartCard: typeof import('./../../components/core/cards/art-donut-chart-card/index.vue')['default'] + ArtDragVerify: typeof import('./../../components/core/forms/art-drag-verify/index.vue')['default'] + ArtDualBarCompareChart: typeof import('./../../components/core/charts/art-dual-bar-compare-chart/index.vue')['default'] + ArtException: typeof import('./../../components/core/views/exception/ArtException.vue')['default'] + ArtFastEnter: typeof import('./../../components/core/layouts/art-fast-enter/index.vue')['default'] + ArtFestivalTextScroll: typeof import('./../../components/core/text-effect/art-festival-text-scroll/index.vue')['default'] + ArtFireworksEffect: typeof import('./../../components/core/layouts/art-fireworks-effect/index.vue')['default'] + ArtForm: typeof import('./../../components/core/forms/art-form/index.vue')['default'] + ArtGlobalComponent: typeof import('./../../components/core/layouts/art-global-component/index.vue')['default'] + ArtGlobalSearch: typeof import('./../../components/core/layouts/art-global-search/index.vue')['default'] + ArtHBarChart: typeof import('./../../components/core/charts/art-h-bar-chart/index.vue')['default'] + ArtHeaderBar: typeof import('./../../components/core/layouts/art-header-bar/index.vue')['default'] + ArtHorizontalMenu: typeof import('./../../components/core/layouts/art-menus/art-horizontal-menu/index.vue')['default'] + ArtIconButton: typeof import('./../../components/core/widget/art-icon-button/index.vue')['default'] + ArtImageCard: typeof import('./../../components/core/cards/art-image-card/index.vue')['default'] + ArtKLineChart: typeof import('./../../components/core/charts/art-k-line-chart/index.vue')['default'] + ArtLineChart: typeof import('./../../components/core/charts/art-line-chart/index.vue')['default'] + ArtLineChartCard: typeof import('./../../components/core/cards/art-line-chart-card/index.vue')['default'] + ArtLogo: typeof import('./../../components/core/base/art-logo/index.vue')['default'] + ArtMenuRight: typeof import('./../../components/core/others/art-menu-right/index.vue')['default'] + ArtMixedMenu: typeof import('./../../components/core/layouts/art-menus/art-mixed-menu/index.vue')['default'] + ArtNotification: typeof import('./../../components/core/layouts/art-notification/index.vue')['default'] + ArtPageContent: typeof import('./../../components/core/layouts/art-page-content/index.vue')['default'] + ArtProgressCard: typeof import('./../../components/core/cards/art-progress-card/index.vue')['default'] + ArtRadarChart: typeof import('./../../components/core/charts/art-radar-chart/index.vue')['default'] + ArtResultPage: typeof import('./../../components/core/views/result/ArtResultPage.vue')['default'] + ArtRingChart: typeof import('./../../components/core/charts/art-ring-chart/index.vue')['default'] + ArtScatterChart: typeof import('./../../components/core/charts/art-scatter-chart/index.vue')['default'] + ArtScreenLock: typeof import('./../../components/core/layouts/art-screen-lock/index.vue')['default'] + ArtSearchBar: typeof import('./../../components/core/forms/art-search-bar/index.vue')['default'] + ArtSettingsPanel: typeof import('./../../components/core/layouts/art-settings-panel/index.vue')['default'] + ArtSidebarMenu: typeof import('./../../components/core/layouts/art-menus/art-sidebar-menu/index.vue')['default'] + ArtStatsCard: typeof import('./../../components/core/cards/art-stats-card/index.vue')['default'] + ArtSvgIcon: typeof import('./../../components/core/base/art-svg-icon/index.vue')['default'] + ArtTable: typeof import('./../../components/core/tables/art-table/index.vue')['default'] + ArtTableHeader: typeof import('./../../components/core/tables/art-table-header/index.vue')['default'] + ArtTextScroll: typeof import('./../../components/core/text-effect/art-text-scroll/index.vue')['default'] + ArtTimelineListCard: typeof import('./../../components/core/cards/art-timeline-list-card/index.vue')['default'] + ArtUserMenu: typeof import('./../../components/core/layouts/art-header-bar/widget/ArtUserMenu.vue')['default'] + ArtWatermark: typeof import('./../../components/core/others/art-watermark/index.vue')['default'] + ArtWorkTab: typeof import('./../../components/core/layouts/art-work-tab/index.vue')['default'] + AuthTopBar: typeof import('./../../components/core/views/login/AuthTopBar.vue')['default'] + BasicSettings: typeof import('./../../components/core/layouts/art-settings-panel/widget/BasicSettings.vue')['default'] + BoxStyleSettings: typeof import('./../../components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue')['default'] + ColorSettings: typeof import('./../../components/core/layouts/art-settings-panel/widget/ColorSettings.vue')['default'] + ContainerSettings: typeof import('./../../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default'] + ElAvatar: typeof import('element-plus/es')['ElAvatar'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElCard: typeof import('element-plus/es')['ElCard'] + ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDrawer: typeof import('element-plus/es')['ElDrawer'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElMenu: typeof import('element-plus/es')['ElMenu'] + ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPopover: typeof import('element-plus/es')['ElPopover'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ElWatermark: typeof import('element-plus/es')['ElWatermark'] + HorizontalSubmenu: typeof import('./../../components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue')['default'] + LoginLeftView: typeof import('./../../components/core/views/login/LoginLeftView.vue')['default'] + MenuLayoutSettings: typeof import('./../../components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue')['default'] + MenuStyleSettings: typeof import('./../../components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + SectionTitle: typeof import('./../../components/core/layouts/art-settings-panel/widget/SectionTitle.vue')['default'] + SettingActions: typeof import('./../../components/core/layouts/art-settings-panel/widget/SettingActions.vue')['default'] + SettingDrawer: typeof import('./../../components/core/layouts/art-settings-panel/widget/SettingDrawer.vue')['default'] + SettingHeader: typeof import('./../../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default'] + SettingItem: typeof import('./../../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default'] + SidebarSubmenu: typeof import('./../../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default'] + ThemeSettings: typeof import('./../../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default'] + ThemeSvg: typeof import('./../../components/core/theme/theme-svg/index.vue')['default'] + } + export interface GlobalDirectives { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } +} diff --git a/admin/vite.config.ts b/admin/vite.config.ts index a5c299c..3a25ff6 100644 --- a/admin/vite.config.ts +++ b/admin/vite.config.ts @@ -70,6 +70,9 @@ export default ({ mode }: { mode: string }) => { } }, build: { + // Windows 本机 es2015 曾在 rendering chunks 阶段触发 esbuild 原生崩溃 + //(退出码 0xC0000409,多次复现),改 esnext 后连续构建稳定; + // 管理后台仅面向现代浏览器,不做低版本降级。 target: 'esnext', outDir: 'dist', chunkSizeWarningLimit: 2000, From 3973abea8b349fcc48b79cff9e9c0df1109f6804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 16:28:11 +0800 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20API=E5=B1=82=E4=B8=8E=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E6=B5=81=EF=BC=88client=E6=8B=A6=E6=88=AA/=E5=AE=88?= =?UTF-8?q?=E5=8D=AB/=E7=99=BB=E5=BD=95=E6=94=B9=E5=AF=86=E5=AE=9E?= =?UTF-8?q?=E8=A3=85/=E5=9F=9FAPI=E9=AA=A8=E6=9E=B6/vitest=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 自建轻量 axios client(src/api/client.ts):Bearer 注入、401 清会话跳登录 (login 请求除外)、409 精确匹配"必须先修改初始密码"跳强制改密(区别于并发 冲突 409)、429/网络错误统一 ElMessage、ApiError 标准化、showErrorMessage 可opt-out——模板 utils/http 的 {code,msg,data} 信封与后端裸 HTTP 语义不符故弃用 - Pinia auth store(src/store/modules/auth.ts):token/role/tenant_slug/ must_change 落地并持久化(all-kefu-auth key),login/logout/changePassword/ 会话过期/强制改密跳转(动态 import 断循环) - 路由守卫:token 事实源判定、must_change 全站门(除改密页)、UserInfo 由登录 快照派生(后端无用户信息端点);user store 残留自愈 - 登录页实装(去除角色调试开关)、强制改密页实装;用户菜单/锁屏对接真实 logout - 域 API 骨架:tenants/config/conversations/stats/settings(类型+端点常量) - mask 哨兵助手:__SET__ ↔ 已设置 表单语义 - vitest 25 例(client 11/store 8/mask 6);登录页拖拽验证 reset 空引用修复 Co-Authored-By: Claude Code --- admin/package.json | 3 + admin/pnpm-lock.yaml | 355 ++++++++++++++++++ admin/src/api/__tests__/auth-store.test.ts | 211 +++++++++++ admin/src/api/__tests__/client.test.ts | 238 ++++++++++++ admin/src/api/__tests__/mask.test.ts | 61 +++ admin/src/api/auth.ts | 78 ++-- admin/src/api/client.ts | 143 +++++++ admin/src/api/config.ts | 78 ++++ admin/src/api/conversations.ts | 98 +++++ admin/src/api/mask.ts | 44 +++ admin/src/api/settings.ts | 34 ++ admin/src/api/stats.ts | 68 ++++ admin/src/api/tenants.ts | 95 +++++ .../art-header-bar/widget/ArtUserMenu.vue | 5 +- .../core/layouts/art-screen-lock/index.vue | 5 +- admin/src/router/guards/beforeEach.ts | 82 +++- admin/src/store/modules/auth.ts | 188 ++++++++++ admin/src/store/modules/user.ts | 4 +- admin/src/types/api/api.d.ts | 23 +- admin/src/types/import/components.d.ts | 2 - .../auth/force-change-password/index.vue | 41 +- admin/src/views/auth/login/index.vue | 53 +-- admin/vitest.config.ts | 32 ++ 23 files changed, 1809 insertions(+), 132 deletions(-) create mode 100644 admin/src/api/__tests__/auth-store.test.ts create mode 100644 admin/src/api/__tests__/client.test.ts create mode 100644 admin/src/api/__tests__/mask.test.ts create mode 100644 admin/src/api/client.ts create mode 100644 admin/src/api/config.ts create mode 100644 admin/src/api/conversations.ts create mode 100644 admin/src/api/mask.ts create mode 100644 admin/src/api/settings.ts create mode 100644 admin/src/api/stats.ts create mode 100644 admin/src/api/tenants.ts create mode 100644 admin/src/store/modules/auth.ts create mode 100644 admin/vitest.config.ts diff --git a/admin/package.json b/admin/package.json index f3b544b..a46c6c0 100644 --- a/admin/package.json +++ b/admin/package.json @@ -21,6 +21,7 @@ "dev": "vite", "build": "vue-tsc --noEmit && vite build", "serve": "vite preview", + "test": "vitest run", "lint": "eslint", "fix": "eslint --fix", "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,vue,html,md}\"", @@ -60,6 +61,7 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-vue": "^9.27.0", "globals": "^15.9.0", + "happy-dom": "^21.0.1", "prettier": "^3.5.3", "rollup-plugin-visualizer": "^5.12.0", "sass": "^1.81.0", @@ -79,6 +81,7 @@ "vite": "^7.1.5", "vite-plugin-compression": "^0.5.1", "vite-plugin-vue-devtools": "^7.7.6", + "vitest": "^3.2.4", "vue-tsc": "~2.1.6" } } diff --git a/admin/pnpm-lock.yaml b/admin/pnpm-lock.yaml index b88b1d6..71e1870 100644 --- a/admin/pnpm-lock.yaml +++ b/admin/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: globals: specifier: ^15.9.0 version: 15.15.0 + happy-dom: + specifier: ^21.0.1 + version: 21.0.1 prettier: specifier: ^3.5.3 version: 3.6.2 @@ -156,6 +159,9 @@ importers: vite-plugin-vue-devtools: specifier: ^7.7.6 version: 7.7.7(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@24.8.1)(happy-dom@21.0.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue-tsc: specifier: ~2.1.6 version: 2.1.10(typescript@5.6.3) @@ -941,6 +947,12 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -965,6 +977,12 @@ packages: '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.2': + resolution: {integrity: sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==} + '@typescript-eslint/eslint-plugin@8.44.1': resolution: {integrity: sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1031,6 +1049,35 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vue: ^3.2.25 + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + '@volar/language-core@2.4.23': resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} @@ -1173,6 +1220,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -1224,10 +1275,18 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cacheable@2.0.2: resolution: {integrity: sha512-dWjhLx8RWnPsAWVKwW/wI6OJpQ/hSVb1qS0NUif8TR9vRiSwci7Gey8x04kRU9iAF+Rnbtex5Kjjfg/aB5w8Pg==} @@ -1242,10 +1301,18 @@ packages: caniuse-lite@1.0.30001745: resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1342,6 +1409,10 @@ packages: supports-color: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1426,6 +1497,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -1561,6 +1636,10 @@ packages: resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -1725,6 +1804,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + happy-dom@21.0.1: + resolution: {integrity: sha512-pRBs/u9fhrncyByOl58DMex0R2uAj6ZPKWIjV5j5yNGZBqLw3P6BTRhlc0DD5b/Ht4/6JYTMuE2LJhUWRC6SGw==} + engines: {node: '>=20.0.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2012,6 +2095,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2178,6 +2264,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -2383,6 +2473,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2418,6 +2511,12 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2539,10 +2638,28 @@ packages: engines: {node: '>=10'} hasBin: true + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2661,6 +2778,11 @@ packages: peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-compression@0.5.1: resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} peerDependencies: @@ -2727,6 +2849,34 @@ packages: yaml: optional: true + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -2784,6 +2934,10 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -2793,6 +2947,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2805,6 +2964,18 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -3507,6 +3678,13 @@ snapshots: tailwindcss: 4.1.14 vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -3527,6 +3705,12 @@ snapshots: '@types/web-bluetooth@0.0.21': {} + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.2': + dependencies: + '@types/node': 24.8.1 + '@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3626,6 +3810,48 @@ snapshots: vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.6.3) + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.19 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@volar/language-core@2.4.23': dependencies: '@volar/source-map': 2.4.23 @@ -3840,6 +4066,8 @@ snapshots: array-union@2.1.0: {} + assertion-error@2.0.1: {} + astral-regex@2.0.0: {} async-validator@4.2.5: {} @@ -3889,10 +4117,16 @@ snapshots: buffer-from@1.1.2: {} + buffer-image-size@0.6.4: + dependencies: + '@types/node': 24.8.1 + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 + cac@6.7.14: {} + cacheable@2.0.2: dependencies: '@cacheable/memoize': 2.0.2 @@ -3910,11 +4144,21 @@ snapshots: caniuse-lite@1.0.30001745: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + check-error@2.1.3: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -4001,6 +4245,8 @@ snapshots: dependencies: ms: 2.1.3 + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deep-pick-omit@1.2.1: {} @@ -4092,6 +4338,8 @@ snapshots: entities@4.5.0: {} + entities@7.0.1: {} + env-paths@2.2.1: {} error-ex@1.3.4: @@ -4282,6 +4530,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.4.0: {} + exsolve@1.0.7: {} fast-deep-equal@3.1.3: {} @@ -4447,6 +4697,19 @@ snapshots: graphemer@1.4.0: {} + happy-dom@21.0.1: + dependencies: + '@types/node': 24.8.1 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.2 + buffer-image-size: 0.6.4 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.22.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -4657,6 +4920,8 @@ snapshots: lodash@4.17.21: {} + loupe@3.2.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -4802,6 +5067,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} @@ -4985,6 +5252,8 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} sirv@3.0.2: @@ -5014,6 +5283,10 @@ snapshots: speakingurl@14.0.1: {} + stackback@0.0.2: {} + + std-env@3.10.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5180,11 +5453,21 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5315,6 +5598,27 @@ snapshots: dependencies: vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node@3.2.4(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-compression@0.5.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: chalk: 4.1.2 @@ -5389,6 +5693,48 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vitest@3.2.7(@types/node@24.8.1)(happy-dom@21.0.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.19 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.8.1 + happy-dom: 21.0.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-uri@3.1.0: {} vue-demi@0.14.10(vue@3.5.22(typescript@5.6.3)): @@ -5443,6 +5789,8 @@ snapshots: webpack-virtual-modules@0.6.2: {} + whatwg-mimetype@3.0.0: {} + which@1.3.1: dependencies: isexe: 2.0.0 @@ -5451,6 +5799,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -5464,6 +5817,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@8.22.0: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 diff --git a/admin/src/api/__tests__/auth-store.test.ts b/admin/src/api/__tests__/auth-store.test.ts new file mode 100644 index 0000000..8bb2a38 --- /dev/null +++ b/admin/src/api/__tests__/auth-store.test.ts @@ -0,0 +1,211 @@ +/** + * auth store 单元测试(Phase 3 Task 3) + * + * 覆盖: + * - login 成功存 token/role/tenant_slug/must_change_password/username(含模板侧同步) + * - logout:调 API + 清空状态 + 模板侧 logOut(API 失败也照常本地清) + * - localStorage 持久化(all-kefu 前缀键;新建 pinia 实例模拟刷新恢复) + * - handleSessionExpired / handleMustChangePassword / changePassword + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' +import { createApp, defineComponent } from 'vue' +import { createPersistedState } from 'pinia-plugin-persistedstate' + +const mocks = vi.hoisted(() => ({ + fetchLogin: vi.fn(), + fetchLogout: vi.fn(), + fetchChangePassword: vi.fn(), + userStore: { + logOut: vi.fn(), + setLoginStatus: vi.fn(), + setToken: vi.fn(), + setUserInfo: vi.fn(), + checkAndClearWorktabs: vi.fn(), + info: {} + }, + routerPush: vi.fn(), + routerReplace: vi.fn(), + currentRoute: { value: { path: '/' } } +})) + +vi.mock('@/api/auth', () => ({ + fetchLogin: mocks.fetchLogin, + fetchLogout: mocks.fetchLogout, + fetchChangePassword: mocks.fetchChangePassword +})) + +vi.mock('@/store/modules/user', () => ({ + useUserStore: () => mocks.userStore +})) + +vi.mock('@/router', () => ({ + router: { push: mocks.routerPush, replace: mocks.routerReplace, currentRoute: mocks.currentRoute } +})) + +import { useAuthStore } from '@/store/modules/auth' + +const LOGIN_RESPONSE = { + token: 'tok-abc', + role: 'super', + tenant_slug: null, + must_change_password: true +} as const + +/** + * 等价应用的持久化 pinia: + * pinia.use() 在未安装 app 时(_a 为空)插件只进 toBeInstalled 队列, + * 必须 app.use(pinia) 才真正生效——真实应用经 initStore(main.ts)如此; + * 测试里用一次性 app 复刻同一装配,否则持久化插件静默不运行。 + */ +function freshPersistedPinia() { + const pinia = createPinia() + pinia.use(createPersistedState()) + createApp(defineComponent(() => () => null)).use(pinia) + setActivePinia(pinia) + return pinia +} + +describe('auth store(src/store/modules/auth.ts)', () => { + beforeEach(() => { + localStorage.clear() + vi.clearAllMocks() + mocks.currentRoute.value.path = '/' + }) + + it('login 成功:存 token/role/tenantSlug/mustChangePassword/username 并同步模板侧', async () => { + freshPersistedPinia() + mocks.fetchLogin.mockResolvedValue({ ...LOGIN_RESPONSE }) + + const store = useAuthStore() + await store.login({ username: 'admin', password: 'admin123' }) + + expect(store.token).toBe('tok-abc') + expect(store.role).toBe('super') + expect(store.tenantSlug).toBeNull() + expect(store.mustChangePassword).toBe(true) + expect(store.username).toBe('admin') + expect(store.isAuthenticated).toBe(true) + + // 模板侧(user store)同步:token / 登录标记 / 用户信息(菜单过滤用 roles) + expect(mocks.userStore.setToken).toHaveBeenCalledWith('tok-abc', '') + expect(mocks.userStore.setLoginStatus).toHaveBeenCalledWith(true) + expect(mocks.userStore.setUserInfo).toHaveBeenCalled() + const info = mocks.userStore.setUserInfo.mock.calls[0][0] + expect(info.roles).toEqual(['super']) + expect(info.userName).toBe('admin') + }) + + it('login 失败:状态保持空、错误向上抛', async () => { + freshPersistedPinia() + mocks.fetchLogin.mockRejectedValue(new Error('用户名或密码错误')) + + const store = useAuthStore() + await expect(store.login({ username: 'a', password: 'b' })).rejects.toThrow('用户名或密码错误') + + expect(store.token).toBe('') + expect(store.isAuthenticated).toBe(false) + expect(mocks.userStore.setLoginStatus).not.toHaveBeenCalled() + }) + + it('logout:调登出 API + 清空状态 + 模板侧 logOut', async () => { + freshPersistedPinia() + mocks.fetchLogin.mockResolvedValue({ ...LOGIN_RESPONSE }) + mocks.fetchLogout.mockResolvedValue({ ok: true }) + + const store = useAuthStore() + await store.login({ username: 'admin', password: 'x' }) + + await store.logout() + + expect(mocks.fetchLogout).toHaveBeenCalledTimes(1) + expect(store.token).toBe('') + expect(store.role).toBe('') + expect(store.tenantSlug).toBeNull() + expect(store.username).toBe('') + expect(store.mustChangePassword).toBe(false) + expect(store.isAuthenticated).toBe(false) + expect(mocks.userStore.logOut).toHaveBeenCalledTimes(1) + }) + + it('logout 时后端已不可达:仍完成本地清理(best effort)', async () => { + freshPersistedPinia() + mocks.fetchLogin.mockResolvedValue({ ...LOGIN_RESPONSE }) + mocks.fetchLogout.mockRejectedValue(new Error('network down')) + + const store = useAuthStore() + await store.login({ username: 'admin', password: 'x' }) + + await expect(store.logout()).resolves.toBeUndefined() + + expect(store.token).toBe('') + expect(mocks.userStore.logOut).toHaveBeenCalledTimes(1) + }) + + it('localStorage 持久化:all-kefu 前缀键;新 pinia 实例(模拟刷新)恢复会话', async () => { + freshPersistedPinia() + mocks.fetchLogin.mockResolvedValue({ ...LOGIN_RESPONSE, role: 'tenant', tenant_slug: 'jiancai', must_change_password: false }) + + await useAuthStore().login({ username: 'jcadmin', password: 'x' }) + + // 持久化键带 all-kefu 前缀 + expect(localStorage.getItem('all-kefu-auth')).toBeTruthy() + + // 模拟页面刷新:全新 pinia + 插件,从 localStorage 恢复 + freshPersistedPinia() + const restored = useAuthStore() + expect(restored.token).toBe('tok-abc') + expect(restored.role).toBe('tenant') + expect(restored.tenantSlug).toBe('jiancai') + expect(restored.username).toBe('jcadmin') + expect(restored.mustChangePassword).toBe(false) + expect(restored.isAuthenticated).toBe(true) + }) + + it('handleSessionExpired:清空会话并委托模板侧 logOut(401 全局处理路径)', () => { + freshPersistedPinia() + const store = useAuthStore() + store.token = 'tok' + store.role = 'super' + store.username = 'admin' + + store.handleSessionExpired() + + expect(store.token).toBe('') + expect(store.isAuthenticated).toBe(false) + expect(mocks.userStore.logOut).toHaveBeenCalledTimes(1) + }) + + it('handleMustChangePassword:置标志并跳强制改密页;已在改密页不重复跳', async () => { + freshPersistedPinia() + const store = useAuthStore() + + await store.handleMustChangePassword() + + expect(store.mustChangePassword).toBe(true) + await vi.waitFor(() => { + expect(mocks.routerReplace).toHaveBeenCalledWith('/auth/force-change-password') + }) + + // 已在改密页:不再跳 + mocks.routerReplace.mockClear() + mocks.currentRoute.value.path = '/auth/force-change-password' + await store.handleMustChangePassword() + expect(mocks.routerReplace).not.toHaveBeenCalled() + }) + + it('changePassword 成功:清 must_change 标志', async () => { + freshPersistedPinia() + mocks.fetchChangePassword.mockResolvedValue({ ok: true, must_change_password: false }) + const store = useAuthStore() + store.mustChangePassword = true + + await store.changePassword({ old_password: 'old', new_password: 'new-pass-123' }) + + expect(mocks.fetchChangePassword).toHaveBeenCalledWith({ + old_password: 'old', + new_password: 'new-pass-123' + }) + expect(store.mustChangePassword).toBe(false) + }) +}) diff --git a/admin/src/api/__tests__/client.test.ts b/admin/src/api/__tests__/client.test.ts new file mode 100644 index 0000000..262c80e --- /dev/null +++ b/admin/src/api/__tests__/client.test.ts @@ -0,0 +1,238 @@ +/** + * HTTP client 单元测试(Phase 3 Task 3) + * + * 契约对齐后端 app/platform/auth.py 与 auth_routes.py: + * - Authorization: Bearer 注入 + * - 401(非登录请求)→ 清会话 + 跳登录(登录请求自身的 401 = 凭据错误,只提示不跳转) + * - 409 detail === "必须先修改初始密码" → 跳强制改密页; + * 其余 409(并发冲突等)为普通业务错误,不跳转 + * - 其余错误 / 网络错误 → ElMessage 出 detail / 统一文案 + * - 成功透传 response.data + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios' +import { createPinia, setActivePinia } from 'pinia' + +const mocks = vi.hoisted(() => ({ + userStore: { + logOut: vi.fn(), + setLoginStatus: vi.fn(), + setToken: vi.fn(), + setUserInfo: vi.fn(), + checkAndClearWorktabs: vi.fn(), + info: {} + }, + routerPush: vi.fn(), + routerReplace: vi.fn(), + currentRoute: { value: { path: '/' } }, + elMessageError: vi.fn() +})) + +vi.mock('element-plus', () => ({ + ElMessage: { error: mocks.elMessageError, success: vi.fn() } +})) + +vi.mock('@/store/modules/user', () => ({ + useUserStore: () => mocks.userStore +})) + +vi.mock('@/router', () => ({ + router: { push: mocks.routerPush, replace: mocks.routerReplace, currentRoute: mocks.currentRoute } +})) + +import { ElMessage } from 'element-plus' +import { http } from '@/api/client' +import { useAuthStore } from '@/store/modules/auth' + +/** 自定义 axios adapter:按计划响应回放,并捕获请求头 */ +function makeAdapter(plan: { status: number; body: unknown } | { networkError: true }) { + let captured: InternalAxiosRequestConfig | null = null + const adapter = (config: InternalAxiosRequestConfig): Promise => { + captured = config + if ('networkError' in plan) { + // 模拟断网:无 response 的 AxiosError + return Promise.reject( + new AxiosError('Network Error', 'ECONNABORTED', config, undefined, undefined) + ) + } + const response: AxiosResponse = { + status: plan.status, + statusText: '', + headers: {}, + config, + data: plan.body + } + if (plan.status >= 200 && plan.status < 300) return Promise.resolve(response) + return Promise.reject( + new AxiosError( + `Request failed with status code ${plan.status}`, + String(plan.status), + config, + undefined, + response + ) + ) + } + return { + adapter, + capturedHeaders: () => (captured as InternalAxiosRequestConfig | null)?.headers + } +} + +describe('HTTP client(src/api/client.ts)', () => { + beforeEach(() => { + setActivePinia(createPinia()) + localStorage.clear() + vi.clearAllMocks() + mocks.currentRoute.value.path = '/' + }) + + describe('token 注入', () => { + it('有 token 时自动注入 Authorization: Bearer ', async () => { + const authStore = useAuthStore() + authStore.token = 'tok-123' + + const { adapter, capturedHeaders } = makeAdapter({ status: 200, body: { ok: true } }) + await http.post<{ ok: boolean }>('/api/platform/auth/logout', null, { adapter }) + + expect(capturedHeaders()?.get('Authorization')).toBe('Bearer tok-123') + }) + + it('无 token 时不注入 Authorization 头', async () => { + const { adapter, capturedHeaders } = makeAdapter({ status: 200, body: { ok: true } }) + await http.post('/api/platform/auth/logout', null, { adapter }) + + expect(capturedHeaders()?.get('Authorization')).toBeFalsy() + }) + }) + + describe('401 处理', () => { + it('非登录请求 401:清空会话并触发登出清理(跳登录)', async () => { + const authStore = useAuthStore() + authStore.token = 'expired-token' + + const { adapter } = makeAdapter({ + status: 401, + body: { detail: '未认证或凭证已失效' } + }) + await expect(http.get('/api/platform/tenants', { adapter })).rejects.toThrow( + '未认证或凭证已失效' + ) + + expect(authStore.token).toBe('') + expect(authStore.isAuthenticated).toBe(false) + // 模板侧全套清理(含跳登录页)由 userStore.logOut 承担 + expect(mocks.userStore.logOut).toHaveBeenCalled() + expect(ElMessage.error).toHaveBeenCalledWith('未认证或凭证已失效') + }) + + it('登录请求自身 401(凭据错误):只提示 detail,不触发登出/跳转', async () => { + const { adapter } = makeAdapter({ + status: 401, + body: { detail: '用户名或密码错误' } + }) + await expect( + http.post('/api/platform/auth/login', { username: 'a', password: 'b' }, { adapter }) + ).rejects.toThrow('用户名或密码错误') + + expect(mocks.userStore.logOut).not.toHaveBeenCalled() + expect(mocks.routerReplace).not.toHaveBeenCalledWith('/auth/force-change-password') + expect(ElMessage.error).toHaveBeenCalledWith('用户名或密码错误') + }) + }) + + describe('409 处理', () => { + it('409 detail 为必须先修改初始密码:置 must_change 并跳强制改密页', async () => { + const authStore = useAuthStore() + authStore.token = 'tok' + authStore.mustChangePassword = false + + const { adapter } = makeAdapter({ + status: 409, + body: { detail: '必须先修改初始密码' } + }) + await expect(http.get('/api/platform/stats', { adapter })).rejects.toThrow( + '必须先修改初始密码' + ) + + await vi.waitFor(() => { + expect(mocks.routerReplace).toHaveBeenCalledWith('/auth/force-change-password') + }) + expect(authStore.mustChangePassword).toBe(true) + }) + + it('已在强制改密页时收到 409:不再重复跳转', async () => { + mocks.currentRoute.value.path = '/auth/force-change-password' + const { adapter } = makeAdapter({ + status: 409, + body: { detail: '必须先修改初始密码' } + }) + await expect(http.get('/api/platform/stats', { adapter })).rejects.toThrow() + + await vi.waitFor(() => { + expect(ElMessage.error).toHaveBeenCalled() + }) + expect(mocks.routerReplace).not.toHaveBeenCalled() + }) + + it('其他 409(并发冲突):普通业务错误,不跳改密页', async () => { + const { adapter } = makeAdapter({ + status: 409, + body: { detail: '并发冲突: 域名已被占用' } + }) + await expect(http.post('/api/platform/tenants', {}, { adapter })).rejects.toThrow( + '并发冲突: 域名已被占用' + ) + + expect(mocks.routerReplace).not.toHaveBeenCalled() + expect(ElMessage.error).toHaveBeenCalledWith('并发冲突: 域名已被占用') + }) + }) + + describe('其他错误与成功', () => { + it('登录 429 限流:弹出后端 detail 提示,不触发跳转', async () => { + const { adapter } = makeAdapter({ + status: 429, + body: { detail: '登录尝试过于频繁,请15分钟后再试' } + }) + await expect( + http.post('/api/platform/auth/login', { username: 'a', password: 'b' }, { adapter }) + ).rejects.toThrow('登录尝试过于频繁,请15分钟后再试') + + expect(ElMessage.error).toHaveBeenCalledWith('登录尝试过于频繁,请15分钟后再试') + expect(mocks.userStore.logOut).not.toHaveBeenCalled() + expect(mocks.routerReplace).not.toHaveBeenCalled() + }) + + it('网络错误(无响应):统一文案提示', async () => { + const { adapter } = makeAdapter({ networkError: true }) + await expect(http.get('/api/platform/stats', { adapter })).rejects.toThrow() + + expect(ElMessage.error).toHaveBeenCalledTimes(1) + const message = vi.mocked(ElMessage.error).mock.calls[0][0] as string + expect(message).toContain('网络') + }) + + it('showErrorMessage: false 时不弹全局错误提示', async () => { + const { adapter } = makeAdapter({ status: 400, body: { detail: '原密码不正确' } }) + await expect( + http.post('/api/platform/auth/change-password', {}, { adapter, showErrorMessage: false }) + ).rejects.toThrow('原密码不正确') + + expect(ElMessage.error).not.toHaveBeenCalled() + }) + + it('成功:直接透传 response.data(非 axios 包装)', async () => { + const body = { + token: 't', + role: 'super', + tenant_slug: null, + must_change_password: true + } + const { adapter } = makeAdapter({ status: 200, body }) + const res = await http.post('/api/platform/auth/login', {}, { adapter }) + + expect(res).toEqual(body) + }) + }) +}) diff --git a/admin/src/api/__tests__/mask.test.ts b/admin/src/api/__tests__/mask.test.ts new file mode 100644 index 0000000..5564d5c --- /dev/null +++ b/admin/src/api/__tests__/mask.test.ts @@ -0,0 +1,61 @@ +/** + * 掩码哨兵辅助纯函数测试(Phase 3 Task 3) + * + * 契约对齐后端 app/platform/routes/config_routes.py: + * 密钥字段 GET 响应非空时回 `__SET__`(MASKED 哨兵),PUT 提交 `__SET__` + * 表示"保持库值"。本模块负责 `__SET__` ↔ 表单"已设置"语义的相互转换, + * 配置中心 / 平台设置页(Task 7/9)消费。 + */ +import { describe, expect, it } from 'vitest' +import { + MASKED_DISPLAY, + MASKED_SENTINEL, + isMaskedSentinel, + secretPlaceholder, + secretToSubmit +} from '@/api/mask' + +describe('掩码哨兵辅助(src/api/mask.ts)', () => { + it('哨兵识别:__SET__ 为哨兵,其他值不是', () => { + expect(isMaskedSentinel('__SET__')).toBe(true) + expect(isMaskedSentinel('sk-real-key')).toBe(false) + expect(isMaskedSentinel('')).toBe(false) + expect(isMaskedSentinel(null)).toBe(false) + expect(isMaskedSentinel(undefined)).toBe(false) + }) + + it('GET 掩码值 → 表单占位语义:已设置提示可覆盖,未设置提示待输入', () => { + expect(MASKED_SENTINEL).toBe('__SET__') + expect(MASKED_DISPLAY).toBe('已设置') + // 后端下发的密钥字段只可能是哨兵(已设置)或空串/缺省(未设置) + expect(secretPlaceholder('__SET__')).toBe('已设置,输入新值覆盖') + expect(secretPlaceholder('')).toBe('未设置,输入新值') + expect(secretPlaceholder(null)).toBe('未设置,输入新值') + expect(secretPlaceholder(undefined)).toBe('未设置,输入新值') + }) + + it('表单值 → 提交值:未改动(空输入 + 原本已设置)回传哨兵保持库值', () => { + // 用户没有输入新值,且库里有值 → 回 __SET__,后端保持原值不落哨兵字面量 + expect(secretToSubmit('', '__SET__')).toBe('__SET__') + }) + + it('表单值 → 提交值:输入了新值则提交新值(覆盖)', () => { + expect(secretToSubmit('sk-new-key', '__SET__')).toBe('sk-new-key') + expect(secretToSubmit('sk-first-key', '')).toBe('sk-first-key') + expect(secretToSubmit('sk-first-key', null)).toBe('sk-first-key') + }) + + it('表单值 → 提交值:未设置且留空 → 提交空串(保持未设置)', () => { + expect(secretToSubmit('', '')).toBe('') + expect(secretToSubmit('', null)).toBe('') + }) + + it('往返一致性:掩码字段未改动时,提交值仍是哨兵(后端可识别为"保持")', () => { + const fromServer = '__SET__' + const displayed = secretPlaceholder(fromServer) + expect(displayed).toContain('已设置') + + const submitted = secretToSubmit('', fromServer) + expect(isMaskedSentinel(submitted)).toBe(true) + }) +}) diff --git a/admin/src/api/auth.ts b/admin/src/api/auth.ts index fc35461..3c08637 100644 --- a/admin/src/api/auth.ts +++ b/admin/src/api/auth.ts @@ -1,60 +1,38 @@ /** - * 认证 API——脚手架阶段(Phase 3 Task 2)的开发桩。 + * 认证 API(Phase 3 Task 3 实装,替换 Task 2 开发桩) * - * 后端真实端点为 /api/platform/auth/*(登录 / 用户信息 / 改密), - * Task 3 对接真实登录时将本文件替换为真实请求。 + * 对接后端 app/platform/routes/auth_routes.py: + * - POST /api/platform/auth/login {username, password} + * → {token, role: "super"|"tenant", tenant_slug, must_change_password} + * - POST /api/platform/auth/logout(Bearer;撤销当前 token) + * - POST /api/platform/auth/change-password {old_password, new_password} + * → {ok, must_change_password} * - * 桩行为: - * - fetchLogin:开发态直接发放本地会话令牌,不发起网络请求; - * - fetchGetUserInfo:按登录页的角色调试开关(sessionStorage - * `all-kefu-dev-role`:super | tenant)返回用户信息,用于演示 - * 纯前端菜单权限控制——super 全量菜单,tenant 隐藏 - * 租户管理 / 平台设置(见 router/modules/*.ts 的 meta.roles); - * - 生产构建(非 DEV)下调用将抛出显式错误,避免带桩上线。 + * 错误语义(HTTP client 统一处理,见 src/api/client.ts): + * - 401 凭据错误/会话过期(登录请求自身 401 只提示不跳转) + * - 429 登录限流(5 次/15 分钟) + * - 400 原密码不正确 */ -const DEV_ROLE_KEY = 'all-kefu-dev-role' -export type DevStubRole = 'super' | 'tenant' +import { http } from './client' +import type { LoginParams, LoginResponse, ChangePasswordParams } from '@/store/modules/auth' -export function setDevStubRole(role: DevStubRole) { - sessionStorage.setItem(DEV_ROLE_KEY, role) +/** 登录 */ +export function fetchLogin(params: LoginParams): Promise { + return http.post('/api/platform/auth/login', params) } -export function getDevStubRole(): DevStubRole { - return sessionStorage.getItem(DEV_ROLE_KEY) === 'tenant' ? 'tenant' : 'super' +/** 登出(撤销当前 token;需 Bearer) */ +export function fetchLogout(): Promise<{ ok: boolean }> { + return http.post<{ ok: boolean }>('/api/platform/auth/logout') } -function assertStubAllowed(): void { - if (!import.meta.env.DEV) { - throw new Error('认证 API 尚未对接真实后端(Task 3 实现),生产构建不可用') - } -} - -/** - * 登录(开发桩:任意账号密码直接获得开发会话) - * @param params 登录参数 - * @returns 登录响应 - */ -export function fetchLogin(params: Api.Auth.LoginParams) { - assertStubAllowed() - void params - return Promise.resolve({ - token: 'dev-stub-token', - refreshToken: '' - }) -} - -/** - * 获取用户信息(开发桩:角色取自登录页调试开关) - * @returns 用户信息 - */ -export function fetchGetUserInfo() { - assertStubAllowed() - const role = getDevStubRole() - return Promise.resolve({ - buttons: [], - roles: [role], - userId: 1, - userName: role === 'super' ? '平台超管(调试)' : '租户管理员(调试)', - email: 'dev@all-kefu.local' - }) +/** 修改密码(验旧密 → 写新 hash;改完当前会话保持有效) */ +export function fetchChangePassword(params: ChangePasswordParams): Promise<{ + ok: boolean + must_change_password: boolean +}> { + return http.post<{ ok: boolean; must_change_password: boolean }>( + '/api/platform/auth/change-password', + params + ) } diff --git a/admin/src/api/client.ts b/admin/src/api/client.ts new file mode 100644 index 0000000..f0a497a --- /dev/null +++ b/admin/src/api/client.ts @@ -0,0 +1,143 @@ +/** + * 平台 API HTTP client(Phase 3 Task 3,自建薄封装) + * + * ## 为什么不用模板的 src/utils/http + * + * 模板封装与后端约定对不上,改造成本高于自建: + * - 响应:模板要求 `{code, msg, data}` 业务信封;后端(FastAPI)业务语义 + * 直用 HTTP 状态码,成功响应就是数据本身 + * - 错误:模板读 `data.msg`;FastAPI 错误体是 `{detail: 中文}` + * - 凭据:模板裸塞 `Authorization: `;后端要求 `Bearer ` + * - 模板无 409 强制改密语义 + * + * ## 本 client 约定 + * + * - 请求拦截:auth store 有 token 时自动注入 `Authorization: Bearer ` + * - 401(非登录请求):清会话 + 跳登录(登录请求自身的 401 = 凭据错误, + * 只弹 detail,不触发登出/跳转——否则登录页死循环) + * - 409 且 detail 为强制改密文案:跳强制改密页;其余 409(并发冲突等) + * 是普通业务错误,只弹 detail(后端 409 双语义:enforce_password_changed + * 与租户/域名并发冲突,见 app/platform/routes/) + * - 其余错误/网络错误:ElMessage 出后端 detail / 统一网络文案 + * - 成功:直接透传 response.data + * - 请求配置 showErrorMessage: false 可关闭全局错误提示(调用方自行处理) + */ +import axios, { + AxiosError, + type AxiosRequestConfig, + type AxiosResponse, + type InternalAxiosRequestConfig +} from 'axios' +import { ElMessage } from 'element-plus' +import { useAuthStore } from '@/store/modules/auth' + +/** + * 强制改密 409 的 detail 契约(app/platform/auth.py enforce_password_changed + * 的静态文案)。只有命中该文案的 409 才触发改密跳转。 + */ +export const MUST_CHANGE_DETAIL = '必须先修改初始密码' + +/** 网络错误统一文案(无响应时后端不可能给 detail) */ +export const NETWORK_ERROR_MESSAGE = '网络连接异常,请检查网络后重试' + +/** 登录端点路径(其 401/429 是凭据/限流业务错误,不走会话过期处理) */ +const LOGIN_URL = '/api/platform/auth/login' + +/** 统一 API 错误(供调用方按需识别状态码) */ +export class ApiError extends Error { + readonly status?: number + readonly url?: string + + constructor(message: string, status?: number, url?: string) { + super(message) + this.name = 'ApiError' + this.status = status + this.url = url + } +} + +/** 后端错误体形态(FastAPI HTTPException:{detail: string}) */ +interface ApiErrorBody { + detail?: string +} + +/** 扩展请求配置:允许关闭全局错误提示 */ +export interface RequestOptions extends AxiosRequestConfig { + showErrorMessage?: boolean +} + +const instance = axios.create({ + timeout: 15000, + // 开发走 vite 代理(/),生产同源反代;两环境 VITE_API_URL 均为 '/' + baseURL: import.meta.env.VITE_API_URL || '/', + // 后端业务语义直用 HTTP 状态码,非 2xx 一律进错误拦截器 + validateStatus: (status) => status >= 200 && status < 300 +}) + +/** 请求拦截:Bearer token 注入 */ +instance.interceptors.request.use((config: InternalAxiosRequestConfig) => { + const { token } = useAuthStore() + if (token) { + config.headers.set('Authorization', `Bearer ${token}`) + } + return config +}) + +/** 提取错误提示文案:优先后端 detail,其次统一网络文案 */ +function extractDetail(error: AxiosError): string { + if (error.response?.data?.detail) { + return error.response.data.detail + } + return error.response ? `请求失败(HTTP ${error.response.status})` : NETWORK_ERROR_MESSAGE +} + +/** 响应拦截:401/409 全局处理 + 统一错误提示 */ +instance.interceptors.response.use( + (response: AxiosResponse) => response, + async (error: AxiosError) => { + const status = error.response?.status + const detail = extractDetail(error) + const url = error.config?.url || '' + const config = error.config as (RequestOptions & InternalAxiosRequestConfig) | undefined + const isLoginRequest = url.includes(LOGIN_URL) + + if (status === 401 && !isLoginRequest) { + // 会话过期:清空状态 + 模板侧全套清理(含跳登录,登录页不重复跳) + await useAuthStore().handleSessionExpired() + } else if (status === 409 && detail === MUST_CHANGE_DETAIL) { + // 强制改密:置标志 + 跳改密页(已在改密页不重复跳) + await useAuthStore().handleMustChangePassword() + } + + if (config?.showErrorMessage !== false) { + ElMessage.error(detail) + } + + throw new ApiError(detail, status, url) + } +) + +/** 发起请求,成功透传 response.data */ +async function request(config: RequestOptions): Promise { + const response = await instance.request(config) + return response.data +} + +/** API 方法集合 */ +export const http = { + get(url: string, config?: RequestOptions): Promise { + return request({ ...config, url, method: 'GET' }) + }, + post(url: string, data?: unknown, config?: RequestOptions): Promise { + return request({ ...config, url, method: 'POST', data }) + }, + put(url: string, data?: unknown, config?: RequestOptions): Promise { + return request({ ...config, url, method: 'PUT', data }) + }, + patch(url: string, data?: unknown, config?: RequestOptions): Promise { + return request({ ...config, url, method: 'PATCH', data }) + }, + del(url: string, config?: RequestOptions): Promise { + return request({ ...config, url, method: 'DELETE' }) + } +} diff --git a/admin/src/api/config.ts b/admin/src/api/config.ts new file mode 100644 index 0000000..f47ec6d --- /dev/null +++ b/admin/src/api/config.ts @@ -0,0 +1,78 @@ +/** + * 租户配置中心 API 骨架(Phase 3 Task 3) + * + * 端点事实源:app/platform/routes/config_routes.py + * (配置快照 / 分组编辑 / 版本历史 / 回滚 / 重置 / 试金石)。 + * 本文件只建类型与端点常量——调用函数在 Task 7(配置中心四件)实装。 + * + * 密钥掩码哨兵:GET 的密钥字段非空时为 '__SET__'(见 src/api/mask.ts + * 的表单语义转换);PUT 提交 '__SET__' = 保持库值。 + */ + +/** 配置组名(快照顶层键) */ +export type ConfigGroupKey = 'brand' | 'prompts' | 'wall' | 'push' | 'email' | 'ai' + +/** 当前生效配置响应(GET /api/platform/tenants/{slug}/config) */ +export interface TenantConfigResponse { + slug: string + version: number + /** 掩码视图的整份快照(密钥字段可能为 '__SET__') */ + config: Record> +} + +/** 配置版本行(GET .../config/versions) */ +export interface ConfigVersionItem { + version: number + created_at: string + created_by: string + note: string +} + +/** 版本列表响应 */ +export interface ConfigVersionListResponse { + slug: string + items: ConfigVersionItem[] +} + +/** 版本详情响应(含整份快照) */ +export interface ConfigVersionDetailResponse extends ConfigVersionItem { + slug: string + config: Record> +} + +/** 回滚请求(POST .../config/rollback,产生新版本) */ +export interface ConfigRollbackRequest { + version: number + note: string +} + +/** 分组保存请求(PUT .../config/{group}) */ +export interface PutConfigGroupRequest { + note: string + value: Record +} + +/** 词表试金石请求(POST .../wall/test) */ +export interface WallTestRequest { + sentences: string[] + has_contact?: boolean +} + +/** 词表试金石响应(逐句判定) */ +export interface WallTestResponse { + results: Array<{ sentence: string; blocked: boolean; rule?: string }> +} + +/** 配置中心端点 */ +export const CONFIG_ENDPOINTS = { + current: (slug: string) => `/api/platform/tenants/${slug}/config`, + putGroup: (slug: string, group: ConfigGroupKey | string) => + `/api/platform/tenants/${slug}/config/${group}`, + resetGroup: (slug: string, group: ConfigGroupKey | string) => + `/api/platform/tenants/${slug}/config/${group}/reset`, + versions: (slug: string) => `/api/platform/tenants/${slug}/config/versions`, + versionDetail: (slug: string, version: number) => + `/api/platform/tenants/${slug}/config/versions/${version}`, + rollback: (slug: string) => `/api/platform/tenants/${slug}/config/rollback`, + wallTest: (slug: string) => `/api/platform/tenants/${slug}/wall/test` +} as const diff --git a/admin/src/api/conversations.ts b/admin/src/api/conversations.ts new file mode 100644 index 0000000..5c96762 --- /dev/null +++ b/admin/src/api/conversations.ts @@ -0,0 +1,98 @@ +/** + * 会话与线索 API 骨架(Phase 3 Task 3) + * + * 端点事实源:app/platform/routes/data_routes.py + * (会话列表/详情/删除、推送日志)。本文件只建类型与端点常量—— + * 调用函数在 Task 5(会话与线索)实装。 + */ + +/** 意向层级(Evaluation.intent_level) */ +export type IntentLevel = 'high' | 'medium' | 'low' | 'none' | null + +/** 联系方式(评估产出) */ +export interface ContactInfo { + email?: string | null + whatsapp?: string | null + phone?: string | null +} + +/** 会话列表行(GET /api/platform/tenants/{slug}/conversations) */ +export interface ConversationListItem { + id: string + session_id: string + status: string + source: string + site: string | null + ip: string + device: string + country: string | null + intent_level: IntentLevel + reasoning: string | null + contact_info: ContactInfo | null + email_sent: boolean + message_count: number + preview: string + created_at: string | null +} + +/** 会话列表查询参数(intent 白名单校验在后端;from/to 为闭区间日期) */ +export interface ConversationListParams { + intent?: string + site?: string + from?: string + to?: string + page?: number + page_size?: number +} + +/** 会话列表响应 */ +export interface ConversationListResponse { + total: number + page: number + page_size: number + items: ConversationListItem[] +} + +/** 会话详情消息行(时间升序) */ +export interface ConversationMessage { + role: 'user' | 'assistant' | 'system' | string + content: string + created_at: string | null +} + +/** 会话详情响应(GET .../conversations/{convId}) */ +export interface ConversationDetailResponse extends ConversationListItem { + client_info: Record + messages: ConversationMessage[] + profile: { + chips: Record + summary: string | null + } | null +} + +/** 推送日志行(GET .../push-logs) */ +export interface PushLogItem { + id: number + conversation_id: string + status: string + error: string | null + created_at: string | null +} + +/** 推送日志响应 */ +export interface PushLogResponse { + total: number + page: number + page_size: number + items: PushLogItem[] +} + +/** 会话与线索端点 */ +export const CONVERSATION_ENDPOINTS = { + list: (slug: string) => `/api/platform/tenants/${slug}/conversations`, + detail: (slug: string, convId: string) => + `/api/platform/tenants/${slug}/conversations/${convId}`, + remove: (slug: string, convId: string) => + `/api/platform/tenants/${slug}/conversations/${convId}`, + pushLogs: (slug: string) => `/api/platform/tenants/${slug}/push-logs` +} as const diff --git a/admin/src/api/mask.ts b/admin/src/api/mask.ts new file mode 100644 index 0000000..60efdfc --- /dev/null +++ b/admin/src/api/mask.ts @@ -0,0 +1,44 @@ +/** + * 掩码哨兵辅助(Phase 3 Task 3) + * + * 契约对齐后端 app/platform/routes/config_routes.py: + * - 密钥字段 GET 响应:库中非空 → `__SET__`(MASKED 哨兵);空/缺省 → 原样 + * - PUT 提交:密钥字段传 `__SET__` = 保持库值(后端还原,哨兵字面量绝不落库) + * + * 本模块提供哨兵 ↔ 表单语义的纯函数转换,配置中心(Task 7)与 + * 平台设置(Task 9)的凭据表单消费: + * - secretPlaceholder:GET 掩码值 → 输入框占位文案("已设置,输入新值覆盖") + * - secretToSubmit:用户输入 + 原掩码状态 → 提交值(未改动回哨兵保持库值) + */ + +/** 后端密钥掩码哨兵(config_routes.MASKED) */ +export const MASKED_SENTINEL = '__SET__' + +/** 表单侧"已设置"展示语义 */ +export const MASKED_DISPLAY = '已设置' + +/** 值是否为掩码哨兵 */ +export function isMaskedSentinel(value: unknown): value is typeof MASKED_SENTINEL { + return value === MASKED_SENTINEL +} + +/** + * GET 掩码值 → 表单占位文案 + * + * @param maskedValue 后端下发的密钥字段值(哨兵=已设置;空串/null/undefined=未设置) + */ +export function secretPlaceholder(maskedValue: string | null | undefined): string { + return isMaskedSentinel(maskedValue) ? '已设置,输入新值覆盖' : '未设置,输入新值' +} + +/** + * 表单输入 → 提交值 + * + * @param input 用户输入(空串=未改动/清空) + * @param originalMasked GET 时的掩码值(哨兵=库中已有值;否则=未设置) + * @returns 提交值:输入非空 → 新值;输入空且已设置 → 哨兵(保持库值);输入空且未设置 → 空串 + */ +export function secretToSubmit(input: string, originalMasked: string | null | undefined): string { + if (input !== '') return input + return isMaskedSentinel(originalMasked) ? MASKED_SENTINEL : '' +} diff --git a/admin/src/api/settings.ts b/admin/src/api/settings.ts new file mode 100644 index 0000000..b6f76dd --- /dev/null +++ b/admin/src/api/settings.ts @@ -0,0 +1,34 @@ +/** + * 平台设置 API 骨架(Phase 3 Task 3) + * + * 端点事实源:app/platform/routes/config_routes.py 的 settings_router + * (/api/platform/settings,仅超管:集团 AI 凭据 / 默认 SMTP 等)。 + * 本文件只建类型与端点常量——调用函数在 Task 9(平台设置)实装。 + * + * 密钥掩码:key 名含 key/password/secret 的非空字符串值在列表响应中 + * 掩码为 '__SET__'(嵌套 dict 递归两层);PUT 提交 '__SET__' = 保持库值。 + * 表单语义转换见 src/api/mask.ts。 + */ + +/** 平台设置项(列表响应行;value 可能是掩码哨兵或任意 JSON) */ +export interface PlatformSettingItem { + key: string + value: unknown +} + +/** 平台设置列表响应 */ +export interface PlatformSettingsResponse { + items: PlatformSettingItem[] +} + +/** upsert 单个设置项请求(同 key 再写 = 整值替换) */ +export interface PutPlatformSettingRequest { + key: string + value: unknown +} + +/** 平台设置端点 */ +export const SETTINGS_ENDPOINTS = { + list: '/api/platform/settings', + put: '/api/platform/settings' +} as const diff --git a/admin/src/api/stats.ts b/admin/src/api/stats.ts new file mode 100644 index 0000000..0b57ddb --- /dev/null +++ b/admin/src/api/stats.ts @@ -0,0 +1,68 @@ +/** + * 统计分析 API 骨架(Phase 3 Task 3) + * + * 端点事实源:app/platform/routes/stats_routes.py + app/platform/stats.py + * (平台合并统计 / 单租户统计,?from/&to= 闭区间)。本文件只建类型与 + * 端点常量——调用函数在 Task 4(仪表盘)与 Task 8(统计分析)实装。 + */ + +/** 指标卡(None 项为 null——如无推送时成功率无定义) */ +export interface StatsCards { + conversations: number + leads: number + high_intent: number + push_success_rate: number | null +} + +/** 漏斗计数 */ +export interface StatsFunnel { + conversations: number + leads: number + evaluated: number + push_succeeded: number +} + +/** 按日趋势行 */ +export interface StatsTrendPoint { + date: string + conversations: number + leads: number +} + +/** 分布视图(intent 按枚举键、source/site 计数表、country Top 数组) */ +export interface StatsDistributions { + intent: Record + source: Record + site: Record + country: Array<{ country: string; count: number }> +} + +/** 推送健康计数 */ +export interface PushHealth { + pending: number + success: number + failed: number + skipped: number + duplicated: number +} + +/** 统计响应(平台合并与单租户同构) */ +export interface StatsResponse { + cards: StatsCards + funnel: StatsFunnel + trend: StatsTrendPoint[] + distributions: StatsDistributions + push_health: PushHealth +} + +/** 统计查询参数(from/to 闭区间,ISO 日期) */ +export interface StatsParams { + from?: string + to?: string +} + +/** 统计端点(platform 合并版仅超管;单租户版超管或本租户管理员) */ +export const STATS_ENDPOINTS = { + platform: '/api/platform/stats', + tenant: (slug: string) => `/api/platform/tenants/${slug}/stats` +} as const diff --git a/admin/src/api/tenants.ts b/admin/src/api/tenants.ts new file mode 100644 index 0000000..599f6c9 --- /dev/null +++ b/admin/src/api/tenants.ts @@ -0,0 +1,95 @@ +/** + * 租户与站点管理 API 骨架(Phase 3 Task 3) + * + * 端点事实源:app/platform/routes/tenant_routes.py + * (/api/platform/tenants,require_super / require_tenant(slug) 鉴权)。 + * 本文件只建类型与端点常量——调用函数在 Task 6(租户管理与站点管理)实装。 + */ + +/** 租户状态(Tenant.status) */ +export type TenantStatus = 'active' | 'disabled' + +/** 站点概要(_site_summary:slug/name/status/domains/brand_overlay) */ +export interface SiteSummary { + slug: string + name: string + status: string + domains: string[] + brand_overlay: Record +} + +/** 管理员概要(_admin_summary:不含任何凭据字段) */ +export interface TenantAdminSummary { + username: string + status: string +} + +/** 租户列表行(GET /api/platform/tenants) */ +export interface TenantListItem { + slug: string + name: string + status: TenantStatus + domain_count: number + current_version: number | null + admins: TenantAdminSummary[] +} + +/** 租户列表响应 */ +export interface TenantListResponse { + items: TenantListItem[] +} + +/** 租户详情响应(GET /api/platform/tenants/{slug}) */ +export interface TenantDetailResponse { + slug: string + name: string + status: TenantStatus + sites: SiteSummary[] + admins: TenantAdminSummary[] + current_version: number | null +} + +/** 创建租户请求(POST /api/platform/tenants,201) */ +export interface CreateTenantRequest { + name: string + slug: string + /** 出厂档案:auto | jiancai | blank */ + profile: string + admin_username: string + admin_password: string + domains: string[] +} + +/** 改名/改状态请求(PATCH /api/platform/tenants/{slug}) */ +export interface PatchTenantRequest { + name?: string + status?: TenantStatus +} + +/** 创建站点请求(POST /api/platform/tenants/{slug}/sites,201) */ +export interface CreateSiteRequest { + slug: string + name?: string + brand_overlay?: Record + domains?: string[] +} + +/** 绑定域名请求(POST .../sites/{siteSlug}/domains,201;409=域名已被占用) */ +export interface AddDomainRequest { + domain: string +} + +/** 租户管理端点(鉴权与 409 并发冲突语义见 tenant_routes.py) */ +export const TENANT_ENDPOINTS = { + list: '/api/platform/tenants', + create: '/api/platform/tenants', + detail: (slug: string) => `/api/platform/tenants/${slug}`, + patch: (slug: string) => `/api/platform/tenants/${slug}`, + createSite: (slug: string) => `/api/platform/tenants/${slug}/sites`, + patchSite: (slug: string, siteSlug: string) => + `/api/platform/tenants/${slug}/sites/${siteSlug}`, + addDomain: (slug: string, siteSlug: string) => + `/api/platform/tenants/${slug}/sites/${siteSlug}/domains`, + deleteDomain: (slug: string, siteSlug: string, domain: string) => + `/api/platform/tenants/${slug}/sites/${siteSlug}/domains/${domain}` +} as const diff --git a/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue b/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue index 7b9a370..c8a4a11 100644 --- a/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue +++ b/admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue @@ -56,6 +56,7 @@ import { useRouter } from 'vue-router' import { ElMessageBox } from 'element-plus' import { useUserStore } from '@/store/modules/user' + import { useAuthStore } from '@/store/modules/auth' import { mittBus } from '@/utils/sys' defineOptions({ name: 'ArtUserMenu' }) @@ -63,6 +64,7 @@ const router = useRouter() const { t } = useI18n() const userStore = useUserStore() + const authStore = useAuthStore() const { getUserInfo: userInfo } = storeToRefs(userStore) const userMenuPopover = ref() @@ -93,7 +95,8 @@ cancelButtonText: t('common.cancel'), customClass: 'login-out-dialog' }).then(() => { - userStore.logOut() + // 先调后端撤销 token(best effort),再走模板全套本地清理 + 跳登录 + void authStore.logout() }) }, 200) } diff --git a/admin/src/components/core/layouts/art-screen-lock/index.vue b/admin/src/components/core/layouts/art-screen-lock/index.vue index 5bf5248..d7002a9 100644 --- a/admin/src/components/core/layouts/art-screen-lock/index.vue +++ b/admin/src/components/core/layouts/art-screen-lock/index.vue @@ -112,6 +112,7 @@ import { useI18n } from 'vue-i18n' import CryptoJS from 'crypto-js' import { useUserStore } from '@/store/modules/user' + import { useAuthStore } from '@/store/modules/auth' import { mittBus } from '@/utils/sys' // 国际化 @@ -122,6 +123,7 @@ // Store const userStore = useUserStore() + const authStore = useAuthStore() const { info: userInfo, lockPassword, isLock } = storeToRefs(userStore) // 响应式数据 @@ -400,7 +402,8 @@ } const toLogin = () => { - userStore.logOut() + // 锁屏页"返回登录":先撤销后端 token,再走本地清理 + 跳登录 + void authStore.logout() } const openLockScreen = () => { diff --git a/admin/src/router/guards/beforeEach.ts b/admin/src/router/guards/beforeEach.ts index bc97e09..d66cbc8 100644 --- a/admin/src/router/guards/beforeEach.ts +++ b/admin/src/router/guards/beforeEach.ts @@ -40,6 +40,7 @@ import { nextTick } from 'vue' import NProgress from 'nprogress' import { useSettingStore } from '@/store/modules/setting' import { useUserStore } from '@/store/modules/user' +import { useAuthStore } from '@/store/modules/auth' import { useMenuStore } from '@/store/modules/menu' import { setWorktab } from '@/utils/navigation' import { setPageTitle } from '@/utils/router' @@ -48,7 +49,6 @@ import { staticRoutes } from '../routes/staticRoutes' import { loadingService } from '@/utils/ui' import { useCommon } from '@/hooks/core/useCommon' import { useWorktabStore } from '@/store/modules/worktab' -import { fetchGetUserInfo } from '@/api/auth' import { ApiStatus } from '@/utils/http/status' import { isHttpError } from '@/utils/http/error' import { RouteRegistry, MenuProcessor, IframeRouteManager, RoutePermissionValidator } from '../core' @@ -151,12 +151,18 @@ async function handleRouteGuard( NProgress.start() } - // 1. 检查登录状态 + // 1. 检查登录状态(会话事实源 = auth store 的 token) if (!handleLoginStatus(to, userStore, next)) { return } - // 2. 检查路由初始化是否已失败(防止死循环) + // 2. 强制改密门:会话有效但 must_change → 除改密页外一律重定向 + // (含登录页——已登录的引导流程不回登录;后端 409 语义的前端镜像) + if (!handleMustChangePassword(to, next)) { + return + } + + // 3. 检查路由初始化是否已失败(防止死循环) if (routeInitFailed) { // 已经失败过,直接放行到错误页面,不再重试 if (to.matched.length > 0) { @@ -168,7 +174,7 @@ async function handleRouteGuard( return } - // 3. 处理动态路由注册 + // 4. 处理动态路由注册 if (!routeRegistry?.isRegistered() && userStore.isLogin) { // 防止并发请求(快速连续导航场景) if (routeInitInProgress) { @@ -180,12 +186,12 @@ async function handleRouteGuard( return } - // 4. 处理根路径重定向 + // 5. 处理根路径重定向 if (handleRootPathRedirect(to, next)) { return } - // 5. 处理已匹配的路由 + // 6. 处理已匹配的路由 if (to.matched.length > 0) { setWorktab(to) setPageTitle(to) @@ -193,12 +199,40 @@ async function handleRouteGuard( return } - // 6. 未匹配到路由,跳转到 404 + // 7. 未匹配到路由,跳转到 404 next({ name: 'Exception404' }) } +/** + * 强制改密门 + * + * 会话有效且 must_change_password=true 时,除强制改密页本身外 + * 一律重定向(含登录页与静态异常页——改完初始密码前不进后台)。 + * 后端同语义:受保护端点 409(HTTP client 全局跳转兜底)。 + * + * @returns true 表示可以继续,false 表示已处理跳转 + */ +function handleMustChangePassword( + to: RouteLocationNormalized, + next: NavigationGuardNext +): boolean { + const authStore = useAuthStore() + if (!authStore.isAuthenticated || !authStore.mustChangePassword) { + return true + } + if (to.path === RoutesAlias.ForceChangePassword) { + next() + return false + } + next({ path: RoutesAlias.ForceChangePassword, replace: true }) + return false +} + /** * 处理登录状态 + * + * 会话事实源为 auth store 的 token(localStorage 持久化,刷新恢复); + * 模板 user store 的 isLogin 由登录/登出同步维护(此处对遗留残留自愈)。 * @returns true 表示可以继续,false 表示已处理跳转 */ function handleLoginStatus( @@ -206,13 +240,26 @@ function handleLoginStatus( userStore: ReturnType, next: NavigationGuardNext ): boolean { - // 已登录或访问登录页或静态路由,直接放行 - if (userStore.isLogin || to.path === RoutesAlias.Login || isStaticRoute(to.path)) { + const authStore = useAuthStore() + + // 已认证:放行(自愈模板侧登录标记——如 Task 2 桩时代的残留状态) + if (authStore.isAuthenticated) { + if (!userStore.isLogin) { + userStore.setLoginStatus(true) + } + return true + } + + // 未认证:登录页/静态路由放行,但强制改密页需要会话——回登录页 + if (to.path === RoutesAlias.Login || isStaticRoute(to.path)) { + if (to.path === RoutesAlias.ForceChangePassword) { + next({ name: 'Login', replace: true }) + return false + } return true } // 未登录且访问需要权限的页面,跳转到登录页并携带 redirect 参数 - userStore.logOut() next({ name: 'Login', query: { redirect: to.fullPath } @@ -266,8 +313,8 @@ async function handleDynamicRoutes( loadingService.showLoading() try { - // 1. 获取用户信息 - await fetchUserInfo() + // 1. 派生用户信息(auth store 登录快照 → 模板 UserInfo) + deriveUserInfo() // 2. 获取菜单数据 const menuList = await menuProcessor.getMenuList() @@ -365,12 +412,15 @@ async function handleDynamicRoutes( } /** - * 获取用户信息 + * 派生用户信息 + * + * 后端无用户信息端点——登录响应(role/username)已随 auth store 持久化, + * 此处映射为模板消费形态(roles 供菜单角色过滤,见 MenuProcessor)。 */ -async function fetchUserInfo(): Promise { +function deriveUserInfo(): void { + const authStore = useAuthStore() const userStore = useUserStore() - const data = await fetchGetUserInfo() - userStore.setUserInfo(data) + userStore.setUserInfo(authStore.userInfo) // 检查并清理工作台标签页(如果是不同用户登录) userStore.checkAndClearWorktabs() } diff --git a/admin/src/store/modules/auth.ts b/admin/src/store/modules/auth.ts new file mode 100644 index 0000000..9b2563c --- /dev/null +++ b/admin/src/store/modules/auth.ts @@ -0,0 +1,188 @@ +/** + * 认证状态管理(Phase 3 Task 3) + * + * 会话事实源:token / role / tenantSlug / username / mustChangePassword。 + * 持久化 localStorage(键 all-kefu-auth,all-kefu 前缀与模板 sys-v* 键区隔), + * 刷新页面恢复会话;路由守卫与 HTTP client(401/409 全局处理)消费。 + * + * 模板侧同步:登录成功后把 token / 登录标记 / 用户信息(roles 供菜单过滤) + * 同步进模板的 user store(isLogin/accessToken/info 是模板路由初始化与 + * 顶栏展示的既有依赖);登出委托 userStore.logOut() 完成模板全套清理 + * (动态路由注销、工作台标签、跳登录页)。 + * + * 与 @/api/client 的模块环(client → 本模块 → @/api/auth → client): + * 双方均只在函数体内访问对方导出(ESM 活绑定),模块求值期无交叉读取。 + */ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { fetchChangePassword, fetchLogin, fetchLogout } from '@/api/auth' +import { useUserStore } from '@/store/modules/user' +import { RoutesAlias } from '@/router/routesAlias' + +/** 平台角色(与后端 token 快照一致) */ +export type AdminRole = 'super' | 'tenant' + +/** 登录参数(POST /api/platform/auth/login) */ +export interface LoginParams { + username: string + password: string +} + +/** 改密参数(POST /api/platform/auth/change-password) */ +export interface ChangePasswordParams { + old_password: string + new_password: string +} + +/** 登录响应(后端 auth_routes.login) */ +export interface LoginResponse { + token: string + role: AdminRole + tenant_slug: string | null + must_change_password: boolean +} + +/** 持久化键(all-kefu 前缀;user store 等模板键为 sys-v* 体系) */ +const AUTH_STORAGE_KEY = 'all-kefu-auth' + +/** + * 用户名 → 稳定伪 ID(djb2 哈希) + * + * 后端不下发 user_id;模板 user store 的工作台跨用户清理 + * (checkAndClearWorktabs)按 userId 比对,用确定性哈希保持该语义。 + */ +function stableUserId(username: string): number { + let hash = 5381 + for (let i = 0; i < username.length; i++) { + hash = ((hash << 5) + hash + username.charCodeAt(i)) | 0 + } + return Math.abs(hash) || 1 +} + +export const useAuthStore = defineStore( + 'authStore', + () => { + // ---- 状态 ------------------------------------------------------------------ + const token = ref('') + const role = ref('') + const tenantSlug = ref(null) + const username = ref('') + const mustChangePassword = ref(false) + + // ---- 计算属性 ---------------------------------------------------------------- + const isAuthenticated = computed(() => token.value !== '') + + /** 模板 Api.Auth.UserInfo 映射(守卫初始化动态路由/菜单角色过滤用) */ + const userInfo = computed(() => ({ + buttons: [], + roles: role.value ? [role.value] : [], + userId: stableUserId(username.value), + userName: username.value, + email: '' + })) + + // ---- 内部工具 ---------------------------------------------------------------- + /** 登录成功后的状态落地(不调 API) */ + function applyLogin(loginUsername: string, res: LoginResponse): void { + token.value = res.token + role.value = res.role + tenantSlug.value = res.tenant_slug + mustChangePassword.value = res.must_change_password + username.value = loginUsername + syncUserStore() + } + + /** 同步模板 user store(模板路由初始化/顶栏依赖其状态) */ + function syncUserStore(): void { + const userStore = useUserStore() + userStore.setToken(token.value, '') + userStore.setLoginStatus(true) + userStore.setUserInfo(userInfo.value) + userStore.checkAndClearWorktabs() + } + + /** 清空本 store 全部会话状态 */ + function clearSession(): void { + token.value = '' + role.value = '' + tenantSlug.value = null + username.value = '' + mustChangePassword.value = false + } + + /** + * 页面跳转(动态 import 断开与 router 模块的静态环; + * 已在目标页则不重复跳——防 401/409 拦截死循环) + */ + async function goTo(path: string): Promise { + const { router } = await import('@/router') + if (router.currentRoute.value.path === path) return + await router.replace(path) + } + + // ---- Actions ----------------------------------------------------------------- + /** 登录:调 API 成功后落地会话状态(失败向上抛,由调用方/全局拦截提示) */ + async function login(params: LoginParams): Promise { + const res = await fetchLogin(params) + applyLogin(params.username, res) + return res + } + + /** + * 登出:调后端撤销 token(best effort,失败也照常本地清理), + * 清空会话后委托模板 userStore.logOut()(动态路由注销 + 跳登录页) + */ + async function logout(): Promise { + try { + await fetchLogout() + } catch { + // token 已失效/网络不可达:本地清理照常进行 + } + clearSession() + useUserStore().logOut() + } + + /** + * 改密:成功后清 must_change 标志(后端已就地清当前 token 的标志, + * 会话保持,改完无需重登) + */ + async function changePassword(params: ChangePasswordParams): Promise { + await fetchChangePassword(params) + mustChangePassword.value = false + } + + /** 401 全局处理:清会话 + 模板侧全套清理(userStore.logOut 含跳登录) */ + function handleSessionExpired(): void { + clearSession() + useUserStore().logOut() + } + + /** 409 强制改密全局处理:置标志 + 跳改密页(已在改密页不重复跳) */ + async function handleMustChangePassword(): Promise { + mustChangePassword.value = true + await goTo(RoutesAlias.ForceChangePassword) + } + + return { + token, + role, + tenantSlug, + username, + mustChangePassword, + isAuthenticated, + userInfo, + login, + logout, + changePassword, + clearSession, + handleSessionExpired, + handleMustChangePassword + } + }, + { + persist: { + key: AUTH_STORAGE_KEY, + storage: localStorage + } + } +) diff --git a/admin/src/store/modules/user.ts b/admin/src/store/modules/user.ts index 08f7684..6ae2fff 100644 --- a/admin/src/store/modules/user.ts +++ b/admin/src/store/modules/user.ts @@ -41,6 +41,7 @@ import { AppRouteRecord } from '@/types/router' import { setPageTitle } from '@/utils/router' import { resetRouterState } from '@/router/guards/beforeEach' import { useMenuStore } from './menu' +import { RoutesAlias } from '@/router/routesAlias' import { StorageConfig } from '@/utils/storage/storage-config' /** @@ -167,8 +168,9 @@ export const useUserStore = defineStore( // 重置路由状态 resetRouterState(500) // 跳转到登录页,携带当前路由作为 redirect 参数 + // (已在登录页则不带——否则登录成功后会跳回登录页自身) const currentRoute = router.currentRoute.value - const redirect = currentRoute.path !== '/login' ? currentRoute.fullPath : undefined + const redirect = currentRoute.path !== RoutesAlias.Login ? currentRoute.fullPath : undefined router.push({ name: 'Login', query: redirect ? { redirect } : undefined diff --git a/admin/src/types/api/api.d.ts b/admin/src/types/api/api.d.ts index ad09c30..45e3a40 100644 --- a/admin/src/types/api/api.d.ts +++ b/admin/src/types/api/api.d.ts @@ -24,8 +24,7 @@ * ## 使用方式 * * ```typescript - * const params: Api.Auth.LoginParams = { userName: 'admin', password: '123456' } - * const response: Api.Auth.UserInfo = await fetchUserInfo() + * const info: Api.Auth.UserInfo = { buttons: [], roles: ['super'], userId: 1, userName: 'admin', email: '' } * ``` * * @module types/api/api @@ -60,21 +59,13 @@ declare namespace Api { type EnableStatus = '1' | '2' } - /** 认证类型 */ + /** 认证类型(登录/改密参数与响应见 src/store/modules/auth.ts,与后端字段一一对应) */ namespace Auth { - /** 登录参数 */ - interface LoginParams { - userName: string - password: string - } - - /** 登录响应 */ - interface LoginResponse { - token: string - refreshToken: string - } - - /** 用户信息 */ + /** + * 用户信息(模板消费形态:路由初始化/菜单角色过滤/顶栏展示)。 + * 后端不下发用户信息端点——由 auth store 从登录快照派生 + * (roles=[role]、userName=登录名、userId=用户名稳定哈希)。 + */ interface UserInfo { buttons: string[] roles: string[] diff --git a/admin/src/types/import/components.d.ts b/admin/src/types/import/components.d.ts index a1af2ef..447520a 100644 --- a/admin/src/types/import/components.d.ts +++ b/admin/src/types/import/components.d.ts @@ -85,8 +85,6 @@ declare module 'vue' { ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] ElPopover: typeof import('element-plus/es')['ElPopover'] - ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] - ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] diff --git a/admin/src/views/auth/force-change-password/index.vue b/admin/src/views/auth/force-change-password/index.vue index aa9910d..c6b590c 100644 --- a/admin/src/views/auth/force-change-password/index.vue +++ b/admin/src/views/auth/force-change-password/index.vue @@ -1,4 +1,4 @@ - + + + + @@ -28,25 +37,31 @@ 提交修改 -

密码修改成功后方可进入后台(对接后端 API 在后续任务实现)

+

密码修改成功后方可进入后台;修改后当前会话保持有效,无需重新登录

diff --git a/admin/src/views/auth/login/index.vue b/admin/src/views/auth/login/index.vue index f0eaa62..77f8bce 100644 --- a/admin/src/views/auth/login/index.vue +++ b/admin/src/views/auth/login/index.vue @@ -78,15 +78,6 @@ {{ $t('login.btnText') }} - - -
- 角色(开发调试) - - 超管 - 租户管理员 - -
@@ -96,9 +87,9 @@ diff --git a/tests/test_auth.py b/tests/test_auth.py index de97d0d..6699a58 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -126,10 +126,13 @@ def test_change_password_flow(app_client): def test_must_change_password_409_semantics(app_client): - """引导密码强制语义: must_change=True 时受保护端点(logout)409; - change-password 本身豁免(200);改完 409 消除。""" + """引导密码强制语义: must_change=True 时受保护端点(logout)409 + (detail 为固定文案——前端 client.ts 的 MUST_CHANGE_DETAIL 依赖此 + 契约,改动文案即红);change-password 本身豁免(200);改完 409 消除。""" token = _login(app_client).json()["token"] - assert app_client.post(LOGOUT, headers=_auth(token)).status_code == 409 + r = app_client.post(LOGOUT, headers=_auth(token)) + assert r.status_code == 409 + assert r.json()["detail"] == "必须先修改初始密码" r = app_client.post( CHANGE_PW, headers=_auth(token), json={"old_password": "admin123", "new_password": "n3w-pass-3"}) From ce668eaf2f2b77e48d224a32c09500ea6b9fc2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 17:31:12 +0800 Subject: [PATCH 06/15] =?UTF-8?q?feat:=20=E4=BC=9A=E8=AF=9D=E4=B8=8E?= =?UTF-8?q?=E7=BA=BF=E7=B4=A2=EF=BC=88=E5=88=97=E8=A1=A8/=E7=AD=9B?= =?UTF-8?q?=E9=80=89/=E7=A7=9F=E6=88=B7=E5=88=87=E6=8D=A2/=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E6=B6=88=E6=81=AF=E6=B5=81/=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E6=97=A5=E5=BF=97/=E5=88=A0=E9=99=A4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 会话列表页签:分页表格 + 筛选(意向三值/站点精确/日期区间 daterange, 筛选变化重置第一页)+ 行操作(详情抽屉/删除确认框,级联消息画像评估 推送日志,末页删空自动回退一页) - 租户切换器:超管专用(fetchTenantList 下拉,默认第一个,切换联动双页签 并关抽屉);租户管理员固定登录态 tenant_slug,无切换器只有标签 - 详情抽屉:元数据(状态/来源/站点/IP 归国/session/设备)+ 画像 chips (ElTag 键值)+ 评估块(意向/理由/联系方式/更正次数)+ 消息流气泡 (user 右 assistant 左,时间直显库内 naive UTC 不换算,与筛选同口径) - 推送日志页签(列表平行):状态筛选(pending/success/failed/skipped) + 分页表格(类型/状态/尝试/outcome/HTTP/payload 大小/会话/错误)—— 列表接口不回大 payload,仅服务端 payload_size - api/conversations.ts 实装调用函数并按 data_routes.py 实际响应修正类型 (IntentLevel 三值/messages.time/详情扁平 evaluation+profile 块/ PushLogItem 全字段),新增枚举中文映射(intentLabel 等,未知值回退原样) - vitest +9(conversations API 契约:URL/参数/白名单/映射),总 45; 后端不动(pytest 577 基线复核) Co-Authored-By: Claude Code --- admin/src/api/__tests__/conversations.test.ts | 162 ++++ admin/src/api/conversations.ts | 179 +++- admin/src/types/import/components.d.ts | 8 + admin/src/views/conversations/list/index.vue | 869 +++++++++++++++++- 4 files changed, 1189 insertions(+), 29 deletions(-) create mode 100644 admin/src/api/__tests__/conversations.test.ts diff --git a/admin/src/api/__tests__/conversations.test.ts b/admin/src/api/__tests__/conversations.test.ts new file mode 100644 index 0000000..d96eb9d --- /dev/null +++ b/admin/src/api/__tests__/conversations.test.ts @@ -0,0 +1,162 @@ +/** + * 会话与线索 API 单元测试(Phase 3 Task 5) + * + * 钉住端点 URL 与查询参数组装(事实源 app/platform/routes/data_routes.py: + * intent 白名单 interested/not_interested/pending、push status 白名单 + * pending/success/failed/skipped、from/to 闭区间、分页 page/page_size ≤100); + * HTTP 层行为已在 client.test.ts 覆盖,此处 mock http 只断言 API 层契约。 + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/api/client', () => ({ + http: { get: vi.fn(), del: vi.fn() } +})) + +import { http } from '@/api/client' +import { + CONVERSATION_ENDPOINTS, + INTENT_OPTIONS, + PUSH_STATUS_OPTIONS, + conversationSourceLabel, + conversationStatusLabel, + deleteConversation, + fetchConversationDetail, + fetchConversations, + fetchPushLogs, + intentLabel, + pushStatusLabel, + pushTypeLabel +} from '@/api/conversations' + +describe('会话与线索 API(src/api/conversations.ts)', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.mocked(http.get).mockResolvedValue({ total: 0, page: 1, page_size: 20, items: [] }) + vi.mocked(http.del).mockResolvedValue({ ok: true }) + }) + + it('fetchConversations:GET /api/platform/tenants/{slug}/conversations 带筛选+分页参数', async () => { + await fetchConversations('jiancai', { + intent: 'interested', + site: 'es', + from: '2026-10-01', + to: '2026-10-13', + page: 2, + page_size: 50 + }) + + expect(http.get).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/conversations', + { + params: { + intent: 'interested', + site: 'es', + from: '2026-10-01', + to: '2026-10-13', + page: 2, + page_size: 50 + } + } + ) + }) + + it('fetchConversations 无参数:params 为 undefined(后端缺省 page=1/page_size=20)', async () => { + await fetchConversations('jiancai') + + expect(http.get).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/conversations', + { params: undefined } + ) + }) + + it('fetchConversationDetail:GET .../conversations/{convId}', async () => { + const detail = { + id: 'c1', + session_id: 'sess-c1', + status: 'completed', + source: 'widget', + client_info: { ip: '8.8.8.8', site: 'es' }, + site: 'es', + ip: '8.8.8.8', + device: '', + country: '美国', + created_at: '2026-10-01T12:00:00', + updated_at: '2026-10-01T12:00:00', + evaluation: null, + profile: { chips: { 姓名: 'John' }, summary: null }, + messages: [{ role: 'user', content: '您好', time: '2026-10-01T12:00:00' }] + } + vi.mocked(http.get).mockResolvedValue(detail) + + const res = await fetchConversationDetail('jiancai', 'c1') + + expect(res).toEqual(detail) + expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai/conversations/c1') + }) + + it('deleteConversation:DELETE .../conversations/{convId},响应 {ok}', async () => { + const res = await deleteConversation('jiancai', 'c1') + + expect(res).toEqual({ ok: true }) + expect(http.del).toHaveBeenCalledWith('/api/platform/tenants/jiancai/conversations/c1') + }) + + it('fetchPushLogs:GET .../push-logs 带 status/conversation_id/分页参数', async () => { + await fetchPushLogs('jiancai', { + status: 'failed', + conversation_id: 'c1', + page: 2, + page_size: 10 + }) + + expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai/push-logs', { + params: { status: 'failed', conversation_id: 'c1', page: 2, page_size: 10 } + }) + }) + + it('fetchPushLogs 无参数:params 为 undefined', async () => { + await fetchPushLogs('jiancai') + + expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai/push-logs', { + params: undefined + }) + }) + + it('CONVERSATION_ENDPOINTS:端点形态与 data_routes.py 路由一致', () => { + expect(CONVERSATION_ENDPOINTS.list('a')).toBe('/api/platform/tenants/a/conversations') + expect(CONVERSATION_ENDPOINTS.detail('a', 'c1')).toBe( + '/api/platform/tenants/a/conversations/c1' + ) + expect(CONVERSATION_ENDPOINTS.remove('a', 'c1')).toBe( + '/api/platform/tenants/a/conversations/c1' + ) + expect(CONVERSATION_ENDPOINTS.pushLogs('a')).toBe('/api/platform/tenants/a/push-logs') + }) + + it('筛选白名单选项:intent 三值 / push status 四值(与后端 INTENTS/PUSH_STATUSES 对齐)', () => { + expect(INTENT_OPTIONS.map((item) => item.value)).toEqual([ + 'interested', + 'not_interested', + 'pending' + ]) + expect(PUSH_STATUS_OPTIONS.map((item) => item.value)).toEqual([ + 'pending', + 'success', + 'failed', + 'skipped' + ]) + }) + + it('展示映射:未知枚举值回退原样,空值有占底文案', () => { + expect(intentLabel('interested')).toBe('有意向') + expect(intentLabel(null)).toBe('未评估') + expect(intentLabel('bogus')).toBe('bogus') + expect(pushStatusLabel('skipped')).toBe('跳过') + expect(pushStatusLabel('bogus')).toBe('bogus') + expect(pushTypeLabel('correction')).toBe('更正') + expect(pushTypeLabel('unknown')).toBe('unknown') + expect(conversationStatusLabel('completed')).toBe('已完成') + expect(conversationSourceLabel('widget')).toBe('挂件') + expect(conversationSourceLabel('weird')).toBe('weird') + }) +}) diff --git a/admin/src/api/conversations.ts b/admin/src/api/conversations.ts index 5c96762..0e386ec 100644 --- a/admin/src/api/conversations.ts +++ b/admin/src/api/conversations.ts @@ -1,15 +1,26 @@ /** - * 会话与线索 API 骨架(Phase 3 Task 3) + * 会话与线索 API(Phase 3 Task 3 骨架;Task 5 实装调用函数并按实际响应修正类型) * - * 端点事实源:app/platform/routes/data_routes.py - * (会话列表/详情/删除、推送日志)。本文件只建类型与端点常量—— - * 调用函数在 Task 5(会话与线索)实装。 + * 端点与响应事实源:app/platform/routes/data_routes.py + * - 列表:{total, page, page_size, items},行含 preview/intent_level/contact_info/ + * email_sent/message_count/country/site 等;筛选 intent 白名单(interested/ + * not_interested/pending,非法 400)/site 精确/client_info["site"]/from-to 闭区间 + * (日期-only 的 to 含当日全天) + * - 详情:扁平结构(元数据 + evaluation/profile/messages 三块),消息行字段为 + * time(非 created_at);profile.chips 为 raw_info 去 _ 前缀键 + * - 删除:{ok: true};不存在 404(级联消息/画像/评估/推送日志) + * - push-logs:列表不回大 payload(load_only 排除 request_payload),带 + * payload_size 服务端长度标记;status 白名单 pending/success/failed/skipped */ +import { http } from './client' -/** 意向层级(Evaluation.intent_level) */ -export type IntentLevel = 'high' | 'medium' | 'low' | 'none' | null +/** 意向层级(Evaluation.intent_level 枚举;列表筛选白名单同值) */ +export type IntentLevel = 'interested' | 'not_interested' | 'pending' -/** 联系方式(评估产出) */ +/** 推送状态(LeadPushLog.status;筛选白名单同值) */ +export type PushLogStatus = 'pending' | 'success' | 'failed' | 'skipped' + +/** 联系方式(评估产出,留资三选一) */ export interface ContactInfo { email?: string | null whatsapp?: string | null @@ -26,7 +37,7 @@ export interface ConversationListItem { ip: string device: string country: string | null - intent_level: IntentLevel + intent_level: IntentLevel | null reasoning: string | null contact_info: ContactInfo | null email_sent: boolean @@ -35,9 +46,9 @@ export interface ConversationListItem { created_at: string | null } -/** 会话列表查询参数(intent 白名单校验在后端;from/to 为闭区间日期) */ +/** 会话列表查询参数(intent 白名单校验在后端;from/to 为闭区间日期 YYYY-MM-DD) */ export interface ConversationListParams { - intent?: string + intent?: IntentLevel site?: string from?: string to?: string @@ -53,30 +64,66 @@ export interface ConversationListResponse { items: ConversationListItem[] } -/** 会话详情消息行(时间升序) */ +/** 会话详情消息行(时间升序;字段为 time) */ export interface ConversationMessage { role: 'user' | 'assistant' | 'system' | string content: string - created_at: string | null + time: string | null } -/** 会话详情响应(GET .../conversations/{convId}) */ -export interface ConversationDetailResponse extends ConversationListItem { +/** 评估块(详情内嵌,未评估为 null) */ +export interface ConversationEvaluation { + intent_level: IntentLevel | null + reasoning: string | null + contact_info: ContactInfo | null + email_sent: boolean + correction_count: number | null + evaluated_at: string | null +} + +/** 会话详情响应(GET .../conversations/{convId}——扁平,与列表行不同构) */ +export interface ConversationDetail { + id: string + session_id: string + status: string + source: string client_info: Record - messages: ConversationMessage[] + site: string | null + ip: string + device: string + country: string | null + created_at: string | null + updated_at: string | null + evaluation: ConversationEvaluation | null profile: { chips: Record summary: string | null } | null + messages: ConversationMessage[] } -/** 推送日志行(GET .../push-logs) */ +/** 推送日志行(GET .../push-logs——无 request_payload,仅 payload_size 长度标记) */ export interface PushLogItem { - id: number + id: string + evaluation_id: string conversation_id: string - status: string - error: string | null + push_type: string + status: PushLogStatus | string + attempts: number + outcome: string | null + response_status: number | null + error_message: string | null + payload_size: number | null created_at: string | null + updated_at: string | null +} + +/** 推送日志查询参数(status 白名单校验在后端) */ +export interface PushLogParams { + status?: PushLogStatus + conversation_id?: string + page?: number + page_size?: number } /** 推送日志响应 */ @@ -96,3 +143,97 @@ export const CONVERSATION_ENDPOINTS = { `/api/platform/tenants/${slug}/conversations/${convId}`, pushLogs: (slug: string) => `/api/platform/tenants/${slug}/push-logs` } as const + +// ---- 展示映射(枚举值事实源 data_routes.py INTENTS/PUSH_STATUSES 与 models.py) ---- + +/** 意向筛选/展示选项 */ +export const INTENT_OPTIONS: Array<{ value: IntentLevel; label: string }> = [ + { value: 'interested', label: '有意向' }, + { value: 'not_interested', label: '无意向' }, + { value: 'pending', label: '待定' } +] + +/** 意向展示文案(未知值回退原样) */ +export function intentLabel(level: string | null | undefined): string { + if (!level) return '未评估' + return INTENT_OPTIONS.find((item) => item.value === level)?.label ?? level +} + +/** 推送状态筛选/展示选项 */ +export const PUSH_STATUS_OPTIONS: Array<{ value: PushLogStatus; label: string }> = [ + { value: 'pending', label: '待推送' }, + { value: 'success', label: '成功' }, + { value: 'failed', label: '失败' }, + { value: 'skipped', label: '跳过' } +] + +/** 推送状态展示文案 */ +export function pushStatusLabel(status: string | null | undefined): string { + if (!status) return '—' + return PUSH_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? status +} + +/** 推送类型展示文案(LeadPushLog.push_type:lead/returning/correction) */ +export function pushTypeLabel(type: string | null | undefined): string { + const labels: Record = { + lead: '新线索', + returning: '回访', + correction: '更正' + } + if (!type) return '—' + return labels[type] ?? type +} + +/** 会话状态展示文案(ConversationStatus) */ +export function conversationStatusLabel(status: string | null | undefined): string { + const labels: Record = { + active: '进行中', + completed: '已完成', + abandoned: '已放弃' + } + if (!status) return '—' + return labels[status] ?? status +} + +/** 会话来源展示文案(ConversationSource) */ +export function conversationSourceLabel(source: string | null | undefined): string { + const labels: Record = { + standalone: '独立站', + embedded: '内嵌页', + widget: '挂件', + app: '应用' + } + if (!source) return '—' + return labels[source] ?? source +} + +// ---- 调用函数 ------------------------------------------------------------------- + +/** 会话列表(分页 + intent/site/日期区间筛选) */ +export function fetchConversations( + slug: string, + params?: ConversationListParams +): Promise { + return http.get(CONVERSATION_ENDPOINTS.list(slug), { params }) +} + +/** 会话详情(元数据 + 画像 chips + 评估 + 消息全量) */ +export function fetchConversationDetail( + slug: string, + convId: string +): Promise { + return http.get(CONVERSATION_ENDPOINTS.detail(slug, convId)) +} + +/** 删除会话(级联消息/画像/评估/推送日志;不存在 404) */ +export function deleteConversation( + slug: string, + convId: string +): Promise<{ ok: boolean }> { + return http.del<{ ok: boolean }>(CONVERSATION_ENDPOINTS.remove(slug, convId)) +} + +/** 推送日志列表(status/conversation_id 筛选 + 分页;不回大 payload) */ +export function fetchPushLogs(slug: string, params?: PushLogParams): Promise { + return http.get(CONVERSATION_ENDPOINTS.pushLogs(slug), { params }) +} diff --git a/admin/src/types/import/components.d.ts b/admin/src/types/import/components.d.ts index 400a080..2ef5b87 100644 --- a/admin/src/types/import/components.d.ts +++ b/admin/src/types/import/components.d.ts @@ -71,6 +71,9 @@ declare module 'vue' { ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] + ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] + ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] @@ -85,6 +88,7 @@ declare module 'vue' { ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] ElPopover: typeof import('element-plus/es')['ElPopover'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] @@ -93,6 +97,10 @@ declare module 'vue' { ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElWatermark: typeof import('element-plus/es')['ElWatermark'] diff --git a/admin/src/views/conversations/list/index.vue b/admin/src/views/conversations/list/index.vue index 6795d15..fcbeec4 100644 --- a/admin/src/views/conversations/list/index.vue +++ b/admin/src/views/conversations/list/index.vue @@ -1,25 +1,874 @@ - + From 6f22e519081590a045710a1407601a7426c64b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 18:27:56 +0800 Subject: [PATCH 07/15] =?UTF-8?q?feat:=20=E7=A7=9F=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E4=B8=8E=E7=AB=99=E7=82=B9=E7=AE=A1=E7=90=86=EF=BC=88?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E5=90=91=E5=AF=BC/=E5=81=9C=E7=94=A8/?= =?UTF-8?q?=E5=9F=9F=E5=90=8D/=E9=97=A8=E9=9D=A2=E8=A6=86=E7=9B=96?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 层实装(api/tenants.ts):创建/详情/PATCH 租户、建站/改站、 域名绑定解绑——400 撞车 detail(slug/域名归属/prefix 引导)经 showErrorMessage:false 关全局 toast、由向导与绑域名弹窗内联常驻展示 - 租户列表:slug/名称/状态/域名数/版本/管理员概要 + 状态筛选 + 行操作(详情/停用复启用/改名);停用确认框写明 503 服务暂停语义 - 创建向导(ElSteps 四步):名称+slug(^[a-z0-9-]+$ 与撞车内联拦截) → 域名多填 → 出厂档案(auto/jiancai 提示 prefix 全局唯一撞车引导 改 blank;blank 提示墙与提示词取建材出厂档案)→ 管理员账号; 201 后列表刷新并提示首登需改密 - 站点管理(详情抽屉):站点表/新建站(门面覆盖层表单——brand 组 子集字段留空=继承租户默认)/停用复启用(404 语义提示)/域名绑定 (撞车归属内联)与解绑(标签 × 确认) - vitest +9(API 层 URL/方法/请求体契约);页面仅超管(菜单 roles 已挡) Co-Authored-By: Claude Code --- admin/src/api/__tests__/tenants.test.ts | 218 +++++ admin/src/api/tenants.ts | 126 ++- admin/src/types/import/components.d.ts | 3 + admin/src/views/tenants/list/index.vue | 1130 ++++++++++++++++++++++- 4 files changed, 1464 insertions(+), 13 deletions(-) create mode 100644 admin/src/api/__tests__/tenants.test.ts diff --git a/admin/src/api/__tests__/tenants.test.ts b/admin/src/api/__tests__/tenants.test.ts new file mode 100644 index 0000000..3928084 --- /dev/null +++ b/admin/src/api/__tests__/tenants.test.ts @@ -0,0 +1,218 @@ +/** + * 租户/站点/域名 API 单元测试(Phase 3 Task 6) + * + * 钉住 API 层契约:端点 URL / 方法 / 请求体形状(事实源 + * app/platform/routes/tenant_routes.py);HTTP 层行为已在 client.test.ts + * 覆盖,此处 mock http 只断言调用形态: + * - 列表/详情:GET /api/platform/tenants[/{slug}] + * - 创建向导:POST 全量六字段(domains 进 main 站点) + * - PATCH 租户:只发 name 或 status(slug 键绝不出现在 body——后端 400) + * - 站点:POST sites(brand_overlay 只含显式覆盖字段)/ PATCH sites/{site} + * - 域名:POST domains {domain} / DELETE domains/{domain}(路径段直拼) + * - RequestOptions 透传(向导/绑域名内联错误关全局 toast) + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/api/client', () => ({ + http: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), del: vi.fn() } +})) + +import { http } from '@/api/client' +import { + addSiteDomain, + createSite, + createTenant, + deleteSiteDomain, + fetchTenantDetail, + fetchTenantList, + patchSite, + patchTenant +} from '@/api/tenants' + +describe('租户与站点管理 API(src/api/tenants.ts)', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('fetchTenantList:GET /api/platform/tenants', async () => { + const body = { + items: [ + { + slug: 'jiancai', + name: '建材租户', + status: 'active', + domain_count: 1, + current_version: 1, + admins: [{ username: 'jc-admin', status: 'active' }] + } + ] + } + vi.mocked(http.get).mockResolvedValue(body) + + const res = await fetchTenantList() + + expect(res).toEqual(body) + expect(http.get).toHaveBeenCalledWith('/api/platform/tenants') + }) + + it('fetchTenantDetail:GET /api/platform/tenants/{slug}', async () => { + const body = { + slug: 'jiancai', + name: '建材租户', + status: 'active', + current_version: 1, + sites: [ + { + slug: 'main', + name: 'main', + status: 'active', + domains: ['chat2.agent.hnxinyouhe.com'], + brand_overlay: {} + } + ], + admins: [{ username: 'jc-admin', status: 'active' }] + } + vi.mocked(http.get).mockResolvedValue(body) + + const res = await fetchTenantDetail('jiancai') + + expect(res).toEqual(body) + expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai', undefined) + }) + + it('createTenant:POST 全量六字段(向导提交形态)', async () => { + const body = { + name: '新租户', + slug: 'new-tenant', + profile: 'blank', + admin_username: 'nt-admin', + admin_password: 'initial-pass', + domains: ['a.example.com', 'b.example.com'] + } + const created = { + slug: 'new-tenant', + name: '新租户', + status: 'active', + profile: 'blank', + version: 1, + site: { slug: 'main', domains: ['a.example.com', 'b.example.com'] }, + admin_username: 'nt-admin' + } + vi.mocked(http.post).mockResolvedValue(created) + + const res = await createTenant(body, { showErrorMessage: false }) + + expect(res).toEqual(created) + expect(http.post).toHaveBeenCalledWith('/api/platform/tenants', body, { + showErrorMessage: false + }) + }) + + it('patchTenant 改名:PATCH {slug} 只发 name(slug 键不可出现)', async () => { + vi.mocked(http.patch).mockResolvedValue({ + slug: 'jiancai', + name: '建材租户改', + status: 'active' + }) + + await patchTenant('jiancai', { name: '建材租户改' }) + + expect(http.patch).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai', + { name: '建材租户改' }, + undefined + ) + }) + + it('patchTenant 停用:PATCH {slug} 只发 status=disabled', async () => { + vi.mocked(http.patch).mockResolvedValue({ + slug: 'jiancai', + name: '建材租户', + status: 'disabled' + }) + + await patchTenant('jiancai', { status: 'disabled' }) + + expect(http.patch).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai', + { status: 'disabled' }, + undefined + ) + }) + + it('createSite:POST sites 带 brand_overlay 覆盖子集(留空字段不进 overlay)', async () => { + const body = { + slug: 'shop1', + name: '西班牙站', + brand_overlay: { title: 'HESUPIPE ES', color: '#ff6600' } + } + const created = { + slug: 'shop1', + name: '西班牙站', + status: 'active', + domains: [], + brand_overlay: body.brand_overlay + } + vi.mocked(http.post).mockResolvedValue(created) + + const res = await createSite('jiancai', body) + + expect(res).toEqual(created) + expect(http.post).toHaveBeenCalledWith('/api/platform/tenants/jiancai/sites', body, undefined) + }) + + it('patchSite 停用:PATCH sites/{siteSlug} 只发 status', async () => { + vi.mocked(http.patch).mockResolvedValue({ + slug: 'shop1', + name: '西班牙站', + status: 'disabled', + domains: [], + brand_overlay: {} + }) + + await patchSite('jiancai', 'shop1', { status: 'disabled' }) + + expect(http.patch).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/sites/shop1', + { status: 'disabled' }, + undefined + ) + }) + + it('addSiteDomain:POST sites/{siteSlug}/domains {domain}', async () => { + vi.mocked(http.post).mockResolvedValue({ + domain: 'es.hesupipe.com', + site: 'shop1' + }) + + const res = await addSiteDomain( + 'jiancai', + 'shop1', + { domain: 'es.hesupipe.com' }, + { showErrorMessage: false } + ) + + expect(res).toEqual({ domain: 'es.hesupipe.com', site: 'shop1' }) + expect(http.post).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/sites/shop1/domains', + { domain: 'es.hesupipe.com' }, + { showErrorMessage: false } + ) + }) + + it('deleteSiteDomain:DELETE sites/{siteSlug}/domains/{domain}(路径直拼)', async () => { + vi.mocked(http.del).mockResolvedValue({ + ok: true, + domain: 'es.hesupipe.com', + site: 'shop1' + }) + + const res = await deleteSiteDomain('jiancai', 'shop1', 'es.hesupipe.com') + + expect(res).toEqual({ ok: true, domain: 'es.hesupipe.com', site: 'shop1' }) + expect(http.del).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/sites/shop1/domains/es.hesupipe.com', + undefined + ) + }) +}) diff --git a/admin/src/api/tenants.ts b/admin/src/api/tenants.ts index f9241d6..e034dbc 100644 --- a/admin/src/api/tenants.ts +++ b/admin/src/api/tenants.ts @@ -1,11 +1,17 @@ /** - * 租户与站点管理 API(Phase 3 Task 3 骨架;Task 4 增列表函数) + * 租户与站点管理 API(Phase 3 Task 3 骨架;Task 4 列表函数;Task 6 实装全部调用函数) * * 端点事实源:app/platform/routes/tenant_routes.py * (/api/platform/tenants,require_super / require_tenant(slug) 鉴权)。 - * 其余调用函数在 Task 6(租户管理与站点管理)实装。 + * + * 错误语义(页面交互消费): + * - 400 预检撞车带中文 detail:slug 已存在 / 域名已绑定(含归属租户与站点)/ + * 出厂 push.prefix 被占(引导改 blank 或先改占用方)——向导与绑域名 + * 弹窗用 showErrorMessage:false 关全局 toast、改内联常驻展示; + * - 409 并发竞态(提交瞬间被抢先); + * - PATCH 带 slug 键一律 400(slug 不可变——界面不提供改 slug 入口)。 */ -import { http } from './client' +import { http, type RequestOptions } from './client' /** 租户状态(Tenant.status) */ export type TenantStatus = 'active' | 'disabled' @@ -75,11 +81,49 @@ export interface CreateSiteRequest { domains?: string[] } -/** 绑定域名请求(POST .../sites/{siteSlug}/domains,201;409=域名已被占用) */ +/** 创建租户响应(create_tenant:201 返回概要;管理员凭据绝不回传) */ +export interface TenantCreatedResponse { + slug: string + name: string + status: TenantStatus + profile: string + version: number + site: { slug: string; domains: string[] } + admin_username: string +} + +/** 改名/改状态响应(patch_tenant / patch_site 返回形状) */ +export interface TenantPatchResponse { + slug: string + name: string + status: TenantStatus +} + +/** 改站点请求(PATCH .../sites/{siteSlug};slug 不可变——后端 400) */ +export interface PatchSiteRequest { + name?: string + status?: TenantStatus + brand_overlay?: Record +} + +/** 绑定域名请求(POST .../sites/{siteSlug}/domains,201;400=撞车带归属) */ export interface AddDomainRequest { domain: string } +/** 绑定域名响应 */ +export interface DomainBindResponse { + domain: string + site: string +} + +/** 解绑域名响应 */ +export interface DomainUnbindResponse { + ok: boolean + domain: string + site: string +} + /** 租户管理端点(鉴权与 409 并发冲突语义见 tenant_routes.py) */ export const TENANT_ENDPOINTS = { list: '/api/platform/tenants', @@ -87,8 +131,7 @@ export const TENANT_ENDPOINTS = { detail: (slug: string) => `/api/platform/tenants/${slug}`, patch: (slug: string) => `/api/platform/tenants/${slug}`, createSite: (slug: string) => `/api/platform/tenants/${slug}/sites`, - patchSite: (slug: string, siteSlug: string) => - `/api/platform/tenants/${slug}/sites/${siteSlug}`, + patchSite: (slug: string, siteSlug: string) => `/api/platform/tenants/${slug}/sites/${siteSlug}`, addDomain: (slug: string, siteSlug: string) => `/api/platform/tenants/${slug}/sites/${siteSlug}/domains`, deleteDomain: (slug: string, siteSlug: string, domain: string) => @@ -102,3 +145,74 @@ export const TENANT_ENDPOINTS = { export function fetchTenantList(): Promise { return http.get(TENANT_ENDPOINTS.list) } + +/** 租户详情(含 sites/domains/brand_overlay/admins;本租户管理员亦可读) */ +export function fetchTenantDetail( + slug: string, + config?: RequestOptions +): Promise { + return http.get(TENANT_ENDPOINTS.detail(slug), config) +} + +/** + * 创建租户(创建向导提交;201)。向导调用建议传 + * `{ showErrorMessage: false }`——400 预检撞车 detail(slug 已存在 / + * 出厂 push.prefix 被占的改 blank 引导)在向导内联常驻展示。 + */ +export function createTenant( + body: CreateTenantRequest, + config?: RequestOptions +): Promise { + return http.post(TENANT_ENDPOINTS.create, body, config) +} + +/** 改名 / 停用 / 复启用(PATCH;slug 不可变,body 不含 slug 键) */ +export function patchTenant( + slug: string, + body: PatchTenantRequest, + config?: RequestOptions +): Promise { + return http.patch(TENANT_ENDPOINTS.patch(slug), body, config) +} + +/** 新建站点(brand_overlay 只放显式覆盖的字段——留空=继承租户默认) */ +export function createSite( + slug: string, + body: CreateSiteRequest, + config?: RequestOptions +): Promise { + return http.post(TENANT_ENDPOINTS.createSite(slug), body, config) +} + +/** 改站点(停用/复启用/改名/改覆盖层;整字典赋值——旧键不残留) */ +export function patchSite( + slug: string, + siteSlug: string, + body: PatchSiteRequest, + config?: RequestOptions +): Promise { + return http.patch(TENANT_ENDPOINTS.patchSite(slug, siteSlug), body, config) +} + +/** 绑定域名(全局唯一;400 detail 带现有归属"租户 x 的站点 y") */ +export function addSiteDomain( + slug: string, + siteSlug: string, + body: AddDomainRequest, + config?: RequestOptions +): Promise { + return http.post(TENANT_ENDPOINTS.addDomain(slug, siteSlug), body, config) +} + +/** 解绑域名(只解本站点的该域名;不存在 404) */ +export function deleteSiteDomain( + slug: string, + siteSlug: string, + domain: string, + config?: RequestOptions +): Promise { + return http.del( + TENANT_ENDPOINTS.deleteDomain(slug, siteSlug, domain), + config + ) +} diff --git a/admin/src/types/import/components.d.ts b/admin/src/types/import/components.d.ts index 2ef5b87..3f71ccc 100644 --- a/admin/src/types/import/components.d.ts +++ b/admin/src/types/import/components.d.ts @@ -90,11 +90,14 @@ declare module 'vue' { ElOption: typeof import('element-plus/es')['ElOption'] ElPagination: typeof import('element-plus/es')['ElPagination'] ElPopover: typeof import('element-plus/es')['ElPopover'] + ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] + ElStep: typeof import('element-plus/es')['ElStep'] + ElSteps: typeof import('element-plus/es')['ElSteps'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] diff --git a/admin/src/views/tenants/list/index.vue b/admin/src/views/tenants/list/index.vue index 91f2993..612396a 100644 --- a/admin/src/views/tenants/list/index.vue +++ b/admin/src/views/tenants/list/index.vue @@ -1,25 +1,1141 @@ - + From cac91e9e3ea519478c19a4c77a96ab568cc89e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 19:27:08 +0800 Subject: [PATCH 08/15] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AE=E4=B8=AD?= =?UTF-8?q?=E5=BF=83-=E5=93=81=E7=89=8C=EF=BC=88=E8=A1=A8=E5=8D=95/?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E9=A2=84=E8=A7=88/=E5=B5=8C=E5=85=A5?= =?UTF-8?q?=E4=BB=A3=E7=A0=81/=E5=A4=B4=E5=83=8F=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=EF=BC=89+=20=E7=AB=99=E7=82=B9=E8=A6=86=E7=9B=96=E5=B1=82?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- admin/src/api/__tests__/config.test.ts | 167 +++ admin/src/api/config.ts | 175 ++- .../business/SiteOverlayFields/index.vue | 76 ++ .../business/SiteOverlayFields/model.ts | 89 ++ admin/src/types/import/components.d.ts | 2 + admin/src/views/config/center/index.vue | 1094 ++++++++++++++++- admin/src/views/tenants/list/index.vue | 184 ++- 7 files changed, 1660 insertions(+), 127 deletions(-) create mode 100644 admin/src/api/__tests__/config.test.ts create mode 100644 admin/src/components/business/SiteOverlayFields/index.vue create mode 100644 admin/src/components/business/SiteOverlayFields/model.ts diff --git a/admin/src/api/__tests__/config.test.ts b/admin/src/api/__tests__/config.test.ts new file mode 100644 index 0000000..bea1034 --- /dev/null +++ b/admin/src/api/__tests__/config.test.ts @@ -0,0 +1,167 @@ +/** + * 配置中心 API 单元测试(Phase 3 Task 7a——品牌相关) + * + * 钉住 API 层契约(事实源 app/platform/routes/config_routes.py 与 + * upload_routes.py);HTTP 层行为已在 client.test.ts 覆盖,此处 mock http + * 只断言调用形态: + * - GET config:快照 + version(原样组——缺键防御读取靠 normalizeBrandGroup) + * - PUT config/brand:请求体是**组值本身**(整组替换语义,非 {note,value} + * 包装),备注走 comment 查询参数,无备注时不带 params + * - 头像上传:multipart file 字段(FormData) + * - normalizeBrandGroup:缺键补空串 / 类型漂移防御 / topics 只留字符串项 + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/api/client', () => ({ + http: { get: vi.fn(), post: vi.fn(), put: vi.fn() } +})) + +import { http } from '@/api/client' +import { + CONFIG_ENDPOINTS, + fetchTenantConfig, + normalizeBrandGroup, + putConfigGroup, + uploadBrandAvatar +} from '@/api/config' + +describe('配置中心 API(src/api/config.ts)', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('fetchTenantConfig:GET /api/platform/tenants/{slug}/config', async () => { + const body = { + slug: 'jiancai', + version: 3, + config: { + brand: { title: 'HESUPIPE Support', color: '#066fc0', topics: ['Get Quotation'] } + } + } + vi.mocked(http.get).mockResolvedValue(body) + + const res = await fetchTenantConfig('jiancai') + + expect(res).toEqual(body) + expect(http.get).toHaveBeenCalledWith('/api/platform/tenants/jiancai/config', undefined) + }) + + it('putConfigGroup:PUT 请求体为组值本身 + comment 查询参数(整组替换契约)', async () => { + const brand = { + title: 'HESUPIPE Support', + color: '#ff6600', + avatar: 'abc123.png', + welcome: 'Hello', + placeholder: 'Ask me', + whatsapp: '+8615937108317', + email: 'hesu@hesuu.com', + privacy_url: 'https://hesupipe.com/privacy-policy', + topics: ['Get Quotation', 'Delivery & Shipping'] + } + const response = { slug: 'jiancai', group: 'brand', version: 4, config: brand } + vi.mocked(http.put).mockResolvedValue(response) + + const res = await putConfigGroup('jiancai', 'brand', { ...brand }, '改主题色') + + expect(res).toEqual(response) + expect(http.put).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/config/brand', + brand, // 组值直传——绝不能包成 { note, value }(后端 _json_body 整组校验) + { params: { comment: '改主题色' } } + ) + }) + + it('putConfigGroup:无备注时不带 params(comment 是可选项)', async () => { + vi.mocked(http.put).mockResolvedValue({ + slug: 'jiancai', + group: 'brand', + version: 5, + config: {} + }) + + await putConfigGroup('jiancai', 'brand', { title: 'T' }) + + expect(http.put).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/config/brand', + { title: 'T' }, + { params: undefined } + ) + }) + + it('uploadBrandAvatar:POST multipart(FormData file 字段)', async () => { + const response = { slug: 'jiancai', avatar: '7f3c9d.png', version: 6 } + vi.mocked(http.post).mockResolvedValue(response) + const file = new File([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], 'logo.png', { + type: 'image/png' + }) + + const res = await uploadBrandAvatar('jiancai', file, { showErrorMessage: false }) + + expect(res).toEqual(response) + expect(http.post).toHaveBeenCalledTimes(1) + const [url, body, config] = vi.mocked(http.post).mock.calls[0] + expect(url).toBe('/api/platform/tenants/jiancai/brand/avatar') + expect(body).toBeInstanceOf(FormData) + expect((body as FormData).get('file')).toBe(file) + expect(config).toEqual({ showErrorMessage: false }) + }) + + it('CONFIG_ENDPOINTS.avatarUpload:端点路径与后端 upload_routes 一致', () => { + expect(CONFIG_ENDPOINTS.avatarUpload('jiancai')).toBe( + '/api/platform/tenants/jiancai/brand/avatar' + ) + expect(CONFIG_ENDPOINTS.putGroup('jiancai', 'brand')).toBe( + '/api/platform/tenants/jiancai/config/brand' + ) + }) +}) + +describe('normalizeBrandGroup(GET 原样组 → 表单完整值)', () => { + it('缺键补空串、topics 缺省为空数组(快照是原样组,可能缺键)', () => { + const out = normalizeBrandGroup({ title: 'HESUPIPE Support' }) + expect(out).toEqual({ + title: 'HESUPIPE Support', + color: '', + avatar: '', + welcome: '', + placeholder: '', + whatsapp: '', + email: '', + privacy_url: '', + topics: [] + }) + }) + + it('类型漂移防御:非字符串字段读作空串、topics 只留字符串项', () => { + const out = normalizeBrandGroup({ + title: 123, + color: null, + topics: ['Get Quotation', 42, null, 'Delivery'], + welcome: true + }) + expect(out.title).toBe('') + expect(out.color).toBe('') + expect(out.welcome).toBe('') + expect(out.topics).toEqual(['Get Quotation', 'Delivery']) + }) + + it('完整组原样保留(正常路径)', () => { + const full = { + title: 'HESUPIPE Support', + color: '#066fc0', + avatar: '/static/kefu-small.png', + welcome: 'What kind of piping project?', + placeholder: 'Ask about pipes...', + whatsapp: '+8615937108317', + email: 'hesu@hesuu.com', + privacy_url: 'https://hesupipe.com/privacy-policy', + topics: ['Get Quotation'] + } + expect(normalizeBrandGroup(full)).toEqual(full) + }) + + it('null / undefined 组值 → 全空表单值', () => { + expect(normalizeBrandGroup(null).title).toBe('') + expect(normalizeBrandGroup(undefined).topics).toEqual([]) + }) +}) diff --git a/admin/src/api/config.ts b/admin/src/api/config.ts index f47ec6d..2f17aac 100644 --- a/admin/src/api/config.ts +++ b/admin/src/api/config.ts @@ -1,16 +1,49 @@ /** - * 租户配置中心 API 骨架(Phase 3 Task 3) + * 租户配置中心 API(Phase 3 Task 3 骨架;Task 7a 实装品牌相关调用函数) * * 端点事实源:app/platform/routes/config_routes.py - * (配置快照 / 分组编辑 / 版本历史 / 回滚 / 重置 / 试金石)。 - * 本文件只建类型与端点常量——调用函数在 Task 7(配置中心四件)实装。 + * (配置快照 / 分组编辑 / 版本历史 / 回滚 / 重置 / 试金石) + * 与 app/platform/routes/upload_routes.py(头像上传)。 * * 密钥掩码哨兵:GET 的密钥字段非空时为 '__SET__'(见 src/api/mask.ts - * 的表单语义转换);PUT 提交 '__SET__' = 保持库值。 + * 的表单语义转换);PUT 提交 '__SET__' = 保持库值。品牌组无密钥字段, + * 无哨兵处理需求。 + * + * ## Task 7a 契约修正(对齐后端事实源) + * + * - PUT /config/{group} 的请求体是**组值本身**(整组替换语义),保存备注 + * 走查询参数 comment(config_routes.put_config_group 的 comment 形参); + * - 版本行字段是 updated_by / comment(非骨架期误写的 created_by / note); + * - 回滚请求体只认 version(note 不被后端读取); + * - 试金石响应逐句为 {sentence, action, reason, replacement}。 */ -/** 配置组名(快照顶层键) */ -export type ConfigGroupKey = 'brand' | 'prompts' | 'wall' | 'push' | 'email' | 'ai' +import { http, type RequestOptions } from './client' + +/** 配置组名(快照顶层键;与 config_validation.CONFIG_GROUPS 对齐) */ +export type ConfigGroupKey = + | 'brand' + | 'prompts' + | 'wall' + | 'contact' + | 'email' + | 'push' + | 'ai' + | 'engine' + | 'misc' + +/** 品牌组值(整组替换语义:字段清单固定,topics 为字符串数组) */ +export interface BrandGroupValue { + title: string + color: string + avatar: string + welcome: string + topics: string[] + placeholder: string + whatsapp: string + email: string + privacy_url: string +} /** 当前生效配置响应(GET /api/platform/tenants/{slug}/config) */ export interface TenantConfigResponse { @@ -20,12 +53,20 @@ export interface TenantConfigResponse { config: Record> } -/** 配置版本行(GET .../config/versions) */ +/** 分组保存响应(PUT .../config/{group};config 为该组掩码视图) */ +export interface PutConfigGroupResponse { + slug: string + group: string + version: number + config: Record +} + +/** 配置版本行(GET .../config/versions;字段名与后端一致) */ export interface ConfigVersionItem { version: number created_at: string - created_by: string - note: string + updated_by: string + comment: string } /** 版本列表响应 */ @@ -40,27 +81,43 @@ export interface ConfigVersionDetailResponse extends ConfigVersionItem { config: Record> } -/** 回滚请求(POST .../config/rollback,产生新版本) */ +/** 回滚请求(POST .../config/rollback,产生新版本;后端只读 version) */ export interface ConfigRollbackRequest { version: number - note: string } -/** 分组保存请求(PUT .../config/{group}) */ -export interface PutConfigGroupRequest { - note: string - value: Record +/** 回滚响应 */ +export interface ConfigRollbackResponse { + slug: string + rollback_to: number + version: number + config: Record> } /** 词表试金石请求(POST .../wall/test) */ export interface WallTestRequest { sentences: string[] has_contact?: boolean + source?: 'web' | 'app' } -/** 词表试金石响应(逐句判定) */ +/** 词表试金石响应(逐句判定;action: blocked/replaced/pass) */ export interface WallTestResponse { - results: Array<{ sentence: string; blocked: boolean; rule?: string }> + slug: string + wall_source: 'current' | 'override' + results: Array<{ + sentence: string + action: 'blocked' | 'replaced' | 'pass' + reason: 'boundary' | 'no_contact' | 'reask' | null + replacement: string | null + }> +} + +/** 头像上传响应(POST .../brand/avatar;avatar 为落盘文件名) */ +export interface AvatarUploadResponse { + slug: string + avatar: string + version: number } /** 配置中心端点 */ @@ -74,5 +131,87 @@ export const CONFIG_ENDPOINTS = { versionDetail: (slug: string, version: number) => `/api/platform/tenants/${slug}/config/versions/${version}`, rollback: (slug: string) => `/api/platform/tenants/${slug}/config/rollback`, - wallTest: (slug: string) => `/api/platform/tenants/${slug}/wall/test` + wallTest: (slug: string) => `/api/platform/tenants/${slug}/wall/test`, + avatarUpload: (slug: string) => `/api/platform/tenants/${slug}/brand/avatar` } as const + +/** + * 当前生效配置(MAX 版本快照 + version)。 + * 注意快照是"原样组"——组内缺省键由消费侧深合并出厂默认, + * 品牌表单消费 config.brand 时用 normalizeBrandGroup 防御读取。 + */ +export function fetchTenantConfig( + slug: string, + config?: RequestOptions +): Promise { + return http.get(CONFIG_ENDPOINTS.current(slug), config) +} + +/** + * 分组保存(整组替换语义):请求体 = 组值本身,备注走 comment 查询参数。 + * 调用方必须先 GET 全组、改字段后整组提交——部分提交会丢掉未携带的键 + * (后端 deep merge 出厂默认兜底,但租户既有定制会被覆盖丢失)。 + */ +export function putConfigGroup( + slug: string, + group: ConfigGroupKey | string, + value: Record, + comment?: string, + config?: RequestOptions +): Promise { + return http.put( + CONFIG_ENDPOINTS.putGroup(slug, group), + value, + { + ...config, + params: comment ? { comment } : undefined + } + ) +} + +/** + * 头像上传(multipart file 字段):魔数校验 / 2MB 上限在后端 + * (upload_routes.py),前端做扩展名与大小预检提前拦截。 + * 成功后 brand.avatar 更新为落盘文件名,版本 +1 并热生效。 + */ +export function uploadBrandAvatar( + slug: string, + file: File, + config?: RequestOptions +): Promise { + const form = new FormData() + form.append('file', file) + return http.post( + CONFIG_ENDPOINTS.avatarUpload(slug), + form, + config + ) +} + +/** + * GET 的 brand 组(原样快照,可能缺键/类型漂移)→ 表单友好的完整值。 + * 缺键补空串(与 DEFAULT_CONFIG.brand 的空串出厂一致);topics 只留字符串项。 + */ +export function normalizeBrandGroup( + value: Record | null | undefined +): BrandGroupValue { + const source = value ?? {} + const str = (key: string): string => { + const v = source[key] + return typeof v === 'string' ? v : '' + } + const topics = source.topics + return { + title: str('title'), + color: str('color'), + avatar: str('avatar'), + welcome: str('welcome'), + placeholder: str('placeholder'), + whatsapp: str('whatsapp'), + email: str('email'), + privacy_url: str('privacy_url'), + topics: Array.isArray(topics) + ? topics.filter((t): t is string => typeof t === 'string') + : [] + } +} diff --git a/admin/src/components/business/SiteOverlayFields/index.vue b/admin/src/components/business/SiteOverlayFields/index.vue new file mode 100644 index 0000000..20a9d00 --- /dev/null +++ b/admin/src/components/business/SiteOverlayFields/index.vue @@ -0,0 +1,76 @@ + + + + + + diff --git a/admin/src/components/business/SiteOverlayFields/model.ts b/admin/src/components/business/SiteOverlayFields/model.ts new file mode 100644 index 0000000..e7350c1 --- /dev/null +++ b/admin/src/components/business/SiteOverlayFields/model.ts @@ -0,0 +1,89 @@ +/** + * 站点门面覆盖层字段模型(Task 6 建立;Task 7a 提取共用) + * + * 消费方:站点管理页(tenants/list)的「新建站点」与「编辑门面」弹窗—— + * 两处共用同一字段清单 / 空表单 / 提交收集逻辑。 + * + * 语义(与后端 tenant_routes.patch_site 的整字典赋值对齐): + * - buildOverlay 只收集非空字段——留空 = 剔除该键的覆盖 = 继承租户默认 + * 品牌(brand 组);旧键不残留(整字典替换,M-2a)。 + * - overlay 值是 brand 组子集(站点级覆盖,public.py 的 site.brand 深合并)。 + */ + +/** 门面覆盖层字符串字段(brand 组子集) */ +export const OVERLAY_FIELDS = [ + { key: 'title', label: '标题 title' }, + { key: 'color', label: '主题色 color' }, + { key: 'avatar', label: '头像路径 avatar' }, + { key: 'welcome', label: '欢迎语 welcome' }, + { key: 'placeholder', label: '输入框占位 placeholder' }, + { key: 'whatsapp', label: 'WhatsApp' }, + { key: 'email', label: '邮箱 email' }, + { key: 'privacy_url', label: '隐私政策 URL' } +] as const + +/** 覆盖层字段 key 联合类型 */ +export type OverlayFieldKey = (typeof OVERLAY_FIELDS)[number]['key'] + +/** 覆盖层表单状态(字符串字段全空串语义 + topics 数组) */ +export interface OverlayFormState extends Record { + topics: string[] +} + +/** 覆盖层字段中文标签(站点表 tooltip 用;topics 单列) */ +export const OVERLAY_LABELS: Record = Object.fromEntries( + OVERLAY_FIELDS.map((f) => [f.key, f.label] as [string, string]).concat([ + ['topics', '快捷话题 topics'] + ]) +) + +/** 空表单(全留空 = 完全继承租户默认) */ +export function blankOverlayForm(): OverlayFormState { + return { + title: '', + color: '', + avatar: '', + welcome: '', + placeholder: '', + whatsapp: '', + email: '', + privacy_url: '', + topics: [] + } +} + +/** + * 表单 → brand_overlay 提交字典:只收集非空字段(留空 = 不覆盖)。 + * 全空表单产出 {} = 清空覆盖层(整字典赋值语义下回到完全继承)。 + */ +export function buildOverlay(form: OverlayFormState): Record { + const overlay: Record = {} + for (const field of OVERLAY_FIELDS) { + const value = form[field.key].trim() + if (value) overlay[field.key] = value + } + const topics = form.topics.map((t) => t.trim()).filter(Boolean) + if (topics.length > 0) overlay.topics = topics + return overlay +} + +/** + * overlay 落库值 → 表单预填(编辑门面用):只认已知键的字符串值; + * 返回表单之外未能识别的键(保存时会被整字典替换丢弃,调用方提示用户)。 + */ +export function overlayToForm( + overlay: Record | null | undefined +): { form: OverlayFormState; unknownKeys: string[] } { + const form = blankOverlayForm() + const source = overlay ?? {} + for (const field of OVERLAY_FIELDS) { + const value = source[field.key] + if (typeof value === 'string') form[field.key] = value + } + if (Array.isArray(source.topics)) { + form.topics = source.topics.filter((t): t is string => typeof t === 'string') + } + const known = new Set([...OVERLAY_FIELDS.map((f) => f.key), 'topics']) + const unknownKeys = Object.keys(source).filter((k) => !known.has(k)) + return { form, unknownKeys } +} diff --git a/admin/src/types/import/components.d.ts b/admin/src/types/import/components.d.ts index 3f71ccc..0a47716 100644 --- a/admin/src/types/import/components.d.ts +++ b/admin/src/types/import/components.d.ts @@ -70,6 +70,7 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] @@ -119,6 +120,7 @@ declare module 'vue' { SettingHeader: typeof import('./../../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default'] SettingItem: typeof import('./../../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default'] SidebarSubmenu: typeof import('./../../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default'] + SiteOverlayFields: typeof import('./../../components/business/SiteOverlayFields/index.vue')['default'] ThemeSettings: typeof import('./../../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default'] ThemeSvg: typeof import('./../../components/core/theme/theme-svg/index.vue')['default'] } diff --git a/admin/src/views/config/center/index.vue b/admin/src/views/config/center/index.vue index 0814069..b123b39 100644 --- a/admin/src/views/config/center/index.vue +++ b/admin/src/views/config/center/index.vue @@ -1,25 +1,1099 @@ - + diff --git a/admin/src/views/tenants/list/index.vue b/admin/src/views/tenants/list/index.vue index 612396a..52a87af 100644 --- a/admin/src/views/tenants/list/index.vue +++ b/admin/src/views/tenants/list/index.vue @@ -8,7 +8,9 @@ 全局唯一 → 400 引导改 blank 或先改占用方)→ ④管理员账号 → 创建 (201 后列表刷新 + 提示"租户已创建,管理员首登需改密") - 站点管理(详情抽屉内):站点列表/新建(门面覆盖层表单,留空=继承租户 - 默认)/停用复启用/域名绑定解绑(撞车 400 的归属信息内联展示) + 默认)/编辑门面(Task 7a:PATCH brand_overlay 整字典,留空剔除,共用 + SiteOverlayFields 组件)/停用复启用/域名绑定解绑(撞车 400 的归属信息 + 内联展示) - 停用后果文案(Phase 2 语义):租户停用=官网挂件 503 服务暂停页; 站点停用=该站点域名 404(不是服务暂停)——确认框写明 - slug 不可变(后端 PATCH 带 slug 键 400)——界面不提供改 slug 入口 @@ -203,8 +205,11 @@ 继承租户默认 - + + + + + + + @@ -365,6 +416,8 @@ uploadBrandAvatar, type BrandGroupValue } from '@/api/config' + import PromptsPane from './PromptsPane.vue' + import WallPane from './WallPane.vue' defineOptions({ name: 'ConfigCenter' }) @@ -375,7 +428,7 @@ const AVATAR_MAX_BYTES = 2 * 1024 * 1024 const AVATAR_EXTS = ['png', 'jpg', 'jpeg', 'webp'] - // ---- 左侧组导航(本任务实装品牌;其余占位) ------------------------------------------- + // ---- 左侧组导航(Task 7a 品牌 / Task 7b 提示词+词表实装;其余占位) -------------------- const GROUP_NAV = [ { key: 'brand', label: '品牌' }, @@ -385,6 +438,9 @@ { key: 'versions', label: '版本' } ] as const + /** 已实装组(导航徽标) */ + const IMPLEMENTED_GROUPS = new Set(['brand', 'prompts', 'wall']) + type GroupKey = (typeof GROUP_NAV)[number]['key'] const activeGroup = ref('brand') @@ -423,12 +479,14 @@ } } - // ---- 配置(品牌组)状态 --------------------------------------------------------------- + // ---- 配置状态(品牌表单 + 整份快照——Task 7b 起提示词/词表 pane 消费快照组) ---------- const version = ref(null) const configLoading = ref(false) const configError = ref('') const configLoaded = ref(false) + /** 整份原样快照(prompts/wall pane 的组值来源;组保存成功后逐组回写) */ + const snapshot = ref> | null>(null) /** 加载世代(切租户时丢弃在途响应) */ const configEpoch = ref(0) @@ -459,6 +517,7 @@ const res = await fetchTenantConfig(slug) if (epoch !== configEpoch.value || slug !== activeSlug.value) return version.value = res.version + snapshot.value = res.config Object.assign(form, normalizeBrandGroup(res.config.brand)) configLoaded.value = true avatarImgFailed.value = false @@ -470,6 +529,21 @@ } } + /** pane(提示词/词表)保存成功:回写版本号与快照组(pane 自持编辑态不重置) */ + function onPaneSaved(payload: { + version: number + group: string + config: Record + }): void { + version.value = payload.version + if (snapshot.value) { + snapshot.value = { + ...snapshot.value, + [payload.group]: payload.config + } + } + } + function reloadConfig(): void { loadConfig(++configEpoch.value) } @@ -670,6 +744,7 @@ configLoaded.value = false configError.value = '' saveComment.value = '' + snapshot.value = null detail.value = null detailError.value = '' embedDomain.value = '' @@ -1096,4 +1171,13 @@ .placeholder-card { flex: 1; } + + // ---- 提示词/词表 pane 容器(Task 7b) ------------------------------------------------ + + .group-pane { + display: flex; + flex: 1; + min-width: 0; + align-items: stretch; + } diff --git a/admin/src/views/config/center/model.ts b/admin/src/views/config/center/model.ts new file mode 100644 index 0000000..db1389f --- /dev/null +++ b/admin/src/views/config/center/model.ts @@ -0,0 +1,425 @@ +/** + * 配置中心提示词/词表编辑模型(Phase 3 Task 7b) + * + * 消费方:config/center 的 PromptsPane.vue / WallPane.vue。纯逻辑无 DOM + * 依赖,vitest 直接覆盖(__tests__/model.test.ts)。 + * + * 事实源对齐: + * - 提示词四件套键名/字节上限:app/engine/services/ai_service.PROMPTS_KEYS + * 与 app/platform/config_validation.PROMPTS_SIZE_CAPS(company_facts + * 50KB 软上限 spec §5.7,其余 100KB;UTF-8 字节口径——与后端 + * len(text.encode('utf-8')) 一致,用 TextEncoder 计数); + * - 词表结构:app/engine/wall_schema.py(seed_profiles/jiancai/wall.json + * 为真实形态样本)。正则条目双形态:纯字符串 | {pattern, flags?, + * comment?}(flags 仅支持 'i');PUT wall 整组提交,未结构化编辑的 + * 顶层键(含 "_" 前缀元信息键)原样透传; + * - 语言池必填 en/zh(wall_schema._REQUIRED_POOLS),reminders 六类 / + * cc_templates 四组的变体键为封闭集合(未知键编译期显性拒绝)。 + */ +// ---- 提示词四件套 ----------------------------------------------------------------- + +/** 四件套键名(ai_service.PROMPTS_KEYS 镜像;prompts 是整组 PUT 的四键 dict) */ +export const PROMPTS_KEYS = [ + 'system', + 'company_facts', + 'extractor', + 'evaluator' +] as const + +export type PromptKey = (typeof PROMPTS_KEYS)[number] + +export type PromptsDraft = Record + +/** 各件字节上限(UTF-8 口径;config_validation.PROMPTS_SIZE_CAPS 镜像) */ +export const PROMPTS_CAPS: Record = { + system: 100 * 1024, + company_facts: 50 * 1024, + extractor: 100 * 1024, + evaluator: 100 * 1024 +} + +/** 四件套编辑器元数据(label/hint 为 UI 展示;deflectionPoolNote 仅 system) */ +export const PROMPTS_META: Array<{ + key: PromptKey + label: string + rows: number + hint: string +}> = [ + { + key: 'system', + label: '系统提示词 system.md', + rows: 14, + hint: 'AI 行为总纲(职能边界/语言跟随等)' + }, + { + key: 'company_facts', + label: '公司事实卡 company_facts.md', + rows: 10, + hint: '每轮整段拼入 system prompt——保持精炼(50KB 软上限)' + }, + { + key: 'extractor', + label: '线索提取器 extractor.md', + rows: 8, + hint: '$existing_info / $messages 占位符由运行时回填' + }, + { + key: 'evaluator', + label: '意向评估器 evaluator.md', + rows: 8, + hint: '$profile / $recent_messages 占位符由运行时回填' + } +] + +/** UTF-8 字节数(后端 len(text.encode('utf-8')) 同口径) */ +export function utf8Bytes(text: string): number { + return new TextEncoder().encode(text).length +} + +/** 字节数展示(KB 一位小数) */ +export function formatKB(bytes: number): string { + return `${(bytes / 1024).toFixed(1)}KB` +} + +/** 单件字节状态(实时提示用) */ +export interface PromptByteInfo { + bytes: number + cap: number + over: boolean + label: string +} + +export function promptByteInfo(key: PromptKey, text: string): PromptByteInfo { + const bytes = utf8Bytes(text) + const cap = PROMPTS_CAPS[key] + return { + bytes, + cap, + over: bytes > cap, + label: `${formatKB(bytes)} / ${formatKB(cap)}` + } +} + +/** GET 的 prompts 组(原样快照,可能缺键/类型漂移)→ 四键字符串表单值 */ +export function normalizePromptsGroup( + value: Record | null | undefined +): PromptsDraft { + const source = value ?? {} + const out = {} as PromptsDraft + for (const key of PROMPTS_KEYS) { + const v = source[key] + out[key] = typeof v === 'string' ? v : '' + } + return out +} + +/** 四件套是否有超限件(超限即禁存——后端同样硬拒,前移到按钮态) */ +export function promptsOverCap(draft: PromptsDraft): boolean { + return PROMPTS_KEYS.some((key) => utf8Bytes(draft[key]) > PROMPTS_CAPS[key]) +} + +// ---- 词表(wall 组)--------------------------------------------------------------- + +/** 正则条目双形态(wall_schema._compile_pattern):纯字符串 | dict */ +export type WallPatternEntry = + | string + | { pattern?: unknown; flags?: unknown; comment?: unknown } + +/** 正则条目编辑行(双形态归一;flags 仅 '' | 'i'——_FLAG_MAP 只认 'i') */ +export interface RegexRow { + pattern: string + flags: '' | 'i' + comment: string + /** 备注编辑展开态(瞬态 UI 状态,不参与序列化) */ + showComment: boolean +} + +function blankRow(): RegexRow { + return { pattern: '', flags: '', comment: '', showComment: false } +} + +/** 防御读取:一层对象或 null */ +function asRecord(value: unknown): Record | null { + return value !== null && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : null +} + +/** 条目 → 编辑行(字符串/dict 双形态;异常值防御归空) */ +export function parsePatternEntry(entry: unknown): RegexRow { + if (typeof entry === 'string') { + return { pattern: entry, flags: '', comment: '', showComment: false } + } + const dict = asRecord(entry) + if (dict) { + return { + pattern: asStr(dict.pattern), + flags: dict.flags === 'i' ? 'i' : '', + comment: asStr(dict.comment), + showComment: false + } + } + return blankRow() +} + +/** 编辑行 → 条目:无 flags 无备注 → 纯字符串(两种形态对编译等价, + * 与 wall.json 的极简条目保持一致);否则 dict 且只带非空键。 + * 空 pattern 行返回 null(编译必 400「缺非空 pattern」,序列化期剔除)。 */ +export function rowToEntry( + row: RegexRow +): string | Record | null { + if (!row.pattern) return null + if (!row.flags && !row.comment.trim()) return row.pattern + const out: Record = { pattern: row.pattern } + if (row.flags) out.flags = row.flags + if (row.comment.trim()) out.comment = row.comment + return out +} + +/** 正则列表序列化(空 pattern 行剔除;列表可为空——由后端编译校验报路径) */ +export function rowsToEntries( + rows: RegexRow[] +): Array> { + return rows + .map(rowToEntry) + .filter((e): e is string | Record => e !== null) +} + +/** 单条目字段(triggers 三键 / zero_number 两键)编辑行 → 条目: + * 无标志无备注且 pattern 非空 → 纯字符串(wall.json zero_number 出厂 + * 形态,编译等价、往返无损);否则 dict 只带非空键——空 pattern 也走 + * dict 分支,后端报「缺非空 pattern」字段路径(裸 '' 会编成匹配一切的 + * 空正则,必须显性失败) */ +export function rowToSingleEntry( + row: RegexRow +): string | Record { + if (row.pattern && !row.flags && !row.comment.trim()) return row.pattern + const out: Record = { pattern: row.pattern } + if (row.flags) out.flags = row.flags + if (row.comment.trim()) out.comment = row.comment + return out +} + +// ---- 语言池 / 话术区结构(wall_schema 变体键镜像;封闭集合) ------------------------ + +/** 四个语言池字段(en/zh 必填——_REQUIRED_POOLS) */ +export const POOL_FIELDS = [ + 'deflections', + 'deflections_contact', + 'pivots', + 'pivots_contact' +] as const + +export type PoolField = (typeof POOL_FIELDS)[number] + +/** 池必填语言(删除时前端拦截;后端编译同样拒绝缺失) */ +export const POOL_REQUIRED_LANGS = ['en', 'zh'] as const + +/** reminders 六类 × 变体(wall_schema._REMINDER_VARIANTS 镜像) */ +export const REMINDER_VARIANTS: Record = { + boundary: ['budget', 'normal'], + contact_ask: ['app_already', 'app', 'web'], + language_follow: ['zh', 'es'], + lead_given: ['forbidden', 'respond_script', 'boundary_tail', 'suspect'], + cc_guide: ['echo', 'ask'], + email_light: ['en', 'zh'] +} + +/** cc_templates 四组 × 语言(wall_schema._CC_TEMPLATE_VARIANTS 镜像) */ +export const CC_TEMPLATE_VARIANTS: Record = { + ask: ['en', 'zh', 'es'], + wall: ['en', 'zh', 'es'], + note: ['en', 'zh'], + echo: ['en', 'zh', 'es'] +} + +export const NO_CONTACT_KEYS = ['web', 'app', 'already'] as const + +/** triggers 三键 / zero_number 两键(单条目正则字段) */ +export const TRIGGER_KEYS = ['boundary', 'budget', 'contact_ask'] as const +export const ZERO_NUMBER_KEYS = ['long_number', 'any_email'] as const + +// ---- 词表整体编辑模型 -------------------------------------------------------------- + +/** 词表编辑模型:结构化编辑区(全部归一为可 v-model 的形态)+ 透传区 */ +export interface WallEditModel { + /** 未提供结构化编辑的顶层键原样透传(含 "_" 前缀元信息键) */ + passthrough: Record + priceRows: RegexRow[] + reaskRows: RegexRow[] + /** triggers.boundary/budget/contact_ask + zero_number.long_number/any_email */ + singles: Record<(typeof TRIGGER_KEYS)[number] | (typeof ZERO_NUMBER_KEYS)[number], RegexRow> + /** 四池:语言 → 句子列表(可增删行;序列化时空行剔除) */ + pools: Record> + noContact: Record<(typeof NO_CONTACT_KEYS)[number], string> + /** 六类 × 变体 → 话术文本(变体键封闭集合,缺项补空串交后端报路径) */ + reminders: Record> + /** 四组 × 语言 → 模板文本({intl}/{cc_script} 等占位符白名单由后端校验) */ + ccTemplates: Record> +} + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T +} + +/** 防御读取:字符串或空串 */ +function asStr(value: unknown): string { + return typeof value === 'string' ? value : '' +} + +/** 防御读取:语言池 → 语言到字符串数组的映射(非字符串项剔除) */ +function asPool(value: unknown): Record { + const source = asRecord(value) + if (!source) return {} + const out: Record = {} + for (const [lang, items] of Object.entries(source)) { + if (Array.isArray(items)) { + const rows = items.filter((t): t is string => typeof t === 'string') + if (rows.length > 0) out[lang] = rows + } + } + return out +} + +/** 防御读取:一层字符串映射 → 只保留 variant 集合内的键(缺项补空串) */ +function asVariantMap( + value: unknown, + variants: string[] +): Record { + const source = asRecord(value) ?? {} + const out: Record = {} + for (const key of variants) { + out[key] = asStr(source[key]) + } + return out +} + +/** 结构化编辑接管的顶层键(其余透传) */ +const KNOWN_WALL_EDIT_KEYS = new Set([ + 'price_patterns', + 'reask_patterns', + 'triggers', + 'zero_number', + ...POOL_FIELDS, + 'no_contact', + 'reminders', + 'cc_templates' +]) + +/** + * GET 的 wall 组 → 编辑模型。wall 缺失/非对象/空对象 → null(面板出空态 + * ——wall 组来自租户 seed 的 wall.json,缺失时无从结构化编辑)。结构上 + * 无法通过 PUT 校验的存量数据(理论不可达——PUT 前有编译校验)也归一为 + * 可编辑形态,保存时由后端 400 带字段路径显性兜底。 + */ +export function initWallEdit( + source: Record | null | undefined +): WallEditModel | null { + const record = asRecord(source) + if (!record || Object.keys(record).length === 0) return null + + const rest = Object.fromEntries( + Object.entries(record).filter(([k]) => !KNOWN_WALL_EDIT_KEYS.has(k)) + ) + const triggersSrc = asRecord(record.triggers) + const zeroSrc = asRecord(record.zero_number) + + const singles = {} as WallEditModel['singles'] + for (const key of TRIGGER_KEYS) { + singles[key] = parsePatternEntry(triggersSrc?.[key]) + } + for (const key of ZERO_NUMBER_KEYS) { + singles[key] = parsePatternEntry(zeroSrc?.[key]) + } + + const pools = {} as WallEditModel['pools'] + for (const field of POOL_FIELDS) { + pools[field] = asPool(record[field]) + } + + const reminders: Record> = {} + const remindersSrc = asRecord(record.reminders) + for (const [cat, variants] of Object.entries(REMINDER_VARIANTS)) { + reminders[cat] = asVariantMap(remindersSrc?.[cat], variants) + } + + const ccTemplates: Record> = {} + const ccSrc = asRecord(record.cc_templates) + for (const [group, langs] of Object.entries(CC_TEMPLATE_VARIANTS)) { + ccTemplates[group] = asVariantMap(ccSrc?.[group], langs) + } + + const noContact = asVariantMap(record.no_contact, [...NO_CONTACT_KEYS]) as WallEditModel['noContact'] + + return { + passthrough: clone(rest), + priceRows: parsePatternList(record.price_patterns), + reaskRows: parsePatternList(record.reask_patterns), + singles, + pools, + noContact, + reminders, + ccTemplates + } +} + +function parsePatternList(value: unknown): RegexRow[] { + return Array.isArray(value) ? value.map(parsePatternEntry) : [] +} + +/** 池序列化:空行剔除;剔除后为空的语言整键丢弃(空列表必被编译拒绝, + * 丢弃后缺 en/zh 由后端报「缺语言」路径——同为显性失败,语义更准) */ +function cleanPool(pool: Record): Record { + const out: Record = {} + for (const [lang, rows] of Object.entries(pool)) { + const cleaned = rows.filter((t) => t.trim() !== '') + if (cleaned.length > 0) out[lang] = cleaned + } + return out +} + +/** + * 编辑模型 → PUT config/wall 整组载荷:结构化区按编辑值重组,其余顶层键 + * 原样透传。正则编译/结构问题不在前端造校验器——后端 compile_wall 的 + * 400 自带字段路径(含试金石预演路径),内联展示即可。 + */ +export function buildWallPayload(model: WallEditModel): Record { + const triggers: Record = {} + for (const key of TRIGGER_KEYS) { + triggers[key] = rowToSingleEntry(model.singles[key]) + } + const zeroNumber: Record = {} + for (const key of ZERO_NUMBER_KEYS) { + zeroNumber[key] = rowToSingleEntry(model.singles[key]) + } + const pools: Record = {} + for (const field of POOL_FIELDS) { + pools[field] = cleanPool(model.pools[field]) + } + return { + ...model.passthrough, + price_patterns: rowsToEntries(model.priceRows), + reask_patterns: rowsToEntries(model.reaskRows), + triggers, + zero_number: zeroNumber, + ...pools, + no_contact: { ...model.noContact }, + reminders: clone(model.reminders), + cc_templates: clone(model.ccTemplates) + } +} + +// ---- 试金石 ------------------------------------------------------------------------ + +/** 试金石默认示例句:一句价格墙(blocked)/ 一句零号码墙(replaced)/ + * 一句干净(pass)——三色判定一屏可见 */ +export const DEFAULT_TOUCHSTONE_SENTENCES = [ + 'The price is 500 USD per ton.', + 'You can reach me at john.doe@example.com.', + 'What sizes of PPR pipes do you have in stock?' +] + +/** 试金石请求句集:去空白过滤空行(后端要求非空字符串列表) */ +export function touchstoneSentences(sentences: string[]): string[] { + return sentences.map((s) => s.trim()).filter(Boolean) +} From bf923d9cf77239f54a8587f089a90e19e472ecb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 20:55:28 +0800 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AE=E4=B8=AD?= =?UTF-8?q?=E5=BF=83-=E5=87=AD=E6=8D=AE=EF=BC=88=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E6=8E=A8=E9=80=81AI=E8=A1=A8=E5=8D=95/=E6=8E=A9=E7=A0=81?= =?UTF-8?q?=E4=BA=A4=E4=BA=92/SMTP=E6=B5=8B=E8=AF=95/=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E6=8E=A2=E6=B5=8B=E7=AB=AF=E7=82=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 POST /tenants/{slug}/email/test:resolve_email_settings 当前配置 真发测试邮件(主题 [all-kefu] SMTP 测试邮件+租户名+UTC+8 时间); 回退后 host/user 仍空 → 400;成败如实返回 {sent, error},密钥不进响应 - 后端 POST /tenants/{slug}/push/test:连通性探测——与 _do_post 同款 连接形态 POST 空 messages 批量(红线:绝不携带线索数据);不可达 reason 分类 dns/timeout/connect(Windows 裸 ConnectError 文案兜底), auth_ok 三态(2xx 真 / 401·403 假 / 其余 null),单次无重试 - email_service 抽 _smtp_send 共用连接逻辑(_send 契约不变)+ send_test_email(成败+错误串如实返回,不走吞异常 bool) - 前端 CredsPane:邮件/推送/AI 三组凭据表单(数值/邮箱前端预检 + 后端 400 双层);密钥哨兵交互(__SET__ 占位符/不动保存保持库值/ 清除按钮显式清空可撤销);SMTP 测试与推送探测三态内联展示 - 测试:pytest 新增 18(email/test 7 + push/test 11),全量 595 通过; vitest 新增 20(model 哨兵往返/预检边界 + API 调用形态),107 通过 Co-Authored-By: Claude Code --- admin/src/api/__tests__/config.test.ts | 71 +- admin/src/api/config.ts | 73 +- admin/src/types/import/components.d.ts | 1 + admin/src/views/config/center/CredsPane.vue | 743 ++++++++++++++++++ .../config/center/__tests__/model.test.ts | 273 ++++++- admin/src/views/config/center/index.vue | 38 +- admin/src/views/config/center/model.ts | 260 +++++- app/engine/services/email_service.py | 50 +- app/platform/routes/config_routes.py | 114 +++ tests/test_email_test_endpoint.py | 177 +++++ tests/test_push_test_endpoint.py | 243 ++++++ 11 files changed, 2024 insertions(+), 19 deletions(-) create mode 100644 admin/src/views/config/center/CredsPane.vue create mode 100644 tests/test_email_test_endpoint.py create mode 100644 tests/test_push_test_endpoint.py diff --git a/admin/src/api/__tests__/config.test.ts b/admin/src/api/__tests__/config.test.ts index 4795102..f267988 100644 --- a/admin/src/api/__tests__/config.test.ts +++ b/admin/src/api/__tests__/config.test.ts @@ -1,5 +1,6 @@ /** - * 配置中心 API 单元测试(Phase 3 Task 7a——品牌相关;Task 7b——试金石) + * 配置中心 API 单元测试(Phase 3 Task 7a——品牌相关;Task 7b——试金石; + * Task 7c——凭据测试端点) * * 钉住 API 层契约(事实源 app/platform/routes/config_routes.py 与 * upload_routes.py);HTTP 层行为已在 client.test.ts 覆盖,此处 mock http @@ -9,6 +10,9 @@ * 包装),备注走 comment 查询参数,无备注时不带 params * - POST wall/test(Task 7b):句子列表 + wall 覆盖(当前表单值预演, * 不落库)+ has_contact/source 可选项——保存前自动验证复用本函数 + * - POST email/test · push/test(Task 7c):前者 body 只带 {to}(用已 + * 保存配置真发一封);后者无请求体(探测读已保存配置,空批量零线索 + * 数据)——两者 showErrorMessage: false 内联展示 * - 头像上传:multipart file 字段(FormData) * - normalizeBrandGroup:缺键补空串 / 类型漂移防御 / topics 只留字符串项 */ @@ -24,6 +28,8 @@ import { fetchTenantConfig, normalizeBrandGroup, putConfigGroup, + testEmail, + testPush, testWall, uploadBrandAvatar } from '@/api/config' @@ -194,6 +200,69 @@ describe('试金石 API(testWall · Phase 3 Task 7b)', () => { }) }) +describe('凭据测试 API(testEmail / testPush · Phase 3 Task 7c)', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('testEmail:POST email/test,body 只带 {to}(收件人)', async () => { + const response = { slug: 'jiancai', sent: true, error: null } + vi.mocked(http.post).mockResolvedValue(response) + + const res = await testEmail('jiancai', { to: 'you@example.com' }, { + showErrorMessage: false + }) + + expect(res).toEqual(response) + expect(http.post).toHaveBeenCalledTimes(1) + expect(http.post).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/email/test', + { to: 'you@example.com' }, + { showErrorMessage: false } // 400「SMTP 未配置」等内联展示,不走全局提示 + ) + }) + + it('testEmail:发送失败也走成功响应通道(sent=false + error 串,非异常)', async () => { + const response = { slug: 'jiancai', sent: false, error: 'SMTPRecipientsRefused(...)' } + vi.mocked(http.post).mockResolvedValue(response) + + const res = await testEmail('jiancai', { to: 'you@example.com' }) + + expect(res.sent).toBe(false) + expect(res.error).toContain('SMTP') + }) + + it('testPush:POST push/test,无请求体(探测读已保存配置)', async () => { + const response = { + slug: 'jiancai', + reachable: false, + auth_ok: null, + status: null, + reason: 'dns', + error: 'Failed to resolve.' + } + vi.mocked(http.post).mockResolvedValue(response) + + const res = await testPush('jiancai', { showErrorMessage: false }) + + expect(res).toEqual(response) + expect(http.post).toHaveBeenCalledTimes(1) + const [url, body, config] = vi.mocked(http.post).mock.calls[0] + expect(url).toBe('/api/platform/tenants/jiancai/push/test') + expect(body).toBeUndefined() // 端点不读 body——探测参数全来自已保存配置 + expect(config).toEqual({ showErrorMessage: false }) + }) + + it('CONFIG_ENDPOINTS.emailTest / pushTest:端点路径与后端 config_routes 一致', () => { + expect(CONFIG_ENDPOINTS.emailTest('jiancai')).toBe( + '/api/platform/tenants/jiancai/email/test' + ) + expect(CONFIG_ENDPOINTS.pushTest('jiancai')).toBe( + '/api/platform/tenants/jiancai/push/test' + ) + }) +}) + describe('normalizeBrandGroup(GET 原样组 → 表单完整值)', () => { it('缺键补空串、topics 缺省为空数组(快照是原样组,可能缺键)', () => { const out = normalizeBrandGroup({ title: 'HESUPIPE Support' }) diff --git a/admin/src/api/config.ts b/admin/src/api/config.ts index fcdd87e..19920bf 100644 --- a/admin/src/api/config.ts +++ b/admin/src/api/config.ts @@ -25,6 +25,14 @@ * 400 带字段路径;has_contact / source 可选(默认 false / web); * - 判定优先级与 conversation.py 执行侧一致:价格墙(blocked)→ * 零号码墙(replaced)→ 再索要墙(仅 has_contact,blocked 无替换)。 + * + * ## Task 7c 凭据测试契约(config_routes.test_email / test_push) + * + * - POST email/test:body {to}——后端用**已保存**的当前邮件配置真发一封 + * 测试邮件;SMTP 未配置 → 400;成败如实返回(sent=false 时带 error); + * - POST push/test:连通性探测——只验 URL 可达 + 鉴权,**不发任何线索 + * 数据**(后端发空 messages 批量);不可达时 reason 分类 + * dns/timeout/connect,auth_ok 三态(true/false/null=不定)。 */ import { http, type RequestOptions } from './client' @@ -132,6 +140,32 @@ export interface AvatarUploadResponse { version: number } +/** SMTP 测试邮件请求(POST .../email/test;用**已保存**配置真发一封到 to) */ +export interface EmailTestRequest { + to: string +} + +/** SMTP 测试邮件响应(发送失败不 5xx——sent=false 时 error 带原因) */ +export interface EmailTestResponse { + slug: string + sent: boolean + error: string | null +} + +/** 推送探测不可达原因分类(前端据此给修复提示文案) */ +export type PushProbeReason = 'dns' | 'timeout' | 'connect' + +/** 推送连通性探测响应(POST .../push/test;空批量预检,零线索数据) */ +export interface PushTestResponse { + slug: string + reachable: boolean + /** true=2xx / false=401·403 / null=其他状态(到达但鉴权不定)或不可达 */ + auth_ok: boolean | null + status: number | null + reason: PushProbeReason | null + error: string | null +} + /** 配置中心端点 */ export const CONFIG_ENDPOINTS = { current: (slug: string) => `/api/platform/tenants/${slug}/config`, @@ -144,7 +178,9 @@ export const CONFIG_ENDPOINTS = { `/api/platform/tenants/${slug}/config/versions/${version}`, rollback: (slug: string) => `/api/platform/tenants/${slug}/config/rollback`, wallTest: (slug: string) => `/api/platform/tenants/${slug}/wall/test`, - avatarUpload: (slug: string) => `/api/platform/tenants/${slug}/brand/avatar` + avatarUpload: (slug: string) => `/api/platform/tenants/${slug}/brand/avatar`, + emailTest: (slug: string) => `/api/platform/tenants/${slug}/email/test`, + pushTest: (slug: string) => `/api/platform/tenants/${slug}/push/test` } as const /** @@ -214,6 +250,41 @@ export function uploadBrandAvatar( ) } +/** + * SMTP 测试邮件(POST .../email/test):用**已保存**的当前邮件配置真发一封 + * 到 to——表单里未保存的编辑不生效(界面需提示"先保存再测试")。SMTP 未 + * 配置 → 400(调用方 showErrorMessage: false 自行内联展示);发送失败 + * 不抛错——sent=false 时 error 带原因串。 + */ +export function testEmail( + slug: string, + body: EmailTestRequest, + config?: RequestOptions +): Promise { + return http.post( + CONFIG_ENDPOINTS.emailTest(slug), + body, + config + ) +} + +/** + * 推送连通性探测(POST .../push/test):只验 URL 可达 + 鉴权,后端发空 + * messages 批量(零线索数据)。URL 未配置 → 400;结果三态由调用方按 + * reachable/auth_ok 展示(不可达 reason 分类 dns/timeout/connect)。 + * 同样用**已保存**配置——改完 URL 先保存再探测。 + */ +export function testPush( + slug: string, + config?: RequestOptions +): Promise { + return http.post( + CONFIG_ENDPOINTS.pushTest(slug), + undefined, + config + ) +} + /** * GET 的 brand 组(原样快照,可能缺键/类型漂移)→ 表单友好的完整值。 * 缺键补空串作表单中性值:提交空串 = 显式清空覆盖出厂(出厂 diff --git a/admin/src/types/import/components.d.ts b/admin/src/types/import/components.d.ts index 4457df4..b9177ab 100644 --- a/admin/src/types/import/components.d.ts +++ b/admin/src/types/import/components.d.ts @@ -78,6 +78,7 @@ declare module 'vue' { ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDivider: typeof import('element-plus/es')['ElDivider'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] diff --git a/admin/src/views/config/center/CredsPane.vue b/admin/src/views/config/center/CredsPane.vue new file mode 100644 index 0000000..5271650 --- /dev/null +++ b/admin/src/views/config/center/CredsPane.vue @@ -0,0 +1,743 @@ + + + + + + diff --git a/admin/src/views/config/center/__tests__/model.test.ts b/admin/src/views/config/center/__tests__/model.test.ts index 3b6bc15..66e3cf1 100644 --- a/admin/src/views/config/center/__tests__/model.test.ts +++ b/admin/src/views/config/center/__tests__/model.test.ts @@ -1,5 +1,5 @@ /** - * 配置中心提示词/词表编辑模型单元测试(Phase 3 Task 7b) + * 配置中心提示词/词表/凭据编辑模型单元测试(Phase 3 Task 7b + 7c) * * 钉住 src/views/config/center/model.ts 的纯逻辑契约(事实源: * app/platform/config_validation.py 的 PROMPTS_SIZE_CAPS 与 @@ -9,7 +9,11 @@ * - 词表:正则条目双形态(纯字符串 | {pattern, flags, comment})的 * 解析/序列化往返、空行剔除、语言池清洗、initWallEdit 对透传键的 * 保留、buildWallPayload 的整组重组; - * - 变体键镜像:reminders 六类 / cc_templates 四组与后端常量同构。 + * - 变体键镜像:reminders 六类 / cc_templates 四组与后端常量同构; + * - 凭据三组(Task 7c):normalize(掩码哨兵态/数值转串/缺键防御)、 + * build(密钥哨兵往返——不动保存回 '__SET__' 保持库值;空串 = 清空; + * 新值优先;数值空串 → null)与 validate 前端预检(端口 1-65535 / + * 邮箱含 @ / push URL 协议与正数·1-10 整数边界,与后端校验同款)。 */ import { describe, expect, it } from 'vitest' import { @@ -19,13 +23,21 @@ import { POOL_FIELDS, PROMPTS_CAPS, PROMPTS_KEYS, + PUSH_PROBE_REASON_LABELS, REMINDER_VARIANTS, TRIGGER_KEYS, ZERO_NUMBER_KEYS, + asSecretMasked, + buildAiCredsPayload, + buildEmailCredsPayload, + buildPushCredsPayload, buildWallPayload, formatKB, initWallEdit, + normalizeAiCredsGroup, + normalizeEmailCredsGroup, normalizePromptsGroup, + normalizePushCredsGroup, parsePatternEntry, promptByteInfo, promptsOverCap, @@ -34,7 +46,11 @@ import { rowToSingleEntry, touchstoneSentences, utf8Bytes, - type PromptsDraft + validateEmailCredsForm, + validatePushCredsForm, + type EmailCredsForm, + type PromptsDraft, + type PushCredsForm } from '../model' // ---- 提示词四件套 ------------------------------------------------------------------- @@ -357,3 +373,254 @@ describe('试金石句集', () => { expect(touchstoneSentences([])).toEqual([]) }) }) + +// ---- 凭据三组(email / push / ai,Phase 3 Task 7c) ----------------------------------- + +describe('asSecretMasked(GET 密钥值 → 掩码哨兵态)', () => { + it('哨兵原样 / 空串与 null·undefined → 空串(区分「配了」与「没配」)', () => { + expect(asSecretMasked('__SET__')).toBe('__SET__') + expect(asSecretMasked('')).toBe('') + expect(asSecretMasked(null)).toBe('') + expect(asSecretMasked(undefined)).toBe('') + }) + + it('掩码失效漏出的任意非空值也归哨兵(防原始密钥回传库外)', () => { + expect(asSecretMasked('sk-real-secret')).toBe('__SET__') + expect(asSecretMasked(123)).toBe('') + }) +}) + +describe('normalize 三组(GET 原样组 → 表单 + 密钥态)', () => { + it('email:smtp 完整组 → 表单值 + 密码哨兵;密钥输入恒空', () => { + const state = normalizeEmailCredsGroup({ + smtp: { host: 'smtp.x.com', port: 465, user: 'bot@x.com', password: '__SET__' }, + sender_name: 'HESUPIPE', + notify_emails: ['a@x.com', 'b@x.com'] + }) + expect(state.form).toEqual({ + smtpHost: 'smtp.x.com', + smtpPort: '465', + smtpUser: 'bot@x.com', + smtpPassword: '', + senderName: 'HESUPIPE', + notifyEmails: ['a@x.com', 'b@x.com'] + }) + expect(state.passwordMasked).toBe('__SET__') + }) + + it('email:jiancai 档案形态(无 smtp 键)→ 全空槽 + 未设置密钥态', () => { + const state = normalizeEmailCredsGroup({ + sender_name: 'HESUPIPE', + notify_emails: ['hesu@hesuu.com'] + }) + expect(state.form.smtpHost).toBe('') + expect(state.form.smtpPort).toBe('') + expect(state.form.smtpPassword).toBe('') + expect(state.passwordMasked).toBe('') + }) + + it('email:smtp null / 类型漂移防御', () => { + const state = normalizeEmailCredsGroup({ smtp: null, notify_emails: [1, 'a@x.com'] }) + expect(state.form.smtpHost).toBe('') + expect(state.form.notifyEmails).toEqual(['a@x.com']) + expect(normalizeEmailCredsGroup(undefined).form.notifyEmails).toEqual([]) + }) + + it('push:完整组 → 表单(数值转串)+ api_key 哨兵', () => { + const state = normalizePushCredsGroup({ + enabled: true, + url: 'https://push.example/v1', + api_key: '__SET__', + prefix: 'ai-kefu-jc', + timeout: 5.5, + max_attempts: 3, + retry_backoff_base: 1 + }) + expect(state.form).toEqual({ + enabled: true, + url: 'https://push.example/v1', + apiKey: '', + prefix: 'ai-kefu-jc', + timeout: '5.5', + maxAttempts: '3', + backoffBase: '1' + }) + expect(state.apiKeyMasked).toBe('__SET__') + }) + + it('push:缺省/未启用 → enabled false + 数值空串(null → 空槽)', () => { + const state = normalizePushCredsGroup({ prefix: 'ai-kefu-jc' }) + expect(state.form.enabled).toBe(false) + expect(state.form.timeout).toBe('') + expect(state.form.maxAttempts).toBe('') + expect(state.form.backoffBase).toBe('') + expect(state.form.apiKey).toBe('') + expect(state.apiKeyMasked).toBe('') + }) + + it('ai:缺省组 → 空表单(出厂默认由后端深合并兜底)+ 未设置密钥态', () => { + const state = normalizeAiCredsGroup(null) + expect(state.form).toEqual({ provider: '', apiKey: '', baseUrl: '', model: '' }) + expect(state.apiKeyMasked).toBe('') + const set = normalizeAiCredsGroup({ + provider: 'glm', + api_key: '__SET__', + model: 'glm-4-flash' + }) + expect(set.form.provider).toBe('glm') + expect(set.form.apiKey).toBe('') + expect(set.apiKeyMasked).toBe('__SET__') + }) +}) + +describe('build 三组载荷(表单 → PUT 整组)+ 密钥哨兵往返', () => { + it('email:普通字段 trim、空行剔除、端口转整数', () => { + const payload = buildEmailCredsPayload( + { + smtpHost: ' smtp.x.com ', + smtpPort: ' 465 ', + smtpUser: ' bot@x.com ', + smtpPassword: '', + senderName: ' HESUPIPE ', + notifyEmails: [' a@x.com ', '', ' '] + }, + '' + ) + expect(payload).toEqual({ + smtp: { host: 'smtp.x.com', port: 465, user: 'bot@x.com', password: '' }, + sender_name: 'HESUPIPE', + notify_emails: ['a@x.com'] + }) + }) + + it('email 密钥三态:未设置不动 → 空串;已设置不动 → 哨兵;输入新值 → 新值', () => { + const form = (): EmailCredsForm => ({ + smtpHost: 'smtp.x.com', + smtpPort: '465', + smtpUser: 'bot@x.com', + smtpPassword: '', + senderName: '', + notifyEmails: [] + }) + // 已设置 + 未动 → 哨兵(后端还原库值,库值不变) + expect(buildEmailCredsPayload(form(), '__SET__').smtp).toMatchObject({ + password: '__SET__' + }) + // 未设置 + 未动 → 空串(「清除」意图同为空串 = 显式清空) + expect(buildEmailCredsPayload(form(), '').smtp).toMatchObject({ password: '' }) + // 输入新值 → 新值优先(无论原态) + const typed = { ...form(), smtpPassword: 'new-pw' } + expect(buildEmailCredsPayload(typed, '__SET__').smtp).toMatchObject({ + password: 'new-pw' + }) + }) + + it('push:数值空串 → null(未设置哨兵)、enabled 布尔原样、密钥哨兵往返', () => { + const payload = buildPushCredsPayload( + { + enabled: true, + url: ' https://push.example/v1 ', + apiKey: '', + prefix: 'demo-p', + timeout: '5.5', + maxAttempts: '3', + backoffBase: '' + }, + '__SET__' + ) + expect(payload).toEqual({ + enabled: true, + url: 'https://push.example/v1', + api_key: '__SET__', + prefix: 'demo-p', + timeout: 5.5, + max_attempts: 3, + retry_backoff_base: null + }) + }) + + it('ai:api_key 哨兵/新值/清空三态 + 字段 trim', () => { + const base = { provider: ' glm ', apiKey: '', baseUrl: ' https://x.com ', model: ' glm-4 ' } + expect(buildAiCredsPayload({ ...base }, '__SET__')).toEqual({ + provider: 'glm', + api_key: '__SET__', + base_url: 'https://x.com', + model: 'glm-4' + }) + expect(buildAiCredsPayload({ ...base, apiKey: 'sk-new' }, '').api_key).toBe('sk-new') + expect(buildAiCredsPayload({ ...base }, '').api_key).toBe('') + }) +}) + +describe('validate 前端预检(后端 400 的前置层)', () => { + it('email:端口非整数/越界报错;空串合法(= 平台默认)', () => { + const ok: EmailCredsForm = { + smtpHost: '', + smtpPort: '', + smtpUser: '', + smtpPassword: '', + senderName: '', + notifyEmails: [] + } + expect(validateEmailCredsForm(ok)).toEqual([]) + for (const bad of ['0', '65536', 'abc', '1.5', '-1']) { + expect(validateEmailCredsForm({ ...ok, smtpPort: bad })).toHaveLength(1) + } + expect(validateEmailCredsForm({ ...ok, smtpPort: '465' })).toEqual([]) + expect(validateEmailCredsForm({ ...ok, smtpPort: '65535' })).toEqual([]) + }) + + it('email:通知邮箱非空行须含 @(空行提交时剔除,不报错)', () => { + const base: EmailCredsForm = { + smtpHost: '', + smtpPort: '', + smtpUser: '', + smtpPassword: '', + senderName: '', + notifyEmails: [] + } + expect(validateEmailCredsForm({ ...base, notifyEmails: ['', ' '] })).toEqual([]) + expect( + validateEmailCredsForm({ ...base, notifyEmails: ['a@x.com', 'not-an-email'] }) + ).toHaveLength(1) + }) + + it('push:URL 协议 / 正数 / 1-10 整数边界(与后端 _check_push 同款)', () => { + const ok: PushCredsForm = { + enabled: false, + url: 'https://push.example/v1', + apiKey: '', + prefix: '', + timeout: '10', + maxAttempts: '3', + backoffBase: '1' + } + expect(validatePushCredsForm(ok)).toEqual([]) + // URL 空串合法(= 平台默认);非 http(s) 协议报错 + expect(validatePushCredsForm({ ...ok, url: '' })).toEqual([]) + expect(validatePushCredsForm({ ...ok, url: 'ftp://x' })).toHaveLength(1) + // timeout 正数 + expect(validatePushCredsForm({ ...ok, timeout: '0' })).toHaveLength(1) + expect(validatePushCredsForm({ ...ok, timeout: '-1' })).toHaveLength(1) + expect(validatePushCredsForm({ ...ok, timeout: 'abc' })).toHaveLength(1) + expect(validatePushCredsForm({ ...ok, timeout: '0.5' })).toEqual([]) + // max_attempts 1-10 整数 + expect(validatePushCredsForm({ ...ok, maxAttempts: '0' })).toHaveLength(1) + expect(validatePushCredsForm({ ...ok, maxAttempts: '11' })).toHaveLength(1) + expect(validatePushCredsForm({ ...ok, maxAttempts: '2.5' })).toHaveLength(1) + expect(validatePushCredsForm({ ...ok, maxAttempts: '10' })).toEqual([]) + // backoff 正数;全部空串 = 平台默认,合法 + expect(validatePushCredsForm({ ...ok, backoffBase: '0' })).toHaveLength(1) + expect( + validatePushCredsForm({ ...ok, url: '', timeout: '', maxAttempts: '', backoffBase: '' }) + ).toEqual([]) + }) +}) + +describe('PUSH_PROBE_REASON_LABELS(不可达原因 → 修复提示)', () => { + it('覆盖后端 reason 全部分类(dns/timeout/connect)', () => { + expect(PUSH_PROBE_REASON_LABELS.dns).toContain('域名解析') + expect(PUSH_PROBE_REASON_LABELS.timeout).toContain('超时') + expect(PUSH_PROBE_REASON_LABELS.connect).toContain('拒绝') + }) +}) diff --git a/admin/src/views/config/center/index.vue b/admin/src/views/config/center/index.vue index 1ca4fba..63cd3b9 100644 --- a/admin/src/views/config/center/index.vue +++ b/admin/src/views/config/center/index.vue @@ -1,7 +1,7 @@ @@ -397,6 +401,29 @@ + + + @@ -418,6 +445,7 @@ } from '@/api/config' import PromptsPane from './PromptsPane.vue' import WallPane from './WallPane.vue' + import CredsPane from './CredsPane.vue' defineOptions({ name: 'ConfigCenter' }) @@ -428,7 +456,7 @@ const AVATAR_MAX_BYTES = 2 * 1024 * 1024 const AVATAR_EXTS = ['png', 'jpg', 'jpeg', 'webp'] - // ---- 左侧组导航(Task 7a 品牌 / Task 7b 提示词+词表实装;其余占位) -------------------- + // ---- 左侧组导航(Task 7a 品牌 / Task 7b 提示词+词表 / Task 7c 凭据实装;版本占位) ------ const GROUP_NAV = [ { key: 'brand', label: '品牌' }, @@ -439,7 +467,7 @@ ] as const /** 已实装组(导航徽标) */ - const IMPLEMENTED_GROUPS = new Set(['brand', 'prompts', 'wall']) + const IMPLEMENTED_GROUPS = new Set(['brand', 'prompts', 'wall', 'creds']) type GroupKey = (typeof GROUP_NAV)[number]['key'] diff --git a/admin/src/views/config/center/model.ts b/admin/src/views/config/center/model.ts index db1389f..4ad63ab 100644 --- a/admin/src/views/config/center/model.ts +++ b/admin/src/views/config/center/model.ts @@ -1,8 +1,8 @@ /** - * 配置中心提示词/词表编辑模型(Phase 3 Task 7b) + * 配置中心提示词/词表/凭据编辑模型(Phase 3 Task 7b + 7c) * - * 消费方:config/center 的 PromptsPane.vue / WallPane.vue。纯逻辑无 DOM - * 依赖,vitest 直接覆盖(__tests__/model.test.ts)。 + * 消费方:config/center 的 PromptsPane.vue / WallPane.vue / CredsPane.vue。 + * 纯逻辑无 DOM 依赖,vitest 直接覆盖(__tests__/model.test.ts)。 * * 事实源对齐: * - 提示词四件套键名/字节上限:app/engine/services/ai_service.PROMPTS_KEYS @@ -14,8 +14,15 @@ * comment?}(flags 仅支持 'i');PUT wall 整组提交,未结构化编辑的 * 顶层键(含 "_" 前缀元信息键)原样透传; * - 语言池必填 en/zh(wall_schema._REQUIRED_POOLS),reminders 六类 / - * cc_templates 四组的变体键为封闭集合(未知键编译期显性拒绝)。 + * cc_templates 四组的变体键为封闭集合(未知键编译期显性拒绝); + * - 凭据三组(Task 7c):DEFAULT_CONFIG 的 email/push/ai 组键 + + * config_validation 数值边界(push.timeout/backoff 正数、max_attempts + * 1-10 整数、notify_emails 项含 @)+ config_views 空槽回退语义 + * (空串/None = 未设置 → 回退 env,表单提示「留空 = 平台默认」)。 + * 密钥哨兵交互见 src/api/mask.ts(GET '__SET__' → 空输入 + 占位符; + * 不动保存回传哨兵保持库值;「清除」= 提交空串)。 */ +import { MASKED_SENTINEL, secretToSubmit } from '@/api/mask' // ---- 提示词四件套 ----------------------------------------------------------------- /** 四件套键名(ai_service.PROMPTS_KEYS 镜像;prompts 是整组 PUT 的四键 dict) */ @@ -423,3 +430,248 @@ export const DEFAULT_TOUCHSTONE_SENTENCES = [ export function touchstoneSentences(sentences: string[]): string[] { return sentences.map((s) => s.trim()).filter(Boolean) } + +// ---- 凭据三组(email / push / ai,Phase 3 Task 7c) -------------------------------- + +/** 邮件凭据表单(数值/密钥输入均为字符串形态——空串 = 未设置回退 env) */ +export interface EmailCredsForm { + smtpHost: string + smtpPort: string + smtpUser: string + /** 密钥输入:初始恒空,「已设置」态由占位符表达(见 mask.ts) */ + smtpPassword: string + senderName: string + notifyEmails: string[] +} + +/** 推送凭据表单(timeout/maxAttempts/backoffBase:'' = null = 平台默认) */ +export interface PushCredsForm { + enabled: boolean + url: string + apiKey: string + prefix: string + timeout: string + maxAttempts: string + backoffBase: string +} + +/** AI 凭据表单 */ +export interface AiCredsForm { + provider: string + apiKey: string + baseUrl: string + model: string +} + +/** GET 组 → 表单 + 密钥掩码状态(掩码值驱动占位符与 secretToSubmit) */ +export interface EmailCredsState { + form: EmailCredsForm + passwordMasked: string +} + +export interface PushCredsState { + form: PushCredsForm + apiKeyMasked: string +} + +export interface AiCredsState { + apiKeyMasked: string + form: AiCredsForm +} + +/** + * 任意非空字符串 → 掩码哨兵。比 isMaskedSentinel 更保守:即使后端掩码 + * 失效漏出原始密钥(理论不可达),也按「已设置」处理——不动保存回传 + * 哨兵由后端还原库值,绝不把原始值再提交回去;空串/null/undefined → '' + * (未设置)。 + */ +export function asSecretMasked(value: unknown): string { + return typeof value === 'string' && value !== '' ? MASKED_SENTINEL : '' +} + +/** 防御读取:数值 → 表单串(非有限数值/缺省 → '') */ +function numberToForm(value: unknown): string { + return typeof value === 'number' && Number.isFinite(value) ? String(value) : '' +} + +/** 防御读取:字符串数组(非字符串项剔除) */ +function strList(value: unknown): string[] { + return Array.isArray(value) + ? value.filter((v): v is string => typeof v === 'string') + : [] +} + +/** 表单串 → 整数 | null(空串 = 未设置;非整数由 validate 前置拦截) */ +function intOrNull(value: string): number | null { + const n = Number(value.trim()) + return value.trim() !== '' && Number.isInteger(n) ? n : null +} + +/** 表单串 → 数值 | null(空串 = 未设置;非数值由 validate 前置拦截) */ +function numOrNull(value: string): number | null { + const n = Number(value.trim()) + return value.trim() !== '' && Number.isFinite(n) ? n : null +} + +/** GET 的 email 组 → 邮件表单(smtp 缺省/null → 全空槽,回退 env) */ +export function normalizeEmailCredsGroup( + value: Record | null | undefined +): EmailCredsState { + const source = value ?? {} + const smtp = asRecord(source.smtp) ?? {} + return { + form: { + smtpHost: asStr(smtp.host), + smtpPort: numberToForm(smtp.port), + smtpUser: asStr(smtp.user), + smtpPassword: '', + senderName: asStr(source.sender_name), + notifyEmails: strList(source.notify_emails) + }, + passwordMasked: asSecretMasked(smtp.password) + } +} + +/** GET 的 push 组 → 推送表单 */ +export function normalizePushCredsGroup( + value: Record | null | undefined +): PushCredsState { + const source = value ?? {} + return { + form: { + enabled: source.enabled === true, + url: asStr(source.url), + apiKey: '', + prefix: asStr(source.prefix), + timeout: numberToForm(source.timeout), + maxAttempts: numberToForm(source.max_attempts), + backoffBase: numberToForm(source.retry_backoff_base) + }, + apiKeyMasked: asSecretMasked(source.api_key) + } +} + +/** GET 的 ai 组 → AI 表单 */ +export function normalizeAiCredsGroup( + value: Record | null | undefined +): AiCredsState { + const source = value ?? {} + return { + form: { + provider: asStr(source.provider), + apiKey: '', + baseUrl: asStr(source.base_url), + model: asStr(source.model) + }, + apiKeyMasked: asSecretMasked(source.api_key) + } +} + +/** + * 邮件表单 → PUT config/email 整组载荷。密钥走 secretToSubmit(未动 → + * 哨兵保持库值;「清除」按钮已把 maskedState 置 '',提交空串 = 显式清空)。 + * 数值/邮箱格式由 validateEmailCredsForm 前置把关(后端 400 二层兜底)。 + */ +export function buildEmailCredsPayload( + form: EmailCredsForm, + passwordMasked: string +): Record { + return { + smtp: { + host: form.smtpHost.trim(), + port: intOrNull(form.smtpPort), + user: form.smtpUser.trim(), + password: secretToSubmit(form.smtpPassword, passwordMasked) + }, + sender_name: form.senderName.trim(), + notify_emails: form.notifyEmails + .map((e) => e.trim()) + .filter((e) => e !== '') + } +} + +/** 推送表单 → PUT config/push 整组载荷(prefix 冲突 400 由后端跨租户查重) */ +export function buildPushCredsPayload( + form: PushCredsForm, + apiKeyMasked: string +): Record { + return { + enabled: form.enabled, + url: form.url.trim(), + api_key: secretToSubmit(form.apiKey, apiKeyMasked), + prefix: form.prefix.trim(), + timeout: numOrNull(form.timeout), + max_attempts: intOrNull(form.maxAttempts), + retry_backoff_base: numOrNull(form.backoffBase) + } +} + +/** AI 表单 → PUT config/ai 整组载荷 */ +export function buildAiCredsPayload( + form: AiCredsForm, + apiKeyMasked: string +): Record { + return { + provider: form.provider.trim(), + api_key: secretToSubmit(form.apiKey, apiKeyMasked), + base_url: form.baseUrl.trim(), + model: form.model.trim() + } +} + +/** 邮件表单预检(空槽合法——回退 env;只查非空行的格式) */ +export function validateEmailCredsForm(form: EmailCredsForm): string[] { + const errors: string[] = [] + const port = form.smtpPort.trim() + if (port !== '') { + const n = Number(port) + if (!Number.isInteger(n) || n < 1 || n > 65535) { + errors.push('SMTP 端口:须为 1-65535 的整数(留空 = 平台默认)') + } + } + form.notifyEmails.forEach((row, i) => { + const t = row.trim() + if (t !== '' && !t.includes('@')) { + errors.push(`通知邮箱第 ${i + 1} 行:须为含 @ 的邮箱地址`) + } + }) + return errors +} + +/** 推送表单预检(与后端 _check_push 的数值边界同款:正数 / 1-10 整数) */ +export function validatePushCredsForm(form: PushCredsForm): string[] { + const errors: string[] = [] + const url = form.url.trim() + if (url !== '' && !/^https?:\/\//.test(url)) { + errors.push('推送 URL:须以 http:// 或 https:// 开头') + } + const timeout = form.timeout.trim() + if (timeout !== '') { + const n = Number(timeout) + if (!Number.isFinite(n) || n <= 0) { + errors.push('推送超时(秒):须为正数(留空 = 平台默认)') + } + } + const attempts = form.maxAttempts.trim() + if (attempts !== '') { + const n = Number(attempts) + if (!Number.isInteger(n) || n < 1 || n > 10) { + errors.push('最大尝试次数:须为 1-10 的整数(留空 = 平台默认)') + } + } + const backoff = form.backoffBase.trim() + if (backoff !== '') { + const n = Number(backoff) + if (!Number.isFinite(n) || n <= 0) { + errors.push('重试退避基数(秒):须为正数(留空 = 平台默认)') + } + } + return errors +} + +/** 推送探测不可达原因 → 修复提示文案(后端 reason 分类的前端展示层) */ +export const PUSH_PROBE_REASON_LABELS: Record = { + dns: '域名解析失败(检查 URL 拼写)', + timeout: '连接超时', + connect: '连接被拒绝或网络不可达' +} diff --git a/app/engine/services/email_service.py b/app/engine/services/email_service.py index e0bbf1a..0d96a57 100644 --- a/app/engine/services/email_service.py +++ b/app/engine/services/email_service.py @@ -1,5 +1,6 @@ import html import logging +from datetime import datetime, timedelta, timezone from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -58,16 +59,23 @@ def _cc_views(contacts: dict, cc_info: dict | None) -> tuple[dict, dict]: return body, subj +async def _smtp_send(msg: MIMEMultipart, cfg: dict) -> None: + """底层连接发送(Phase 3 Task 7c 从 _send 抽出): 直调 aiosmtplib, + 失败抛原始异常——需要错误串的调用方(配置中心测试邮件 send_test_email) + 复用同一连接逻辑;_send 在此之上包"吞异常返 bool"契约,行为不变。""" + await aiosmtplib.send( + msg, hostname=cfg["host"], port=cfg["port"], + username=cfg["user"], password=cfg["password"], + use_tls=True, + ) + + async def _send(msg: MIMEMultipart, cfg: dict) -> bool: if not all([cfg["host"], cfg["user"], cfg["notify_emails"]]): logger.warning("SMTP not configured, skipping email") return False try: - await aiosmtplib.send( - msg, hostname=cfg["host"], port=cfg["port"], - username=cfg["user"], password=cfg["password"], - use_tls=True, - ) + await _smtp_send(msg, cfg) logger.info("Lead email sent to %s", cfg["notify_emails"]) return True except Exception: @@ -170,3 +178,35 @@ async def send_correction_email( f"{_contact_rows(body_contacts)}
" f"", "html", "utf-8")) return await _send(msg, cfg) + + +async def send_test_email( + to: str, tenant_name: str = "", + config_data: dict | None = None, +) -> tuple[bool, str | None]: + """配置中心测试邮件(Phase 3 Task 7c): 按当前生效邮件配置真发一封到 + 指定收件人——主题/正文钉契约("[all-kefu] SMTP 测试邮件"+租户名+时间, + 时间与 lead_push_service 同款 +08:00 口径)。 + + 与 _send 的吞异常 bool 契约解耦: 成败与错误串如实返回(探测端点直传 + 前端,凭据对不对让收件箱与错误串说话);连接逻辑复用 _smtp_send。 + "SMTP 未配置"的 400 判定在端点层(本函数假定 cfg 已可用)。""" + cfg = resolve_email_settings(config_data) + now = datetime.now(timezone(timedelta(hours=8))) + msg = MIMEMultipart("alternative") + msg["Subject"] = "[all-kefu] SMTP 测试邮件" + msg["From"] = f"【{cfg['sender_name']}】 <{cfg['user']}>" + msg["To"] = to + msg.attach(MIMEText( + f"" + f"

这是一封 SMTP 配置测试邮件——收到本邮件说明该租户的邮件配置" + f"可以正常发信。

" + f"

租户:{_esc(tenant_name)}

" + f"

时间:{now.strftime('%Y-%m-%d %H:%M:%S')}(UTC+8)

" + f"", "html", "utf-8")) + try: + await _smtp_send(msg, cfg) + return True, None + except Exception as exc: + logger.exception("Test email to %s failed", to) + return False, repr(exc) diff --git a/app/platform/routes/config_routes.py b/app/platform/routes/config_routes.py index cac7ccd..a02aa43 100644 --- a/app/platform/routes/config_routes.py +++ b/app/platform/routes/config_routes.py @@ -19,6 +19,10 @@ 缺省用当前墙——逐句调 boundary_filter 纯函数给判定,**不落库不 bump 版本** (reset/编辑器的预览入口)。判定优先级与 conversation.py 执行侧一致: 价格墙 → 零号码墙 → 再索要墙(仅 has_contact); +- POST /email/test 与 /push/test(凭据测试,Phase 3 Task 7c):分别用 + resolve_email_settings/resolve_push_settings 的当前配置真发测试邮件 / + 探测推送连通性——**push 探测绝不携带线索数据**(空 messages 批量), + 两端点不落库不 bump 版本,成败如实返回(400 只用于"未配置"预检); - GET/PUT /api/platform/settings:PlatformSetting CRUD,仅超管。本阶段只落 存储与接口(value 以 JSON Text 直存,读回还原类型);resolver 的平台默认 回退链接入归 Phase 3。 @@ -36,12 +40,15 @@ email.smtp.password 及平台设置的密钥名值,所有携带 config/value 的 """ import copy import json +import socket +import httpx from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.engine.services import boundary_filter +from app.engine.services.email_service import send_test_email from app.engine.wall_schema import WallConfigError, compile_wall from app.platform.auth import UserContext, require_super from app.platform.config_validation import ( @@ -49,6 +56,10 @@ from app.platform.config_validation import ( ValidationContext, validate_group, ) +from app.platform.config_views import ( + resolve_email_settings, + resolve_push_settings, +) from app.platform.db import ( PlatformSetting, Tenant, @@ -488,6 +499,109 @@ async def test_wall(slug: str, request: Request, user=Depends(_tenant_guard)): return {"slug": slug, "wall_source": wall_source, "results": results} +# ---- 凭据测试端点(email/push test,Phase 3 Task 7c) ------------------------------- + +_EMAIL_TEST_KEYS = frozenset({"to"}) + + +@router.post("/{slug}/email/test") +async def test_email(slug: str, request: Request, + user=Depends(_tenant_guard)): + """SMTP 测试邮件: 用当前生效邮件配置(resolve_email_settings,租户组 + 优先/空槽回退 env SMTP_*)真发一封到 body.to——主题/正文契约见 + email_service.send_test_email。SMTP 未配置(回退后 host/user 仍空) + → 400;发送失败不 5xx,{sent: false, error} 如实返回。不落库不 bump + 版本;密钥绝不进响应。""" + body = await _json_body(request) + unknown = set(body) - _EMAIL_TEST_KEYS + if unknown: + raise HTTPException( + status_code=400, + detail=f"未知字段: {', '.join(sorted(unknown))}(合法: to)") + to = body.get("to") + if not isinstance(to, str) or "@" not in to.strip(): + raise HTTPException( + status_code=400, + detail=f"to: 须为含 @ 的收件人邮箱(实际 {to!r})") + + engine = request.app.state.platform_engine + async with AsyncSession(engine, expire_on_commit=False) as session: + tenant = await _get_tenant_or_404(session, slug) + _, snapshot = await _load_current(session, tenant.id, slug) + cfg = resolve_email_settings(snapshot) + if not cfg["host"] or not cfg["user"]: + raise HTTPException( + status_code=400, + detail="SMTP 未配置(服务器/账号在租户配置与环境回退后均为空)" + "——请先在「配置中心 → 凭据」保存邮件 SMTP") + sent, error = await send_test_email(to.strip(), tenant.name, snapshot) + return {"slug": slug, "sent": sent, "error": error} + + +def _transport_reason(exc: Exception) -> str: + """传输异常 → 不可达原因分类(前端三态展示): dns(域名解析失败)/ + timeout(TimeoutException 族)/connect(拒绝·不可达·TLS 等其余)。 + + dns 判定双通道: 沿 __cause__/__context__ 链找 gaierror(httpx 把解析 + 错误包在 ConnectError 里),并对链上异常做文案匹配——Windows 实测 + (冒烟 Task 7c)httpx/anyio 会把 errno 11001 包成裸 ConnectError, + cause 链不带 gaierror,只能从 "[Errno 11001] getaddrinfo failed" + 文案识别;分类仅供修复提示,错误原文始终随响应返回兜底。""" + node: BaseException | None = exc + while node is not None: + if isinstance(node, socket.gaierror): + return "dns" + text = str(node).lower() + if "getaddrinfo" in text or "name or service not known" in text: + return "dns" + node = node.__cause__ or node.__context__ + if isinstance(exc, httpx.TimeoutException): + return "timeout" + return "connect" + + +async def _probe_push(cfg: dict) -> dict: + """单次连通性探测(无重试): 与 lead_push_service._do_post 同款连接形态 + (timeout/follow_redirects/X-Api-Key 头),body 恒为空 messages 列表—— + **绝不携带任何线索数据**(UniService 是批量结构,空批量对方案应为 + 无操作;契约文档不在库内,这是可用的最保守预检形态)。""" + try: + async with httpx.AsyncClient(timeout=cfg["timeout"], + follow_redirects=True) as client: + resp = await client.post( + cfg["url"], json={"messages": []}, + headers={"X-Api-Key": cfg["api_key"]}) + except httpx.HTTPError as exc: + return {"reachable": False, "auth_ok": None, "status": None, + "reason": _transport_reason(exc), + "error": str(exc) or repr(exc)} + auth_ok = (True if resp.status_code < 300 + else False if resp.status_code in (401, 403) + else None) + return {"reachable": True, "auth_ok": auth_ok, + "status": resp.status_code, "reason": None, "error": None} + + +@router.post("/{slug}/push/test") +async def test_push(slug: str, request: Request, + user=Depends(_tenant_guard)): + """推送连通性探测: 只验 URL 可达 + 鉴权通过,不发真数据(空 messages + 批量,见 _probe_push)。URL 回退后仍空 → 400;其余如实返回——不可达 + (含 dns/timeout/connect 原因分类)/可达但鉴权失败(401/403)/正常(2xx); + 其余状态码可达但鉴权不定(auth_ok=null)。不落库不 bump 版本。""" + engine = request.app.state.platform_engine + async with AsyncSession(engine, expire_on_commit=False) as session: + tenant = await _get_tenant_or_404(session, slug) + _, snapshot = await _load_current(session, tenant.id, slug) + cfg = resolve_push_settings(snapshot) + if not cfg["url"]: + raise HTTPException( + status_code=400, + detail="推送 URL 未配置(租户配置与环境回退后均为空)" + "——请先在「配置中心 → 凭据」保存推送 URL") + return {"slug": slug, **await _probe_push(cfg)} + + # ---- 平台设置(仅超管;resolver 回退链接入归 Phase 3) ----------------------------- diff --git a/tests/test_email_test_endpoint.py b/tests/test_email_test_endpoint.py new file mode 100644 index 0000000..f709dd5 --- /dev/null +++ b/tests/test_email_test_endpoint.py @@ -0,0 +1,177 @@ +"""POST /api/platform/tenants/{slug}/email/test(Phase 3 Task 7c)——SMTP 测试邮件。 + +端点契约(计划 Task 7c): +- 用当前生效配置(resolve_email_settings:租户 email 组优先,空槽回退 env + SMTP_*)**真发一封**:主题 "[all-kefu] SMTP 测试邮件",正文含租户名与 + 时间,From = sender_name/user,To = body.to; +- SMTP 未配置(回退后 host/user 仍空)→ 400; +- 发送成败如实返回 {sent, error}(失败不 5xx——凭据对不对让收件箱与 + 错误串说话);密钥绝不进响应。 + +测试纪律: monkeypatch email_service.aiosmtplib.send 捕获报文与连接参数, +绝不真发(同 test_email_rendering 的 _capture_send 姿势,但打在 +aiosmtplib.send 一层——端点经 send_test_email 走完整组包路径)。 +""" +from datetime import datetime, timedelta, timezone + +from app.engine.config import settings +from app.engine.services import email_service +from tests.test_tenant_admin_api import _auth, _bearer, _create, _tenant_admin_token + +CONFIG = "/api/platform/tenants/{slug}/config" +EMAIL_TEST = "/api/platform/tenants/{slug}/email/test" + + +def _capture_smtp(monkeypatch): + """aiosmtplib.send 桩: 捕获报文与连接参数,返回 True 语义(发送成功)。""" + captured = {} + + async def fake_send(msg, **kwargs): + captured["msg"] = msg + captured["kwargs"] = kwargs + return msg + + monkeypatch.setattr(email_service.aiosmtplib, "send", fake_send) + return captured + + +def _failing_smtp(monkeypatch, message: str): + async def failing_send(msg, **kwargs): + raise RuntimeError(message) + + monkeypatch.setattr(email_service.aiosmtplib, "send", failing_send) + + +def _clear_smtp_env(monkeypatch): + """env SMTP_* 清空(隔离本机 .env 联调值——同 test_push_settings_defaults + 的隔离教训);"回退后仍空"的 400 分支才可控。""" + for key in ("SMTP_HOST", "SMTP_USER", "SMTP_PASS", "NOTIFY_EMAIL"): + monkeypatch.setattr(settings, key, "") + monkeypatch.setattr(settings, "SMTP_PORT", 465) + + +def _put_email_group(client, slug, payload): + return client.put(f"{CONFIG.format(slug=slug)}/email", + headers=_auth(client), json=payload) + + +def _post_test(client, slug, body, headers=None): + return client.post(EMAIL_TEST.format(slug=slug), + headers=headers or _auth(client), json=body) + + +SMTP_GROUP = { + "smtp": {"host": "t.smtp.example.com", "port": 587, + "user": "bot@demo.com", "password": "s3cret-pw"}, + "sender_name": "Demo 发件人", + "notify_emails": ["notify@demo.com"], +} + + +def test_email_test_sends_with_tenant_config(super_client, monkeypatch): + """已配置 SMTP: 真发路径走通——主题/收件人/发件人/正文(租户名+时间) + 与连接参数(租户四字段)逐项钉死;密钥不进响应。""" + _create(super_client, slug="demo", name="Demo 建材") + assert _put_email_group(super_client, "demo", SMTP_GROUP).status_code == 200 + captured = _capture_smtp(monkeypatch) + + r = _post_test(super_client, "demo", {"to": "checker@example.com"}) + assert r.status_code == 200, r.text + assert r.json() == {"slug": "demo", "sent": True, "error": None} + + msg = captured["msg"] + assert str(msg["Subject"]) == "[all-kefu] SMTP 测试邮件" + assert str(msg["To"]) == "checker@example.com" + assert str(msg["From"]) == "【Demo 发件人】 " + body = msg.get_payload(0).get_payload(decode=True).decode("utf-8") + assert "Demo 建材" in body # 正文含租户名 + cst_today = datetime.now(timezone(timedelta(hours=8))).strftime("%Y-%m-%d") + assert cst_today in body and "UTC+8" in body # 正文含时间(与推送同款 CST 口径) + # 连接参数 = 租户配置(与 email_service._send 同一连接逻辑) + assert captured["kwargs"] == { + "hostname": "t.smtp.example.com", "port": 587, + "username": "bot@demo.com", "password": "s3cret-pw", "use_tls": True} + assert "s3cret-pw" not in r.text # 密钥不进响应 + + +def test_email_test_env_fallback(super_client, monkeypatch): + """租户 smtp 未配置 → 回退 env SMTP_*(部署级平台默认同样可发)。""" + _create(super_client, slug="demo") # jiancai 档案: email 组无 smtp 键 + for key, value in (("SMTP_HOST", "env.smtp.example.com"), + ("SMTP_PORT", 465), ("SMTP_USER", "env-bot@x.com"), + ("SMTP_PASS", "env-pass")): + monkeypatch.setattr(settings, key, value) + captured = _capture_smtp(monkeypatch) + + r = _post_test(super_client, "demo", {"to": "checker@example.com"}) + assert r.status_code == 200, r.text + assert r.json()["sent"] is True + assert captured["kwargs"]["hostname"] == "env.smtp.example.com" + assert captured["kwargs"]["password"] == "env-pass" + # sender_name 无租户覆盖 → 平台默认;user 走 env + assert str(captured["msg"]["From"]) == "【HESUPIPE】 " + + +def test_email_test_smtp_unconfigured_400(super_client, monkeypatch): + """SMTP 未配置(租户组与 env 回退后仍空)→ 400 提示(不试探连接)。""" + _clear_smtp_env(monkeypatch) + _create(super_client, slug="demo") + + r = _post_test(super_client, "demo", {"to": "checker@example.com"}) + assert r.status_code == 400 + detail = r.json()["detail"] + assert "SMTP" in detail and "未配置" in detail + + +def test_email_test_send_failure_reports_error(super_client, monkeypatch): + """发送失败不 5xx: {sent: false, error 带原始错误串}如实返回。""" + _create(super_client, slug="demo") + assert _put_email_group(super_client, "demo", SMTP_GROUP).status_code == 200 + _failing_smtp(monkeypatch, "smtp auth rejected") + + r = _post_test(super_client, "demo", {"to": "checker@example.com"}) + assert r.status_code == 200, r.text + body = r.json() + assert body["sent"] is False + assert "smtp auth rejected" in body["error"] + + +def test_email_test_validates_to(super_client, monkeypatch): + """to 必填且须为含 @ 的字符串;未知字段 400(错误契约带字段路径)。""" + _clear_smtp_env(monkeypatch) + _create(super_client, slug="demo") + for bad in (None, 123, "", "no-at-sign", ["a@b.com"], {"to": "a@b.com"}): + r = _post_test(super_client, "demo", {"to": bad}) + assert r.status_code == 400, f"{bad!r}: {r.text}" + assert "to" in r.json()["detail"] + # 缺 to / 未知字段 + assert _post_test(super_client, "demo", {}).status_code == 400 + r = _post_test(super_client, "demo", {"to": "a@b.com", "extra": 1}) + assert r.status_code == 400 + assert "extra" in r.json()["detail"] + + +def test_email_test_unknown_tenant_404(super_client, monkeypatch): + _clear_smtp_env(monkeypatch) + r = _post_test(super_client, "ghost", {"to": "a@b.com"}) + assert r.status_code == 404 + + +def test_email_test_auth_matrix(super_client, monkeypatch): + """require_tenant(slug): 本租户管理员放行(400 是业务分支,鉴权先于 + 业务);跨租户 403;未认证 401。""" + _clear_smtp_env(monkeypatch) + _create(super_client, slug="jc2", admin="jc2-admin", password="jc2-pass-1") + _create(super_client, slug="au2", profile="auto", + admin="au2-admin", password="au2-pass-1") + token = _tenant_admin_token(super_client, "jc2-admin", "jc2-pass-1") + + # 本租户管理员: 鉴权过,落在"未配置"业务分支 400(而非 403) + r = _post_test(super_client, "jc2", {"to": "a@b.com"}, headers=_bearer(token)) + assert r.status_code == 400 + # 别的租户 → 403 + r = _post_test(super_client, "au2", {"to": "a@b.com"}, headers=_bearer(token)) + assert r.status_code == 403 + # 未认证 → 401 + r = super_client.post(EMAIL_TEST.format(slug="jc2"), json={"to": "a@b.com"}) + assert r.status_code == 401 diff --git a/tests/test_push_test_endpoint.py b/tests/test_push_test_endpoint.py new file mode 100644 index 0000000..2019a51 --- /dev/null +++ b/tests/test_push_test_endpoint.py @@ -0,0 +1,243 @@ +"""POST /api/platform/tenants/{slug}/push/test(Phase 3 Task 7c)——推送连通性探测。 + +端点红线: **不发任何线索数据**——探测 body = {"messages": []}(空批量, +与 lead_push_service._do_post 同端点同头;UniService 是批量结构,空列表 +对方案应为无操作)。测试断言钉死这一形态。 + +响应契约: {reachable, auth_ok(可空), status, reason, error}—— +- 传输层异常 → reachable=false,reason 分类 dns/timeout/connect; +- HTTP 有响应 → reachable=true + status;2xx → auth_ok=true, + 401/403 → false,其余(4xx/5xx)→ null(到达但状态异常,鉴权不定); +- 单次探测不重试(与 _run_push 的重试矩阵无关)。 + +测试纪律: monkeypatch config_routes.httpx.AsyncClient(假客户端),绝不真连。 +""" +import socket + +import httpx + +from app.engine.config import settings +from app.platform.routes import config_routes +from tests.test_tenant_admin_api import _auth, _bearer, _create, _tenant_admin_token + +CONFIG = "/api/platform/tenants/{slug}/config" +PUSH_TEST = "/api/platform/tenants/{slug}/push/test" + +PUSH_GROUP = { + "enabled": True, + "url": "https://push.example/open/v1/inquiry/push", + "api_key": "k-t1", + "prefix": "demo-p", + "timeout": 5.5, + "max_attempts": 3, + "retry_backoff_base": 1, +} + + +def _install_fake_client(monkeypatch, handler): + """config_routes.httpx.AsyncClient 桩: handler(call) → httpx.Response, + 抛异常照抛。返回调用记录(init 构造参数 + 每次 post 的 url/json/headers)。""" + calls = [] + + class FakeClient: + def __init__(self, **kwargs): + calls.append({"init": kwargs}) + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc_info): + return False + + async def post(self, url, json=None, headers=None): + call = {"url": url, "json": json, "headers": dict(headers or {})} + calls.append(call) + return handler(call) + + monkeypatch.setattr(config_routes.httpx, "AsyncClient", FakeClient) + return calls + + +def _ok(call): + return httpx.Response(200, json={"success": True, "data": { + "accepted": 0, "duplicated": 0, "failed": []}}) + + +def _put_push_group(client, slug, payload): + return client.put(f"{CONFIG.format(slug=slug)}/push", + headers=_auth(client), json=payload) + + +def _post_test(client, slug, headers=None): + return client.post(PUSH_TEST.format(slug=slug), headers=headers or _auth(client)) + + +def _post_calls(calls): + return [c for c in calls if "url" in c] + + +def test_push_test_probe_shape_and_ok(super_client, monkeypatch): + """正常路径: 200 → reachable+auth_ok;请求形态逐项钉死——同 _do_post 的 + 端点/X-Api-Key 头/超时/follow_redirects,body 恒为空 messages(红线)。""" + _create(super_client, slug="demo") + assert _put_push_group(super_client, "demo", PUSH_GROUP).status_code == 200 + calls = _install_fake_client(monkeypatch, _ok) + + r = _post_test(super_client, "demo") + assert r.status_code == 200, r.text + assert r.json() == {"slug": "demo", "reachable": True, "auth_ok": True, + "status": 200, "reason": None, "error": None} + assert calls[0]["init"] == {"timeout": 5.5, "follow_redirects": True} + post = _post_calls(calls)[0] + assert post["url"] == "https://push.example/open/v1/inquiry/push" + assert post["json"] == {"messages": []} # 红线: 空批量,零线索数据 + assert post["headers"] == {"X-Api-Key": "k-t1"} # 与 _do_post 同头 + + +def test_push_test_auth_rejected(super_client, monkeypatch): + """可达但鉴权失败: 401/403 → auth_ok=false(前端三态展示的第二态)。""" + _create(super_client, slug="demo") + assert _put_push_group(super_client, "demo", PUSH_GROUP).status_code == 200 + for status in (401, 403): + _install_fake_client( + monkeypatch, lambda call, s=status: httpx.Response(s)) + r = _post_test(super_client, "demo") + assert r.status_code == 200 + body = r.json() + assert body["reachable"] is True and body["status"] == status + assert body["auth_ok"] is False + + +def test_push_test_5xx_reachable_auth_unknown_single_shot(super_client, monkeypatch): + """5xx: 可达但状态异常(auth_ok=null);探测单次发起,不重试。""" + _create(super_client, slug="demo") + assert _put_push_group(super_client, "demo", PUSH_GROUP).status_code == 200 + calls = _install_fake_client( + monkeypatch, lambda call: httpx.Response(502)) + + r = _post_test(super_client, "demo") + assert r.status_code == 200 + body = r.json() + assert body["reachable"] is True and body["status"] == 502 + assert body["auth_ok"] is None + assert len(_post_calls(calls)) == 1 # 无重试矩阵 + + +def test_push_test_unreachable_dns(super_client, monkeypatch): + """DNS 失败(异常 cause 链含 gaierror)→ reachable=false + reason=dns。""" + _create(super_client, slug="demo") + assert _put_push_group(super_client, "demo", PUSH_GROUP).status_code == 200 + + def dns_fail(call): + raise httpx.ConnectError("Failed to resolve.") from socket.gaierror( + -2, "Name or service not known") + + _install_fake_client(monkeypatch, dns_fail) + r = _post_test(super_client, "demo") + assert r.status_code == 200 + body = r.json() + assert body["reachable"] is False and body["auth_ok"] is None + assert body["status"] is None + assert body["reason"] == "dns" and body["error"] + + +def test_push_test_unreachable_connect(super_client, monkeypatch): + """连接拒绝/不可达(无 gaierror cause 的 ConnectError)→ reason=connect。""" + _create(super_client, slug="demo") + assert _put_push_group(super_client, "demo", PUSH_GROUP).status_code == 200 + + def refused(call): + raise httpx.ConnectError("Connection refused") + + _install_fake_client(monkeypatch, refused) + r = _post_test(super_client, "demo") + body = r.json() + assert body["reachable"] is False + assert body["reason"] == "connect" + + +def test_push_test_unreachable_dns_windows_text(super_client, monkeypatch): + """Windows 实测形态(冒烟 Task 7c): getaddrinfo 失败被 httpx/anyio 包成 + 裸 ConnectError("[Errno 11001] getaddrinfo failed"),cause 链不带 + gaierror——文案匹配兜底,仍须归 dns 而非 connect。""" + _create(super_client, slug="demo") + assert _put_push_group(super_client, "demo", PUSH_GROUP).status_code == 200 + + def win_dns_fail(call): + raise httpx.ConnectError("[Errno 11001] getaddrinfo failed") + + _install_fake_client(monkeypatch, win_dns_fail) + r = _post_test(super_client, "demo") + body = r.json() + assert body["reachable"] is False + assert body["reason"] == "dns" + assert "getaddrinfo" in body["error"] + + +def test_push_test_unreachable_timeout(super_client, monkeypatch): + """连接超时(TimeoutException 族)→ reason=timeout。""" + _create(super_client, slug="demo") + assert _put_push_group(super_client, "demo", PUSH_GROUP).status_code == 200 + + def timed_out(call): + raise httpx.ConnectTimeout("timed out") + + _install_fake_client(monkeypatch, timed_out) + r = _post_test(super_client, "demo") + body = r.json() + assert body["reachable"] is False + assert body["reason"] == "timeout" + + +def test_push_test_url_unconfigured_400(super_client, monkeypatch): + """URL 回退后仍空(租户 url 空槽 + env PUSH_API_URL 清空)→ 400。""" + monkeypatch.setattr(settings, "PUSH_API_URL", "") + _create(super_client, slug="demo") + assert _put_push_group(super_client, "demo", {"prefix": "demo-p"}).status_code == 200 + + r = _post_test(super_client, "demo") + assert r.status_code == 400 + detail = r.json()["detail"] + assert "URL" in detail and "未配置" in detail + + +def test_push_test_env_fallback(super_client, monkeypatch): + """租户 push 组空槽 → env PUSH_* 回退(探测用的就是部署级平台默认)。""" + monkeypatch.setattr(settings, "PUSH_API_URL", "https://env-push.example/v1") + monkeypatch.setattr(settings, "PUSH_API_KEY", "env-key") + monkeypatch.setattr(settings, "PUSH_TIMEOUT_SECONDS", 8.0) + _create(super_client, slug="demo") + assert _put_push_group(super_client, "demo", {"prefix": "demo-p"}).status_code == 200 + calls = _install_fake_client(monkeypatch, _ok) + + r = _post_test(super_client, "demo") + assert r.status_code == 200 + assert r.json()["reachable"] is True + assert calls[0]["init"]["timeout"] == 8.0 + post = _post_calls(calls)[0] + assert post["url"] == "https://env-push.example/v1" + assert post["headers"] == {"X-Api-Key": "env-key"} + + +def test_push_test_unknown_tenant_404(super_client, monkeypatch): + _install_fake_client(monkeypatch, _ok) + r = _post_test(super_client, "ghost") + assert r.status_code == 404 + + +def test_push_test_auth_matrix(super_client, monkeypatch): + """require_tenant(slug): 本租户管理员放行;跨租户 403;未认证 401。""" + _create(super_client, slug="jc2", admin="jc2-admin", password="jc2-pass-1") + _create(super_client, slug="au2", profile="auto", + admin="au2-admin", password="au2-pass-1") + assert _put_push_group(super_client, "jc2", PUSH_GROUP).status_code == 200 + token = _tenant_admin_token(super_client, "jc2-admin", "jc2-pass-1") + _install_fake_client(monkeypatch, _ok) + + # 本租户管理员放行 + assert _post_test(super_client, "jc2", headers=_bearer(token)).status_code == 200 + # 别的租户 → 403 + assert _post_test(super_client, "au2", headers=_bearer(token)).status_code == 403 + # 未认证 → 401 + r = super_client.post(PUSH_TEST.format(slug="jc2")) + assert r.status_code == 401 From 481243b958493908ebc3d12b690e1fec46362e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 13 Oct 2026 21:39:16 +0800 Subject: [PATCH 11/15] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AE=E4=B8=AD?= =?UTF-8?q?=E5=BF=83-=E7=89=88=E6=9C=AC=E5=8E=86=E5=8F=B2=EF=BC=88?= =?UTF-8?q?=E5=88=97=E8=A1=A8/=E4=B8=A4=E7=89=88=E5=AF=B9=E6=AF=94/?= =?UTF-8?q?=E5=9B=9E=E6=BB=9A/=E7=BB=84=E7=BA=A7=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=E5=87=BA=E5=8E=82=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 7d——Phase 3 配置中心四连收尾: - API(api/config.ts):fetchConfigVersions / fetchConfigVersionDetail / rollbackConfig / resetConfigGroup 四函数补齐(骨架端点与类型已就位; 响应形态对照 config_routes 实测——列表无 config 本体、详情整份+掩码、 reset 响应与 PUT 分组保存同形) - VersionsPane:版本 tab 实装(导航占位转正)——列表倒序(版本/时间/ 作者/备注,当前版标记)+ 详情抽屉整份 JSON;勾两版(限两个,第三个 自动撤销最早勾选行)并排等宽 JSON 对比(spec §7.3 最低形态,范围 下拉可聚焦单组——wall 20KB+ 场景);回滚确认框明示「将产生新版本, 历史不变」→ 响应内新快照直接回写页头版本/快照/品牌表单,无需再 GET - 组级重置出厂:六个已实装编辑卡(品牌/提示词/词表/邮件/推送/AI)头部 危险色小按钮 → reset.ts 共用确认入口(明示组名与「恢复出厂档案值」) → POST config/{group}/reset → 表单/密钥态/版本刷新(RESET_GROUP_ LABELS 九组镜像后端 CONFIG_GROUPS,含待实装的联系方式/引擎/杂项) - 测试:vitest 120(+13——API 四函数调用形态 5 条 + model 辅助 8 条); 后端不动 595;冒烟 jiancai:v1-v10 列表 → v1/v10 并排对比(push 组 __SET__ 掩码可见)→ 回滚 v1 产 v11 且品牌回 v1 值 → 改占位存 v12 → 重置品牌产 v13 表单回档案值 → 回滚 v10 复原现场(v14) Co-Authored-By: Claude Code --- admin/src/api/__tests__/config.test.ts | 127 ++++- admin/src/api/config.ts | 86 ++++ admin/src/views/config/center/CredsPane.vue | 99 +++- admin/src/views/config/center/PromptsPane.vue | 44 +- .../src/views/config/center/VersionsPane.vue | 486 ++++++++++++++++++ admin/src/views/config/center/WallPane.vue | 44 +- .../config/center/__tests__/model.test.ts | 81 +++ admin/src/views/config/center/index.vue | 102 +++- admin/src/views/config/center/model.ts | 68 ++- admin/src/views/config/center/reset.ts | 45 ++ 10 files changed, 1166 insertions(+), 16 deletions(-) create mode 100644 admin/src/views/config/center/VersionsPane.vue create mode 100644 admin/src/views/config/center/reset.ts diff --git a/admin/src/api/__tests__/config.test.ts b/admin/src/api/__tests__/config.test.ts index f267988..50f05a4 100644 --- a/admin/src/api/__tests__/config.test.ts +++ b/admin/src/api/__tests__/config.test.ts @@ -1,6 +1,6 @@ /** * 配置中心 API 单元测试(Phase 3 Task 7a——品牌相关;Task 7b——试金石; - * Task 7c——凭据测试端点) + * Task 7c——凭据测试端点;Task 7d——版本历史/回滚/组级重置) * * 钉住 API 层契约(事实源 app/platform/routes/config_routes.py 与 * upload_routes.py);HTTP 层行为已在 client.test.ts 覆盖,此处 mock http @@ -13,6 +13,12 @@ * - POST email/test · push/test(Task 7c):前者 body 只带 {to}(用已 * 保存配置真发一封);后者无请求体(探测读已保存配置,空批量零线索 * 数据)——两者 showErrorMessage: false 内联展示 + * - GET versions / versions/{n}(Task 7d):列表不含 config 本体;详情含 + * 整份快照(密钥 '__SET__' 掩码视图) + * - POST rollback(Task 7d):body 只带 {version}——目标版本复制为新版本 + * (历史不可变),响应含新版本号 + 新快照 + * - POST config/{group}/reset(Task 7d):无请求体(后端 reset 不读 + * body),响应与 PUT 分组保存同形 * - 头像上传:multipart file 字段(FormData) * - normalizeBrandGroup:缺键补空串 / 类型漂移防御 / topics 只留字符串项 */ @@ -25,9 +31,13 @@ vi.mock('@/api/client', () => ({ import { http } from '@/api/client' import { CONFIG_ENDPOINTS, + fetchConfigVersionDetail, + fetchConfigVersions, fetchTenantConfig, normalizeBrandGroup, putConfigGroup, + resetConfigGroup, + rollbackConfig, testEmail, testPush, testWall, @@ -263,6 +273,121 @@ describe('凭据测试 API(testEmail / testPush · Phase 3 Task 7c)', () => }) }) +describe('版本历史 / 回滚 / 重置 API(Task 7d · 事实源 config_routes)', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('fetchConfigVersions:GET versions 列表(升序元数据,不含 config 本体)', async () => { + const response = { + slug: 'jiancai', + items: [ + { + version: 1, + created_at: '2026-10-12T09:00:00', + updated_by: 'super#1', + comment: '创建向导初始版本' + }, + { + version: 2, + created_at: '2026-10-13T10:30:00', + updated_by: 'tenant#3', + comment: '改主题色' + } + ] + } + vi.mocked(http.get).mockResolvedValue(response) + + const res = await fetchConfigVersions('jiancai', { showErrorMessage: false }) + + expect(res).toEqual(response) + expect(res.items[0]).not.toHaveProperty('config') // 列表行绝无 config 本体 + expect(http.get).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/config/versions', + { showErrorMessage: false } + ) + }) + + it('fetchConfigVersionDetail:GET versions/{n} 详情(整份快照 + 掩码)', async () => { + const response = { + slug: 'jiancai', + version: 2, + created_at: '2026-10-13T10:30:00', + updated_by: 'tenant#3', + comment: '改主题色', + config: { + brand: { title: 'HESUPIPE Support' }, + push: { api_key: '__SET__' } + } + } + vi.mocked(http.get).mockResolvedValue(response) + + const res = await fetchConfigVersionDetail('jiancai', 2) + + expect(res).toEqual(response) + expect(http.get).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/config/versions/2', + undefined + ) + }) + + it('rollbackConfig:POST rollback,body 只带 {version}(note 不被后端读取)', async () => { + const response = { + slug: 'jiancai', + rollback_to: 2, + version: 11, + config: { brand: { title: 'HESUPIPE Support' } } + } + vi.mocked(http.post).mockResolvedValue(response) + + const res = await rollbackConfig('jiancai', { version: 2 }, { + showErrorMessage: false + }) + + expect(res).toEqual(response) + expect(http.post).toHaveBeenCalledTimes(1) + expect(http.post).toHaveBeenCalledWith( + '/api/platform/tenants/jiancai/config/rollback', + { version: 2 }, // 回滚请求体只认 version——多余字段是死代码 + { showErrorMessage: false } + ) + }) + + it('resetConfigGroup:POST config/{group}/reset,无请求体(后端不读 body)', async () => { + const response = { + slug: 'jiancai', + group: 'brand', + version: 12, + config: { title: 'AI Support', color: '#066fc0' } + } + vi.mocked(http.post).mockResolvedValue(response) + + const res = await resetConfigGroup('jiancai', 'brand', { showErrorMessage: false }) + + expect(res).toEqual(response) // 响应与 PUT 分组保存同形(新版本 + 组掩码视图) + expect(http.post).toHaveBeenCalledTimes(1) + const [url, body, config] = vi.mocked(http.post).mock.calls[0] + expect(url).toBe('/api/platform/tenants/jiancai/config/brand/reset') + expect(body).toBeUndefined() + expect(config).toEqual({ showErrorMessage: false }) + }) + + it('CONFIG_ENDPOINTS.versions / versionDetail / rollback / resetGroup:路径与后端一致', () => { + expect(CONFIG_ENDPOINTS.versions('jiancai')).toBe( + '/api/platform/tenants/jiancai/config/versions' + ) + expect(CONFIG_ENDPOINTS.versionDetail('jiancai', 7)).toBe( + '/api/platform/tenants/jiancai/config/versions/7' + ) + expect(CONFIG_ENDPOINTS.rollback('jiancai')).toBe( + '/api/platform/tenants/jiancai/config/rollback' + ) + expect(CONFIG_ENDPOINTS.resetGroup('jiancai', 'push')).toBe( + '/api/platform/tenants/jiancai/config/push/reset' + ) + }) +}) + describe('normalizeBrandGroup(GET 原样组 → 表单完整值)', () => { it('缺键补空串、topics 缺省为空数组(快照是原样组,可能缺键)', () => { const out = normalizeBrandGroup({ title: 'HESUPIPE Support' }) diff --git a/admin/src/api/config.ts b/admin/src/api/config.ts index 19920bf..d11dd52 100644 --- a/admin/src/api/config.ts +++ b/admin/src/api/config.ts @@ -33,6 +33,20 @@ * - POST push/test:连通性探测——只验 URL 可达 + 鉴权,**不发任何线索 * 数据**(后端发空 messages 批量);不可达时 reason 分类 * dns/timeout/connect,auth_ok 三态(true/false/null=不定)。 + * + * ## Task 7d 版本历史 / 回滚 / 重置契约(config_routes) + * + * - GET versions:升序 items(version/created_at/updated_by/comment), + * **不含 config 本体**(列表几十 KB × N 版本会白白撑爆响应);展示侧 + * 自行倒序(最新在前); + * - GET versions/{n}:含整份快照(掩码视图——密钥字段 '__SET__',历史行 + * 本身永远存原值);created_at 是 naive isoformat(无时区后缀); + * - POST rollback {version}:目标版本整份复制为新版本(历史不可变), + * 响应 {rollback_to, version(新), config(新快照掩码视图)}; + * - POST config/{group}/reset:该组恢复出厂档案值(代码库当前模板,非 + * 创建时快照),响应形态与 PUT 相同({group, version, config}); + * 合法组 = config_validation.CONFIG_GROUPS(RESET_GROUP_LABELS 镜像); + * push 组重置可能因出厂 prefix 被其他租户占用而 400(跨租户查重)。 */ import { http, type RequestOptions } from './client' @@ -78,6 +92,12 @@ export interface PutConfigGroupResponse { config: Record } +/** + * 组级重置响应(POST .../config/{group}/reset):与 PUT 分组保存同形 + * (新版本号 + 该组出厂值的掩码视图——密钥字段 '__SET__')。 + */ +export type ResetConfigGroupResponse = PutConfigGroupResponse + /** 配置版本行(GET .../config/versions;字段名与后端一致) */ export interface ConfigVersionItem { version: number @@ -217,6 +237,72 @@ export function putConfigGroup( ) } +/** + * 版本列表(GET .../config/versions):升序 items(版本/时间/作者/备注), + * 不含 config 本体。展示侧自行倒序(最新在前)——后端列表只回元数据, + * 详情走 fetchConfigVersionDetail 按需取整份。 + */ +export function fetchConfigVersions( + slug: string, + config?: RequestOptions +): Promise { + return http.get( + CONFIG_ENDPOINTS.versions(slug), + config + ) +} + +/** + * 单版本详情(GET .../config/versions/{n}):含整份快照(密钥字段为 + * '__SET__' 掩码视图——历史行存原值但绝不回传)。版本不存在 → 404。 + */ +export function fetchConfigVersionDetail( + slug: string, + version: number, + config?: RequestOptions +): Promise { + return http.get( + CONFIG_ENDPOINTS.versionDetail(slug, version), + config + ) +} + +/** + * 回滚(POST .../config/rollback):目标版本整份复制为新版本(历史不可变 + * ——旧版本行永不改写),reload 热生效。响应含新版本号 + 新快照掩码视图 + * (调用方据此回写当前配置态,无需再 GET config)。 + */ +export function rollbackConfig( + slug: string, + body: ConfigRollbackRequest, + config?: RequestOptions +): Promise { + return http.post( + CONFIG_ENDPOINTS.rollback(slug), + body, + config + ) +} + +/** + * 组级重置出厂(POST .../config/{group}/reset,无请求体):该组恢复出厂 + * 档案值(代码库当前模板,随平台升级演进——非创建时快照),产新版本 + + * reload 热生效。合法组见 model.ts 的 RESET_GROUP_LABELS(后端 + * CONFIG_GROUPS 镜像,未知组 400);push 组重置可能因出厂 prefix 被其他 + * 租户占用而 400(跨租户查重)——调用方 showErrorMessage: false 内联展示。 + */ +export function resetConfigGroup( + slug: string, + group: ConfigGroupKey | string, + config?: RequestOptions +): Promise { + return http.post( + CONFIG_ENDPOINTS.resetGroup(slug, group), + undefined, + config + ) +} + /** * 词表试金石(POST .../wall/test):逐句墙判定预览。传 wall 覆盖 = * 编辑器当前表单值的预演(不落库不 bump 版本)——保存词表前的自动 diff --git a/admin/src/views/config/center/CredsPane.vue b/admin/src/views/config/center/CredsPane.vue index 5271650..cdb6704 100644 --- a/admin/src/views/config/center/CredsPane.vue +++ b/admin/src/views/config/center/CredsPane.vue @@ -13,6 +13,9 @@ - 推送连通性探测:POST push/test(后端发空 messages 批量,零线索数据) ——三态展示:不可达(dns/timeout/connect 原因分类)/ 可达但鉴权失败 (401·403)/ 正常(2xx);其余状态码 = 可达但鉴权不定 + - 重置出厂(Task 7d):三卡头部危险色小按钮 → 确认框 → POST + config/{group}/reset → 表单/密钥态回档案值 + emit saved;push 出厂 + 前缀被其他租户占用的 400 detail 内联展示 -->

yjkcOw&(>Y8mz^;5!^gv~u``#^d4Gx|bp z2-7XGTS)^qhf7!w*}02Gj=X#OF@kb6Jt3zqO+zHUi|*y6rT)zw2fWpG5HATLaEHBA?3FMMUbKN! z#PTx@LRbQWSrGV>{xDg;Y&mz%O`b0->jK)iZ$3px^C{9_{ufhP&J*iK#sD|4mEY65 zo_)#pd^kuy=pjs7EPZ($mXiA4=KVU~KK%qdoT$y{+sF>raM|L3$68%+)I;OfUJTDx z1U>Y4LD0a6u&j$I!_&)rdJZMen2S(UBFCq5k=S`7fSS^^=A=x(J*#i zKz~~J*K*g#S=`a;a@2hrk*YK539RJvg8R}@o;E{PoRRvhvd?og-ce(w(-^A772(B( zwL2heplT-AQ4b4Hp0}RR@qK_+3oDXt)Qd>>d`CW+gt~#ZDk|uueQ--tz-xBiqRf(p zSffC>me&9Q_wx4x z#)Qrvl1BIk!nP|C;SW46WI9zBNIC596gbh(Um)?0RRV@0Qr)AD2Mixv&BN8B^nL0xEYRQ+MWL)?U}O`I{8z}8Q? zqG61P(MtfE8g1vOCeR!PQzyS$q+)y+-wzSkduvQVFUmM|rHAQycjo0CzZDu)rXQXWDzDqA%HPG7qYby8MxfS z>2kznxxt&D$SV6{@UgaOVu)ace0YO7ty}dTMy^_LW@vip+jR!lQOLYfcUR}c^Y@!Jnct35&-L@u9|L%xY>y6!J@{c3l!bY^F&>hg%@dSqG{+5` zE4-aaWE0hb+H_tHF}e*kiqSi+VPyDuDOBh%0Q9PS8x*jW{fsR42v9aN&nPoT-pD;Mch2^&mj&z{}% z?ao99Xsn)nJGGiN&%)>Y*vrk5Qd)>~eENEgD#>kYk?|f?-Dc2*)GlQs5fWw2%`c@1 zquNsRdGT*p7sf+aGhl!xV{-_s@?%#S8;Q64tRn2_{uzADxkbqN!l&w_!_$_;R zdnsX>lMrYouxu71`|g>Q3b*oO4VgPmk9>qyZ|=MVra^f>#y@Cjo3Le`o&ytIK5lE8sW9IS|@f;Q>AtCT=tq~<&h4j>sRb|(%9~?-avmzJm z(1;a^3(S5?{Nf<=?U3i=pJL|ad65!sGoKsxu4*RAB1&0F-Mi^-asgy-m6dR=Z{s3z zavmTR&xu&hAESu(?TAP1rOXTC!xu7C0j{|8YD=rQ4Ws+FS?yX}djleb%}G-Eu3=yv z)mED!6H2>+u;Zxto<8<9Xt45@(?|y8300?P{epb5UOjZ0LTRvgAN0Gwa^aTvL}hcM z)9UMLmZ(pjz3255xlAiZd9kwXYEhk{s~v3B#SbqriecXENeu4c8U6V)#W*Wz7YpRb zLVC&9vT1wI*EHG3Ct7ZF#!Rq;MAkc;sIb$ya+2Ek5XwAq3L;suewr*%>z-P4&q}Ax zsEye_4g<-Mn9J*lN*ADRlVX_0btd~eEGu;uXcR_)`tC!^^j(VFwKJ-BR~mm~ZhU>= zTNr;_cxu*T!7VxB&C=LX(l&Z7ZKcou4OcsR9#@%L%ez_E-QhOB!gpU@uEl@cTj}=6 zb4utCT-AslKou@|scXX_;#vr13|;2cdxC|i810%0mPcpmHEW+^gai|EgUzEXEtq-2 zG@*;-2-P_$0p0UO1rm9p^wUi_;7Y?p7k{1K!t4@hxiGazC{z9&y8B&~{Soo0+aYe{ z$lVfa8?;-5&uRUxp?d4ZgPt(ISdo%bOB#jOF+-ujYKA}jn`5q`)J-P4USjFJZw%MhK;@VL4a^|*giXyGFs{+74$5gu-fsXz^`$=Lh literal 0 HcmV?d00001 diff --git a/admin/src/assets/images/ceremony/yd.png b/admin/src/assets/images/ceremony/yd.png new file mode 100644 index 0000000000000000000000000000000000000000..426912d581d699afa00e95ad5e9c54d9cdda03f7 GIT binary patch literal 4629 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