Quellcode durchsuchen

feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释)

框架层(index.html + vendor/,8 项 UMD,自托管 OSS):
- vue@3.5.45 / vue-demi@0.14.10 / vue-router@4.6.4 / pinia@2.3.1 /
  element-plus@2.14.7 (CSS+JS+zh-cn) / axios@1.21.0
- 新增 vue-demi 桥接层:pinia IIFE 构建硬依赖 VueDemi 全局(官方构建写死
  }({}, VueDemi)),漏加载会导致 store 全崩。加载顺序 vue→vue-demi→pinia。
  2026-11-06 经 vm 沙箱端到端冒烟验证(defineStore+createPinia+ref 联动通过)。

业务层(src/ 19 文件,TS strict,全部三段式注释):
- 禁 import 框架运行时:裸标识符经 vite define 注入为 Vue.*/VueRouter.*/Pinia.* 全局;
  仅允许 import type(编译期擦除);SFC 编译器产物经 alias 重定向 shims/。
- shims/(gen-shims.cjs 生成):vue 161 导出 + vue-router 23 + pinia 17 逐一静态转发,
  运行时值取 globalThis,框架本体不进 dist。
- 示例页:org/institution/list.vue(泛型 SFC + useListPage 顶层解构)+
  login/index.vue + org/scope-select.vue;store/login + store/scope(setup store)。

质量门禁(全绿):
- vue-tsc --noEmit 0 错误;vite build 0 错误(主 chunk ~10KB,Vue UMD 172KB 未进 dist);
  eslint 0 错误(修正 no-restricted-syntax 精确拦截"值 import 框架"、放行 import type,
  补 .vue TS parser、关 no-undef、ignore shims/scripts);
  dist grep 门禁判据修正为 runtime-core 内部标识("vnode"/createBaseVNode 等,
  非公开 API 名),避免 shims 转发层属性名误报。

文档:
- README:技术栈(8 项清单+加载顺序+vue-demi 说明)/ 目录树(补 shims/scripts/
  global.d.ts/directives/identity)/ 编码规范(§3.3-1 define 注入定稿、§3.6 dist 门禁判据)/
  vite.config 机制说明;globals.d.ts→global.d.ts 路径统一。
- EXAMPLES.md(新建):三段式注释模板 + 19 文件索引 + 4 个真实代码范式摘录。

本地 commit(未 push):push 需用户逐次确认(工程纪律)。
Q vor 3 Tagen
Ursprung
Commit
0681a3a046

+ 5 - 0
.env.example

@@ -0,0 +1,5 @@
+# 后端 API base(生产由 nginx 按 location 分发 /api,通常留空即可)
+VITE_API_BASE=
+
+# dev 联调时 /api 代理目标(vite dev 用;可用环境变量覆盖,默认 http://127.0.0.1:8080)
+# VITE_API_PROXY_TARGET=http://127.0.0.1:8080

+ 2 - 0
.gitignore

@@ -22,3 +22,5 @@ npm-debug.log*
 .vscode/
 *.iml
 .DS_Store
+
+# 注:vendor/(框架层 UMD 自托管源文件)必须入库——它是版本事实的一部分,勿加 ignore。

+ 2 - 0
.npmrc

@@ -0,0 +1,2 @@
+registry=https://registry.npmmirror.com
+save-exact=false

+ 204 - 0
EXAMPLES.md

@@ -0,0 +1,204 @@
+# PC 前端样例索引与注释规范
+
+> 本仓库 `src/` 下 19 个文件全部按「三段式注释标准」编写(与后端 `EXAMPLES.md` 同源),既是**可运行脚手架**,也是**编码范式参照**。
+> 新建任何 `.ts` / `.vue` 文件时,对照本文件的「注释模板」与「范式摘录」落笔。
+> 接口契约与角色权限见工作区文档库 `unified/`、`academic/05-architecture/`、`teaching/05-architecture/`。
+
+## 1. 三段式注释标准(强制)
+
+每个**结构体(interface/type/const 对象)**与**函数/方法**必须包含三段:
+
+| 段 | 必含要素 | 写法约定 |
+|---|---|---|
+| ① 功能说明 | 用途 + 核心逻辑 + **典型使用样例**(调用代码) | 文件头块注释;函数用 JSDoc `/** */` |
+| ② 入参说明 | 逐项:`名称 类型:含义`(参数名用反引号) | 复杂入参分多行;简单单行 |
+| ③ 出参说明 | 逐项:`名称 类型:含义` + **异常场景**(何时抛错/返回空/兜底) | 明确"恒 resolve / 不 reject / 抛错"之一 |
+
+**铁律**:
+- 注释必须与实现**逐字一致**(改签名必改注释,CI 不卡但评审必查)。
+- **禁**空泛套话("这是一个工具函数");**禁**复制粘贴不改(每个文件的"典型使用"必须真实可运行)。
+- 异常场景**必写**:网络失败、空数据、并发、权限不足、未登录——至少覆盖本函数可能触达的 1 类。
+- 中文书写(与项目默认简体中文一致),术语保留英文(如 `ref`/`computed`/`PageData`)。
+
+### 1.1 文件头模板
+
+```ts
+/**
+ * <文件名> —— <一句话定位(关联 README 章节号)>。
+ *
+ * 职责边界:
+ *   - <这条文件做什么>;
+ *   - <这条文件不做什么(防越界)>。
+ *
+ * 典型使用(<调用方文件>):
+ *
+ *   <3~6 行真实调用代码,可复制到新文件直接跑>
+ */
+```
+
+### 1.2 函数/方法模板
+
+```ts
+/**
+ * <函数名> —— <一句话功能>。
+ *
+ * 入参:
+ *   - <参数名> <类型>:<含义>(边界条件:如"必须 > 0")
+ *   - <参数名2> <类型>:<含义>(可选,缺省 <值>)
+ *
+ * 出参:
+ *   - <返回类型>:<含义>
+ *
+ * 异常场景:
+ *   - <场景1>:<行为(抛错/返回空/静默)>
+ *   - <场景2>:<行为>
+ */
+```
+
+## 2. 样例文件索引(19 文件,按目录)
+
+> 每行标注「范式亮点」——读对应文件时重点看什么。
+
+### 仓库根(构建/类型侧)
+
+| 文件 | 职责 | 范式亮点 |
+|---|---|---|
+| `vite.config.ts` | define 注入 + alias→shims + dev 代理 + build 门禁 | 顶部块注释讲清"裸标识符替换"与"alias 重定向"两套机制的分工(§1.2) |
+| `global.d.ts` | UMD 全局 + define 裸标识符 TS 声明 | `declare global` 内 `var ElementPlus`/`var axios`(允许属性式访问)vs `const`(纯引用)的差异注释 |
+| `shims/vue.ts` | `'vue'` 模块名 → UMD 全局转发(161 导出) | **生成文件**(`scripts/gen-shims.cjs`),文件头讲清"import type 取类型 + 运行时取 globalThis"双轨 |
+| `index.html` | 框架层挂载 + vendor 版本表 | 注释块含"加载顺序 + vue-demi 桥接原因 + OSS 布局"三件事 |
+
+### `src/` 业务层
+
+| 文件 | 职责 | 范式亮点 |
+|---|---|---|
+| `main.ts` | 业务装配入口 | 7 步装配顺序逐行注释 + `failMissingGlobal` 失败提示函数(异常场景示范) |
+| `App.vue` | 根组件(locale 声明式注入) | `const locale = ElementPlusLocaleZhCn` 顶层解构(模板可引用) |
+| `types/api.d.ts` | 契约类型 `R<T>`/`PageData<T>`/`PageQuery` | 泛型 `T` 的默认值与边界注释(`R<undefined>` 省略 data) |
+| `identity/token-store.ts` | token 物理存取收口 | **为何独立于 store**(http 拦截器在 pinia 激活前可能触发)——职责边界注释范本 |
+| `api/http.ts` | axios 实例 + 信封处理 + 401 refresh 单例 | 请求/响应拦截器分段注释;`refreshAccessToken` 单例 Promise 的并发安全注释 |
+| `api/auth/login.ts` | 登录/登出接口 | 一资源一文件:导出 `login`/`logout` + `LoginResult` DTO 类型 |
+| `api/org/institution.ts` | 机构列表接口(示范) | fetcher 签名对齐 `useListPage` 的 `PageR<T>` 返回 |
+| `store/login.ts` | 登录态 store(setup 风格) | **setup store 返回面类型**(`Ref`/`ComputedRef` 字段)+ 消费时自动解包的注释 |
+| `store/scope.ts` | 作用域 store | `toQuery()` 的"campus=0 省略键"边界注释 |
+| `composables/use-list-page.ts` | 列表页唯一规范入口 | **泛型 composable**:入参 `UseListPageOptions<T>` + 出参 `ListPageState<T>` 双接口定义范式 |
+| `components/ListPage.vue` | 泛型表格+分页组件 | **Vue 3.5 泛型 SFC**(`generic="T extends object"`)+ `withDefaults` 注释 |
+| `components/list-column.ts` | 列定义类型 `ListColumn<T>` | 纯类型文件:字段逐项注释(`formatter` 签名) |
+| `directives/perm.ts` | `v-perm` 权限指令 | `mounted`/`updated` 双钩子 + fail-closed 行为注释 |
+| `router/index.ts` | 路由表 + 守卫链 | 守卫链 4 步顺序注释 + `declare module 'vue-router'` 扩展 `RouteMeta` |
+| `views/login/index.vue` | 登录页(公开路由) | SFC 三段式:`<script setup>` 内 `defineProps` + 响应式 + 生命周期 |
+| `views/org/scope-select.vue` | 作用域选择器 | 联动组件:从 store 读(例外于"零业务耦合"的注释说明) |
+| `views/org/institution/list.vue` | 机构列表(示范页) | **setup 顶层解构** `const { rows, total, page... } = useListPage(...)` 范式 |
+| `utils/format.ts` | 纯函数工具集 | 零 Vue 依赖:每个函数独立 JSDoc(入参/出参/异常) |
+| `styles/variables.scss` | 全局样式 token | 色板/间距 token 命名约定注释 |
+
+## 3. 范式摘录(可直接套用的真实片段)
+
+### 3.1 文件头(三要素齐全)—— 摘自 `identity/token-store.ts`
+
+```ts
+/**
+ * token-store.ts —— token 物理存取收口(README §2 目录约定:唯一 localStorage 触点)。
+ *
+ * 职责边界:
+ *   - 只存 access/refresh/expiresAt 三个键;提供 set/get/clear 原子操作;
+ *   - **不**做登录态判定(那是 store/login 的事);**不**发请求(那是 api/http 的事)。
+ * 独立于 pinia 的原因:http 拦截器在 pinia 激活前就可能触发(401 refresh),
+ *   不能依赖 useLogin(),故 token 物理值走本模块。
+ *
+ * 典型使用(api/http.ts 请求拦截器):
+ *
+ *   const t = getAccessToken();
+ *   if (t) cfg.headers.Authorization = `Bearer ${t}`;
+ */
+```
+
+### 3.2 函数三段式(入参/出参/异常)—— 摘自 `composables/use-list-page.ts`
+
+```ts
+/**
+ * 执行一次分页拉取。
+ *
+ * 入参:无(page/pageSize 取当前响应式值;作用域自动并入)。
+ *
+ * 出参:
+ *   - Promise<void>:恒 resolve(错误经 error 响应式暴露,不 reject——调用方不必 try/catch)。
+ * 异常场景:
+ *   - 并发点击:loading 期间重入直接忽略(防重复请求)。
+ */
+async function fetchList(): Promise<void> {
+  if (loading.value) return;
+  loading.value = true;
+  error.value = '';
+  try {
+    const res = await options.fetcher(page.value, pageSize.value);
+    const body: PageData<T> | undefined = res.data;
+    if (res.code === 200 && body) {
+      rows.value = body.list ?? [];
+      total.value = body.total ?? 0;
+    } else {
+      rows.value = []; total.value = 0;
+      error.value = res.message || '查询失败';
+    }
+  } catch {
+    rows.value = []; total.value = 0;
+    error.value = '网络连接异常';
+  } finally {
+    loading.value = false;
+  }
+}
+```
+
+### 3.3 结构体(interface 字段逐项)—— 摘自 `store/login.ts`
+
+```ts
+/**
+ * setup store 的返回面类型(defineStore 的 setup 函数返回值;字段为 ref 本体)。
+ * 注:经 `useLogin()` 消费时 Pinia **自动解包**(组件里 login.accessToken 是 string | null),
+ * 本接口仅约束 setup 函数返回,消费方以 useLogin() 推断类型为准。
+ */
+export interface LoginStore {
+  /** access token(响应式视图;物理值以 token-store 为准) */
+  accessToken: Ref<string | null>;
+  /** 过期时间(Unix 毫秒;null = 无记录,按已过期处理) */
+  expiresAt: Ref<number | null>;
+  /** 角色码数组(7 角色字典;未登录为 []) */
+  roles: Ref<number[]>;
+  /** RBAC 权限码集合(`域.资源.动作`;菜单/按钮显隐唯一依据,README §3.1) */
+  permissions: Ref<Set<string>>;
+  /** 是否已登录(有 access token 且未过期;守卫链第一步) */
+  isLoggedIn: ComputedRef<boolean>;
+  /** 判断是否持有指定权限码(v-perm 指令用) */
+  hasPerm: (perm: string) => boolean;
+}
+```
+
+### 3.4 异常场景兜底 —— 摘自 `main.ts`
+
+```ts
+/** UMD 全局缺失提示(开发期定位"vendor 没加载"用;生产 nginx 侧应监控 404) */
+function failMissingGlobal(name: string): never {
+  document.getElementById('app')?.replaceWith(
+    Object.assign(document.createElement('pre'), {
+      style: 'padding:24px;color:#c0392b;font-size:14px',
+      textContent: `[框架未加载] 全局 ${name} 不存在——请检查 index.html 的 vendor 引用与 OSS/静态资源可达性。`,
+    }),
+  );
+  throw new Error(`missing UMD global: ${name}`);
+}
+```
+
+## 4. 与后端的对照
+
+| 维度 | 后端(`backend/EXAMPLES.md`) | 前端(本文件) |
+|---|---|---|
+| 注释语言 | Go doc 风格(`// FuncName 一句话`) | JSDoc(`/** */`) |
+| 三段式 | 功能/入参/出参 + panic 场景 | 功能/入参/出参 + 异常场景 |
+| 异常表述 | "panic" / "返回 error" | "抛错" / "恒 resolve" / "静默" |
+| 契约对齐 | `R` 信封 / `PageData` 分页 | `R<T>` / `PageData<T>`(同一契约的前端类型化) |
+
+## 5. 质量门禁关联
+
+- 注释**不**被 ESLint/vue-tsc 自动校验(注释无语法错误即通过);
+- 但**评审必查**:签名改动后注释未同步 = 打回(README §3.7 提交前自检)。
+- `shims/` 由 `scripts/gen-shims.cjs` 生成,**不手改注释**(重新生成会覆盖)。

+ 54 - 30
README.md

@@ -14,7 +14,7 @@
 
 | 层 | 内容 | 引入方式 | 版本管理 |
 |---|---|---|---|
-| **框架层** | Vue 3 / Vue Router 4 / Pinia / Element Plus(JS + CSS + zh-cn locale)/ axios,共 7 项 UMD 资源 | `index.html` 内 `<link>`/`<script>` 按**固定顺序**引入,源文件**自托管到自有 OSS**(自定义域名 + SSL,不用第三方 CDN) | 版本事实 = `index.html` 头部"vendor 版本表"(钉死次版本)+ 仓库 `vendor/` 源文件(git 入库) |
+| **框架层** | Vue 3 / vue-demi / Vue Router 4 / Pinia / Element Plus(JS + CSS + zh-cn locale)/ axios,共 8 项 UMD 资源 | `index.html` 内 `<link>`/`<script>` 按**固定顺序**引入,源文件**自托管到自有 OSS**(自定义域名 + SSL,不用第三方 CDN) | 版本事实 = `index.html` 头部"vendor 版本表"(钉死次版本)+ 仓库 `vendor/` 源文件(git 入库) |
 | **业务层** | 本项目全部业务代码(TS 编写) | 本地 `pnpm` 工具链编译(Vite/vue-tsc/ESLint/Prettier 为 dev 依赖),产物 `dist/` 经 nginx 部署 | `pnpm-lock.yaml` 入库(只锁工具链,不含框架) |
 
 - **开发测试**:`pnpm dev` 起 Vite 本地服务(HMR 只热更业务代码,框架层从自托管 OSS 加载);`/api` 代理到后端 dev 地址。
@@ -25,12 +25,13 @@
 | # | 资源 | OSS 路径模板 | 全局变量 | 用途 |
 |---|---|---|---|---|
 | 1 | Vue 3(prod UMD) | `vue/{v}/vue.global.prod.js` | `Vue` | 框架本体 |
-| 2 | Vue Router 4 | `vue-router/{v}/vue-router.global.js` | `VueRouter` | 路由(按端组织 + 守卫链) |
-| 3 | Pinia | `pinia/{v}/pinia.iife.prod.js` | `Pinia` | 状态(login / scope 两 store) |
-| 4 | Element Plus CSS | `element-plus/{v}/index.css` | — | UI 样式(走自托管,不打包) |
-| 5 | Element Plus(full UMD) | `element-plus/{v}/index.full.min.js` | `ElementPlus` | UI 组件(表格/表单/日期等全量) |
-| 6 | Element Plus 中文 locale | `element-plus/{v}/locale/zh-cn.js` | `ElementPlusLocaleZhCn` | 默认简体中文 |
-| 7 | axios | `axios/{v}/axios.min.js` | `axios` | HTTP(统一封装于 `src/api/`) |
+| 2 | vue-demi(IIFE) | `vue-demi/{v}/vue-demi.iife.js` | `VueDemi` | **pinia 桥接层**:pinia IIFE 构建硬依赖 `VueDemi` 全局(官方构建写死 `}({}, VueDemi)`);本文件读 `globalThis.Vue` 后把 Vue API 镜像到 `VueDemi`(Vue3 下等价 re-export Vue)。漏加载 → pinia 报 `VueDemi is not defined`,store 全崩。与 pinia 同升同降(2026-11-06 补入) |
+| 3 | Vue Router 4 | `vue-router/{v}/vue-router.global.js` | `VueRouter` | 路由(按端组织 + 守卫链) |
+| 4 | Pinia | `pinia/{v}/pinia.iife.prod.js` | `Pinia` | 状态(login / scope 两 store) |
+| 5 | Element Plus CSS | `element-plus/{v}/index.css` | — | UI 样式(走自托管,不打包) |
+| 6 | Element Plus(full UMD) | `element-plus/{v}/index.full.min.js` | `ElementPlus` | UI 组件(表格/表单/日期等全量) |
+| 7 | Element Plus 中文 locale | `element-plus/{v}/locale/zh-cn.js` | `ElementPlusLocaleZhCn` | 默认简体中文 |
+| 8 | axios | `axios/{v}/axios.min.js` | `axios` | HTTP(统一封装于 `src/api/`) |
 
 > OSS 根路径固定:`https://<自有OSS域名>/static/vendor/<库名>/<版本号>/<文件名>/`,**只增不删**(旧版本路径保留,回滚 = 改回 index.html 版本号)。
 
@@ -39,7 +40,7 @@
 | 关注点 | 选型 | 理由 / 依据 |
 |---|---|---|
 | 构建 | **Vite 6.x** | 业务代码 HMR + 产物构建;框架 external 后主 chunk 只剩业务代码 |
-| 语言 | **TypeScript strict** | 契约类型化:`R<T>` / `PageData<T>` / 各域 DTO;UMD 全局经 `src/types/globals.d.ts` 声明类型 |
+| 语言 | **TypeScript strict** | 契约类型化:`R<T>` / `PageData<T>` / 各域 DTO;UMD 全局经仓库根 `global.d.ts` 声明类型 |
 | UI 组件库 | **Element Plus 2.x**(UMD 自托管) | 教学 admin 现有栈,复用成本最低;zhCn locale 双注入 |
 | 状态 | **Pinia** | `login`(token/角色/权限码)+ `scope`(institution/campus 作用域) |
 | 路由 | **Vue Router 4** | 按端组织 + 守卫链(登录态 → 静默刷新 → 权限码 → 作用域就绪) |
@@ -64,50 +65,73 @@
 
 ```
 .
-├── index.html                 ← 【框架层挂载点】lang="zh-CN";7 项 vendor 资源按固定顺序引入;
+├── index.html                 ← 【框架层挂载点】lang="zh-CN";8 项 vendor 资源按固定顺序引入;
 │                                  头部注释含"vendor 版本表"(库 → OSS 路径 → 版本,一处维护);
 │                                  <div id="app"> + 业务入口 <script type="module" src="/src/main.ts">
 ├── vendor/                    ← 【框架层自托管源文件】git 入库的版本事实(升级五步流程落点)
 │   ├── vue@x.y/                 vue.global.prod.js
+│   ├── vue-demi@x.y/            vue-demi.iife.js(pinia 桥接层,须在 vue 之后、pinia 之前加载)
 │   ├── vue-router@x.y/          vue-router.global.js
-│   ├── pinia@x.y/               pinia.iife.prod.js
+│   ├── pinia@x.y/               pinia.iife.prod.js(依赖 VueDemi 全局)
 │   ├── element-plus@x.y/        index.full.min.js + index.css + locale/zh-cn.js
 │   └── axios@x.y/               axios.min.js
 │                                  (上传 OSS 经 /static/vendor/<库名>/<版本号>/ 暴露;只增不删)
 ├── package.json               ← 仅【业务层工具链】:vite / @vitejs/plugin-vue / vue-tsc / eslint / prettier / typescript
 ├── pnpm-lock.yaml             ← 工具链版本锁定(入库)
-├── vite.config.ts             ← ① rollup external:vue/vue-router/pinia/axios → 全局变量映射(运行时只认 UMD 全局)
-│                                  ② dev server:/api 代理 → 后端 dev 地址  ③ 产物 chunk 拆分(业务代码)
-├── tsconfig.json              ← strict 基线 + paths 别名(@ → src/)
+├── vite.config.ts             ← ① define 注入:裸标识符 ref/computed/defineStore… → Vue.*/VueRouter.*/Pinia.* 全局
+│                                  ② alias:'vue'/'vue-router'/'pinia' → shims/*.ts(SFC 编译器产物 import 重定向)
+│                                  ③ dev server:/api 代理 → 后端 dev 地址;publicDir=vendor(dev 本地命中 vendor)
+│                                  ④ build.publicDir=false(dist 不拷贝任何框架文件)+ chunkSizeWarningLimit=1024
+├── shims/                     ← 【胶水层】'vue'/'vue-router'/'pinia' 模块名 → UMD 全局的唯一转发层(git 入库)
+│   ├── vue.ts                 ←   由 scripts/gen-shims.cjs 生成:import type 取 ESM 命名空间类型 + 运行时值取 globalThis.Vue
+│   ├── vue-router.ts          ←   同机制 → globalThis.VueRouter
+│   └── pinia.ts               ←   同机制 → globalThis.Pinia(编译后只剩"取全局 + 转发",框架本体不进 dist)
+├── scripts/                   ← 构建辅助脚本:gen-shims.cjs(重新生成 shims/ 转发清单,框架升级时运行)
+├── tsconfig.json              ← strict 基线 + paths 别名(@ → src/);include 含仓库根 global.d.ts + src/** + shims/**
+├── global.d.ts                ← UMD 全局 + define 注入裸标识符的 TS 声明(declare global;放仓库根以覆盖 shims/)
 ├── .env.example               ← 环境变量模板(VITE_API_BASE 等;实际 .env.* 走 .gitignore)
 └── src/                       ← 【业务层】全部业务代码(本地编译进 dist/)
     ├── main.ts                ← 业务装配入口:取 UMD 全局 Vue.createApp + Element Plus locale 双注入
-    │                              + Pinia + Router 挂载;不 import 框架运行时
+    │                              + Pinia + Router + v-perm 指令挂载;不 import 框架运行时
+    ├── App.vue                ← 根组件:<el-config-provider :locale="ElementPlusLocaleZhCn"><router-view /></…>
+    ├── identity/
+    │   └── token-store.ts     ←   token 物理存取(localStorage 收口):setTokens/getAccessToken/getRefreshToken/
+    │                               getExpiresAt/clearTokens;http 拦截器与 store 都经此,禁止散写 localStorage
+    ├── directives/
+    │   └── perm.ts            ←   v-perm 按钮级权限指令:读 login store 权限码集,缺失时 el.remove()(fail-closed)
     ├── types/
-    │   ├── globals.d.ts       ←   UMD 全局变量类型声明(declare global:Vue/VueRouter/Pinia/ElementPlus/axios)
-    │   └── api.d.ts           ←   契约类型:R<T> 信封 / PageData<T>(list,total,page,pageSize,pages)/ 角色与权限码
+    │   └── api.d.ts           ←   契约类型:R<T> 信封 / PageData<T>(list,total,page,pageSize,pages)/ PageQuery
+    │                              (UMD 全局声明在仓库根 global.d.ts,不在此目录)
     ├── views/                 ← 页面层(只放视图与页面级状态;禁散写 axios)
     │   ├── academic/          ←   教务域:排班/费用/打卡/充值/报课 ——【新写】(旧 jQuery 视图全部重写)
     │   ├── teaching/          ←   教学 admin:备课/题库/组卷/练习分析 ——【按现有 Vue3 逐步迁入】
-    │   ├── org/               ←   机构/校区/班级/成员管理
-    │   └── system/            ←   系统:用户/角色/字典/任务监控/日志
-    ├── components/            ← 跨页面复用组件:ListPage(表格+分页+筛选+批量收口)、作用域选择器
-    │                              (institution·campus 联动)、GradeBadge/SubjectBadge(色板纯数据零 Vue 依赖)
-    ├── api/                   ← 接口封装(一域一目录、一资源一文件):axios 实例 + 统一信封处理 +
-    │                              401 refresh 单例重放 + 分页统一解构 res.data.{list,total,page,pageSize,pages}
-    ├── router/                ← 路由表按端组织 + 守卫链:登录态 → 静默刷新 → 权限码 → 作用域就绪
-    ├── store/                 ← Pinia:login(token/角色/权限码)、scope(institution/campus)——唯一 token 存取点
-    ├── composables/           ← 可组合逻辑:useListPage<T>(列表页唯一规范入口)等
-    ├── utils/                 ← 纯函数工具集(无 Vue 依赖):日期/金额格式化、脱敏回显、下载/导出辅助
-    └── styles/                ← 全局样式:variables.scss(色板/间距 token)+ reset + Element Plus 主题覆盖
+    │   ├── org/               ←   机构/校区/班级/成员管理(示范页 institution/list.vue + scope-select.vue)
+    │   ├── system/            ←   系统:用户/角色/字典/任务监控/日志
+    │   └── login/             ←   登录页(公开路由,守卫白名单)
+    ├── components/            ← 跨页面复用组件:ListPage.vue(泛型表格+分页+工具条)、list-column.ts(列定义类型)
+    ├── api/
+    │   ├── http.ts            ←   axios 实例 + 统一信封处理 + 401 refresh 单例重放 + 业务错误 toast 去重
+    │   ├── auth/login.ts      ←   登录/登出接口(一资源一文件,返回类型化 DTO)
+    │   └── org/institution.ts ←   机构列表接口(示范:fetcher 供 useListPage 用)
+    ├── router/                ← 路由表按端组织 + 守卫链:登录态 → 静默刷新 → 白名单 → 作用域就绪
+    ├── store/                 ← Pinia:login(token/角色/权限码)、scope(institution/campus)——唯一 token 视图存取点
+    ├── composables/
+    │   └── use-list-page.ts   ←   useListPage<T>:列表页唯一规范入口(分页五元组 + 统一解构 res.data.{list,total})
+    ├── utils/
+    │   └── format.ts          ←   纯函数工具集(无 Vue 依赖):日期/金额格式化、脱敏回显、下载/导出辅助
+    └── styles/
+        └── variables.scss     ← 全局样式与 token:色板/间距 token + Element Plus 主题覆盖
 ```
 
 **目录用途约定**:
 
 | 目录 | 用途 | 约束 |
 |---|---|---|
-| `index.html` | 框架层挂载 + 版本表 | 7 项 vendor 引用**顺序固定**;版本号只改这里,禁止散落 src |
+| `index.html` | 框架层挂载 + 版本表 | 8 项 vendor 引用**顺序固定**(vue → vue-demi → vue-router → pinia → …);版本号只改这里,禁止散落 src |
 | `vendor/` | 框架层源文件版本事实 | 与 OSS 内容一致(提交前 diff 核对);只增不删,禁止覆盖已发布旧版本路径 |
+| `shims/` | 框架模块名 → UMD 全局转发胶水层 | 全仓**唯一**允许出现 `vue`/`vue-router`/`pinia` 模块名的目录;由 `scripts/gen-shims.cjs` 生成,**禁手改**;运行时值取自 `globalThis`,编译后框架本体不进 dist |
+| `scripts/` | 构建辅助脚本 | `gen-shims.cjs`(框架升级五步流程后重跑);Node CJS,不 lint、不随业务进 dist |
+| `global.d.ts` | UMD 全局 + define 裸标识符 TS 声明 | 放**仓库根**(覆盖 shims/ 与 src/);新增框架资源先补声明后写业务 |
 | `src/views/{域}/` | 页面层(四域分包:academic/teaching/org/system) | 只经 `src/api/` 发请求;views 之间互引禁止(复用 → 上提 components/) |
 | `src/components/` | 跨页面复用组件 | 零业务耦合(作用域感知组件除外,从 store 读) |
 | `src/api/{域}/` | 接口封装(一域一目录,一资源一文件) | 信封处理与分页解构**只在此**;返回类型化 DTO |
@@ -161,7 +185,7 @@
 
 ### 3.3 组件编写
 
-1. **框架运行时全走 UMD 全局**:`src/` 内**禁止** `import Vue/VueRouter/Pinia/ElementPlus/axios`;组合 API 标识符经 vite `define` 注入映射到 UMD 全局(或统一 `const { ref } = Vue` 解构,两种写法脚手架定稿时二选一),规范条文恒为"禁 import 框架"。
+1. **框架运行时全走 UMD 全局(2026-11-06 定稿:define 注入)**:`src/` 内**禁止值 import 框架**(`import { ref } from 'vue'` 会把框架运行时打进 dist,ESLint `no-restricted-syntax` 拦截)。落地机制:组合式 API 裸标识符(`ref`/`computed`/`defineStore`…)经 vite `define` 编译期替换为 `Vue.*`/`VueRouter.*`/`Pinia.*` 全局访问(清单见 `vite.config.ts` 的 VUE_APIS/VUE_ROUTER_APIS/PINIA_APIS);**仅允许 `import type`**(编译期擦除、零运行时足迹,TS strict 下类型必须由此引入,如 `import type { Ref } from 'vue'`)。SFC 模板编译器产物内部的模块名 import 经 `vite.config.ts` 的 `alias` 重定向到 `shims/*.ts`(运行时从 `globalThis` 取值转发,框架本体不进 dist)。纪律:`shims/` 是全仓唯一允许出现框架模块名的目录,业务代码不写。
 2. **Props**:显式 `interface` 类型;可选项给默认值;禁内联 `any`。
 3. **状态最小化**:能用 store 的(token/scope/权限码)不本地存;列表状态收口 `useListPage<T>`(page/pageSize/total/loading 全在里面,组件只传 fetcher)。
 4. **作用域纪律**:业务查询参数里的 `institutionId/campusId` **只从 `useScope()` 取**,禁止页面自拼(对齐后端"作用域强制注入",消灭旧系统隐式 `schoolid` 坑)。
@@ -189,7 +213,7 @@
 | 门禁 | 命令 / 做法 | 基线 |
 |---|---|---|
 | 类型检查 | `vue-tsc --noEmit` | 0 错误 |
-| 构建 | `pnpm build`(vite) | 0 错误;主 chunk < 1024KB;**dist/ 产物 grep 不得含 vue/element-plus 运行时**(防 external 配错回退成本地打包) |
+| 构建 | `pnpm build`(vite) | 0 错误;主 chunk < 1024KB;**dist/ 产物 grep 不得含 Vue 运行时本体特征串**(判据:`"vnode"`/`"Teleport"`/`"Suspense"`/`createBaseVNode` 等 runtime-core 内部标识——prod 压缩后仍保留、且**不属于**公开 API,shims 转发层不会引用。⚠️ 不可用公开 API 名如 `openBlock`/`createVNode`/`Fragment` 当判据:shims 层 `export const openBlock = V.openBlock` 压缩后必然保留属性名,属设计预期而非残留。辅助佐证:dist 主 chunk 体积(业务代码 ~10KB 量级)vs vendor Vue UMD(172KB)——框架本体物理装不进小 chunk) |
 | Lint | `pnpm lint`(eslint + prettier) | 0 错误 |
 | vendor 一致性 | 发布前 `vendor/` 与 OSS 路径内容 md5 对账 | 一致方可发布 |
 | 端点联调 | 真实登录 token → 页面操作 → 断言 body code + 结构 | 结构不符先查后端路由重复注册 |

+ 72 - 0
eslint.config.js

@@ -0,0 +1,72 @@
+// ESLint 9 flat config:vue3 + TS strict 配套(README §3.5:any 全面禁用)
+import js from '@eslint/js';
+import tseslint from 'typescript-eslint';
+import pluginVue from 'eslint-plugin-vue';
+import prettier from 'eslint-config-prettier';
+
+/**
+ * 框架运行时模块名(业务代码禁"值 import",见 README §3.3-1)。
+ *
+ * 精确边界:
+ *   - `import type { Ref } from 'vue'` 属**类型导入**(编译期擦除、零运行时足迹)——
+ *     不在限内,TS strict 下类型必须有来源,type import 是唯一合规写法;
+ *   - `import { ref } from 'vue'` 属**值导入**——会把框架运行时打包进 dist,必须拦。
+ *
+ * 实现用 `no-restricted-syntax`(而非 no-restricted-imports,后者不区分 type/value):
+ *   `importKind` 属性仅 @typescript-eslint/parser 产生:
+ *     import type {...} → 'type'(放行)  |  import {...} → 'value'(拦截)
+ */
+const FRAMEWORK_MODULE_SELECTOR =
+  ":is([source.value='vue'],[source.value='vue-router'],[source.value='pinia'],[source.value='axios'],[source.value='element-plus'])";
+
+export default [
+  js.configs.recommended,
+  ...tseslint.configs.recommended,
+  ...pluginVue.configs['flat/essential'],
+  prettier,
+  {
+    // shims/ 由 scripts/gen-shims.cjs 生成(含逐行 eslint-disable 注释,引用了未安装的 import 插件),不 lint
+    // scripts/ 是 Node CJS 工具脚本(require / __dirname),不属于浏览器 TS 产物,不 lint
+    ignores: ['dist/**', 'vendor/**', 'node_modules/**', 'shims/**', 'scripts/**', '**/*.cjs'],
+  },
+  {
+    // .vue:SFC 内 <script lang="ts"> 交给 @typescript-eslint/parser,
+    // 否则 TS 语法(类型注解 / interface / 泛型)在 SFC 内触发 Parsing error。
+    // vue-eslint-parser 解析 SFC 结构,本 block 只补"内部 script 用哪个 parser"。
+    files: ['**/*.vue'],
+    languageOptions: {
+      parserOptions: {
+        parser: tseslint.parser,
+        sourceType: 'module',
+      },
+    },
+  },
+  {
+    // TS 项目里 no-undef 必须由 TS 编译器负责(global.d.ts 声明的 ref/reactive/window 等
+    // 对 JS 规则的 no-undef 不可见,会误报);关掉它,TS 编译器 + 上面的"值 import 拦截"已足够。
+    files: ['**/*.ts', '**/*.vue', '**/*.tsx'],
+    rules: {
+      'no-undef': 'off',
+    },
+  },
+  {
+    rules: {
+      '@typescript-eslint/no-explicit-any': 'error',
+      'vue/multi-word-component-names': 'off',
+      '@typescript-eslint/no-unused-vars': [
+        'error',
+        { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
+      ],
+      // 拦截"值 import 框架"(值导入会把框架运行时打包进 dist,违反 README §3.3-1)。
+      // 放行 import type(编译期擦除)。importKind='value' 只匹配值导入。
+      'no-restricted-syntax': [
+        'error',
+        {
+          selector: `ImportDeclaration[importKind='value']${FRAMEWORK_MODULE_SELECTOR}`,
+          message:
+            '框架运行时经 UMD 全局使用(README §3.3-1),业务代码禁止值 import 框架模块;仅允许 `import type`(编译期擦除,零运行时足迹)。',
+        },
+      ],
+    },
+  },
+];

+ 93 - 0
global.d.ts

@@ -0,0 +1,93 @@
+/**
+ * globals.d.ts —— UMD 全局变量的 TypeScript 声明(README §3.3-1「禁 import 框架」的类型侧配套)。
+ *
+ * 背景:框架运行时全部走 index.html 引入的 UMD 全局(Vue / VueRouter / Pinia / ElementPlus / axios),
+ * 业务代码不 import 框架模块。
+ * 落点说明:本文件放**仓库根**(而非 src/types)——shims/ 目录在 src 之外,
+ * 若声明放 src 内则 shims 文件不生效(全局声明只对 include 范围生效)。
+ * 本文件做两件事:
+ *   ① 声明 5 个 UMD 全局常量(typeof 引自已安装的同版本 dev 依赖,类型与运行时一一对应);
+ *   ② 声明 vite define 注入的组合式 API 裸标识符(ref / computed / onMounted …),
+ *      使业务代码里"裸写 ref()"既能通过类型检查、又能在运行时命中 Vue 全局。
+ *
+ * 版本纪律:dev 依赖版本必须与 index.html 头部"vendor 版本表"保持一致(升级走 README §1.5 五步流程)。
+ */
+import type { AxiosStatic } from 'axios';
+import type { Language } from 'element-plus';
+import type * as PiniaNS from 'pinia';
+import type * as VueNS from 'vue';
+import type * as VueRouterNS from 'vue-router';
+
+declare global {
+  // —— UMD 全局常量(index.html 按序注入)——
+  const Vue: typeof VueNS;
+  /**
+   * vue-demi 桥接全局(vendor/vue-demi@0.14.10 注入):读 globalThis.Vue 后把 Vue API
+   * 镜像到本全局。纯 Vue3 下等价于 re-export Vue;业务代码不直接引用,
+   * 仅供 pinia IIFE 构建内部取用(pinia 官方构建硬依赖 VueDemi 全局)。声明仅为全局清单完整。
+   */
+  var VueDemi: typeof VueNS;
+  const VueRouter: typeof VueRouterNS;
+  const Pinia: typeof PiniaNS;
+  /** var 声明(而非 const):允许 globalThis.ElementPlus 属性式访问(http.ts 拦截器内)+ 模板回退解析 */
+  var ElementPlus: typeof import('element-plus');
+  /** Element Plus 简体中文 locale(vendor/element-plus@2.14.7/locale/zh-cn.min.js 注入) */
+  var ElementPlusLocaleZhCn: Language;
+  /** UMD 全局 axios 是**实例**(root.axios = 默认实例,含 post/get/create),类型 = AxiosStatic */
+  var axios: AxiosStatic;
+
+  // —— vite define 注入的组合式 API 裸标识符(vite.config.ts VUE_APIS 清单与之一一对应)——
+  const ref: typeof VueNS.ref;
+  const shallowRef: typeof VueNS.shallowRef;
+  const reactive: typeof VueNS.reactive;
+  const shallowReactive: typeof VueNS.shallowReactive;
+  const readonly: typeof VueNS.readonly;
+  const shallowReadonly: typeof VueNS.shallowReadonly;
+  const computed: typeof VueNS.computed;
+  const watch: typeof VueNS.watch;
+  const watchEffect: typeof VueNS.watchEffect;
+  const watchPostEffect: typeof VueNS.watchPostEffect;
+  const watchSyncEffect: typeof VueNS.watchSyncEffect;
+  const effectScope: typeof VueNS.effectScope;
+  const customRef: typeof VueNS.customRef;
+  const triggerRef: typeof VueNS.triggerRef;
+  const toRef: typeof VueNS.toRef;
+  const toRefs: typeof VueNS.toRefs;
+  const toRaw: typeof VueNS.toRaw;
+  const toValue: typeof VueNS.toValue;
+  const markRaw: typeof VueNS.markRaw;
+  const isRef: typeof VueNS.isRef;
+  const isReactive: typeof VueNS.isReactive;
+  const isReadonly: typeof VueNS.isReadonly;
+  const isVNode: typeof VueNS.isVNode;
+  const unref: typeof VueNS.unref;
+  const onMounted: typeof VueNS.onMounted;
+  const onBeforeMount: typeof VueNS.onBeforeMount;
+  const onUnmounted: typeof VueNS.onUnmounted;
+  const onBeforeUnmount: typeof VueNS.onBeforeUnmount;
+  const onUpdated: typeof VueNS.onUpdated;
+  const onBeforeUpdate: typeof VueNS.onBeforeUpdate;
+  const onActivated: typeof VueNS.onActivated;
+  const onDeactivated: typeof VueNS.onDeactivated;
+  const onErrorCaptured: typeof VueNS.onErrorCaptured;
+  const onRenderTracked: typeof VueNS.onRenderTracked;
+  const onRenderTriggered: typeof VueNS.onRenderTriggered;
+  const onScopeDispose: typeof VueNS.onScopeDispose;
+  const nextTick: typeof VueNS.nextTick;
+  const createApp: typeof VueNS.createApp;
+  const inject: typeof VueNS.inject;
+  const provide: typeof VueNS.provide;
+  const h: typeof VueNS.h;
+  // —— Vue Router(define 注入为 VueRouter.*)——
+  const createRouter: typeof VueRouterNS.createRouter;
+  const createWebHashHistory: typeof VueRouterNS.createWebHashHistory;
+  const createWebHistory: typeof VueRouterNS.createWebHistory;
+  const useRoute: typeof VueRouterNS.useRoute;
+  const useRouter: typeof VueRouterNS.useRouter;
+  // —— Pinia(define 注入为 Pinia.*)——
+  const defineStore: typeof PiniaNS.defineStore;
+  const createPinia: typeof PiniaNS.createPinia;
+  const storeToRefs: typeof PiniaNS.storeToRefs;
+}
+
+export {};

+ 41 - 0
index.html

@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>统一管理平台</title>
+    <!--
+      ============================================================
+      vendor 版本表(框架层 · UMD 全局 · 自托管 OSS)
+      ============================================================
+      库            版本        文件
+      vue           3.5.45      vendor/vue@3.5.45/vue.global.prod.js
+      vue-demi      0.14.10     vendor/vue-demi@0.14.10/vue-demi.iife.js
+      vue-router    4.6.4       vendor/vue-router@4.6.4/vue-router.global.prod.js
+      pinia         2.3.1       vendor/pinia@2.3.1/pinia.iife.prod.js
+      element-plus  2.14.7      vendor/element-plus@2.14.7/index.css
+                                vendor/element-plus@2.14.7/index.full.min.js
+                                vendor/element-plus@2.14.7/locale/zh-cn.min.js
+      axios         1.21.0      vendor/axios@1.21.0/axios.min.js
+
+      加载顺序固定(依赖在前):vue → vue-demi → vue-router → pinia → EP CSS → EP JS → EP locale → axios
+      注:vue-demi 是 pinia 的桥接层——pinia IIFE 构建硬依赖 VueDemi 全局(官方构建写死 }({}, VueDemi)),
+          vue-demi 读 globalThis.Vue 后把 Vue API 镜像到 VueDemi 全局(Vue3 下即 re-export Vue)。
+          漏加载 vue-demi 会导致 pinia 报 "VueDemi is not defined",store 全崩。
+      生产环境 OSS 桶布局与仓库 vendor/ 完全一致;nginx location /vendor/ → OSS 桶根(或同机静态目录)。
+      升级框架:执行 README §1.5 五步流程,OSS 路径只增不删。
+    -->
+    <link rel="stylesheet" href="vendor/element-plus@2.14.7/index.css" />
+    <script src="vendor/vue@3.5.45/vue.global.prod.js"></script>
+    <script src="vendor/vue-demi@0.14.10/vue-demi.iife.js"></script>
+    <script src="vendor/vue-router@4.6.4/vue-router.global.prod.js"></script>
+    <script src="vendor/pinia@2.3.1/pinia.iife.prod.js"></script>
+    <script src="vendor/element-plus@2.14.7/index.full.min.js"></script>
+    <script src="vendor/element-plus@2.14.7/locale/zh-cn.min.js"></script>
+    <script src="vendor/axios@1.21.0/axios.min.js"></script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 32 - 0
package.json

@@ -0,0 +1,32 @@
+{
+  "name": "unified-pc-frontend",
+  "version": "0.1.0",
+  "private": true,
+  "description": "统一 Go 后端 PC 管理端(教务域重写 + 教学 admin 复用)——框架层 UMD 自托管 OSS,业务层本地编译",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "typecheck": "vue-tsc --noEmit",
+    "lint": "eslint ."
+  },
+  "packageManager": "pnpm@10.12.1+sha1.4bd977ce548dc9fcd1ca0c62838c55646cea6847",
+  "devDependencies": {
+    "@eslint/js": "^9.39.4",
+    "@vitejs/plugin-vue": "^6.0.10",
+    "axios": "1.21.0",
+    "dayjs": "1.11.23",
+    "element-plus": "2.14.7",
+    "eslint": "^9.39.4",
+    "eslint-config-prettier": "^10.1.8",
+    "eslint-plugin-vue": "^10.12.0",
+    "pinia": "2.3.1",
+    "prettier": "^3.9.10",
+    "typescript": "~5.8.3",
+    "typescript-eslint": "^8.72.1",
+    "vite": "^6.4.5",
+    "vue": "3.5.45",
+    "vue-router": "4.6.4",
+    "vue-tsc": "^3.3.14"
+  }
+}

+ 2427 - 0
pnpm-lock.yaml

@@ -0,0 +1,2427 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    devDependencies:
+      '@eslint/js':
+        specifier: ^9.39.4
+        version: 9.39.4
+      '@vitejs/plugin-vue':
+        specifier: ^6.0.10
+        version: 6.0.10(vite@6.4.5)(vue@3.5.45(typescript@5.8.3))
+      axios:
+        specifier: 1.21.0
+        version: 1.21.0
+      dayjs:
+        specifier: 1.11.23
+        version: 1.11.23
+      element-plus:
+        specifier: 2.14.7
+        version: 2.14.7(vue@3.5.45(typescript@5.8.3))
+      eslint:
+        specifier: ^9.39.4
+        version: 9.39.4
+      eslint-config-prettier:
+        specifier: ^10.1.8
+        version: 10.1.8(eslint@9.39.4)
+      eslint-plugin-vue:
+        specifier: ^10.12.0
+        version: 10.12.0(@typescript-eslint/parser@8.72.1(eslint@9.39.4)(typescript@5.8.3))(eslint@9.39.4)(vue-eslint-parser@10.4.1(eslint@9.39.4))
+      pinia:
+        specifier: 2.3.1
+        version: 2.3.1(typescript@5.8.3)(vue@3.5.45(typescript@5.8.3))
+      prettier:
+        specifier: ^3.9.10
+        version: 3.9.10
+      typescript:
+        specifier: ~5.8.3
+        version: 5.8.3
+      typescript-eslint:
+        specifier: ^8.72.1
+        version: 8.72.1(eslint@9.39.4)(typescript@5.8.3)
+      vite:
+        specifier: ^6.4.5
+        version: 6.4.5
+      vue:
+        specifier: 3.5.45
+        version: 3.5.45(typescript@5.8.3)
+      vue-router:
+        specifier: 4.6.4
+        version: 4.6.4(vue@3.5.45(typescript@5.8.3))
+      vue-tsc:
+        specifier: ^3.3.14
+        version: 3.3.14(typescript@5.8.3)
+
+packages:
+
+  '@babel/helper-string-parser@7.29.13':
+    resolution: {integrity: sha512-fWegr+WsEsiMVW4KUNdIIzzMTZSzNiqhy0//6+2MRi2d5NFNsL/AYwBQW8eBnRbTBVRnDVzjaJEUqKaCWjMMdQ==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-validator-identifier@7.29.7':
+    resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/parser@7.29.13':
+    resolution: {integrity: sha512-fJM50YYjlTiBhgdkGmC1tBpv+FMAyo4annf2X6Aks+0yRlaiVkYW/3f07xAfVVkYIoZ9fFZp+mEThUFnuybhkQ==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+
+  '@babel/types@7.29.11':
+    resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==}
+    engines: {node: '>=6.9.0'}
+
+  '@ctrl/tinycolor@4.2.2':
+    resolution: {integrity: sha512-9rQHijqB2Rq6qOEC/xHaA6aPlsKGi5kmCQW50O1asn9g63XFeD5NmBwysriistW5Xx0JPafyD/TA7yk2S3pXGQ==}
+    engines: {node: '>=14'}
+
+  '@element-plus/icons-vue@2.3.2':
+    resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==}
+    peerDependencies:
+      vue: ^3.2.0
+
+  '@esbuild/aix-ppc64@0.25.12':
+    resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/android-arm64@0.25.12':
+    resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm@0.25.12':
+    resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-x64@0.25.12':
+    resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/darwin-arm64@0.25.12':
+    resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.25.12':
+    resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/freebsd-arm64@0.25.12':
+    resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.25.12':
+    resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/linux-arm64@0.25.12':
+    resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.25.12':
+    resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.25.12':
+    resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.25.12':
+    resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.25.12':
+    resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.25.12':
+    resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.25.12':
+    resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.25.12':
+    resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.25.12':
+    resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/netbsd-arm64@0.25.12':
+    resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.25.12':
+    resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-arm64@0.25.12':
+    resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.25.12':
+    resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openharmony-arm64@0.25.12':
+    resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@esbuild/sunos-x64@0.25.12':
+    resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/win32-arm64@0.25.12':
+    resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.25.12':
+    resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.25.12':
+    resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
+  '@eslint-community/eslint-utils@4.10.1':
+    resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
+    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.2':
+    resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+    engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+  '@eslint/config-array@0.21.2':
+    resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/config-helpers@0.4.2':
+    resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/core@0.17.0':
+    resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/eslintrc@3.3.7':
+    resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/js@9.39.4':
+    resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/object-schema@2.1.7':
+    resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/plugin-kit@0.4.1':
+    resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@floating-ui/core@1.8.0':
+    resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
+
+  '@floating-ui/dom@1.8.0':
+    resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
+
+  '@floating-ui/utils@0.2.12':
+    resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
+
+  '@humanfs/core@0.19.2':
+    resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanfs/node@0.16.8':
+    resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanfs/types@0.15.0':
+    resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+    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'}
+
+  '@jridgewell/sourcemap-codec@1.6.0':
+    resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+  '@rolldown/pluginutils@1.0.1':
+    resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+  '@rollup/rollup-android-arm-eabi@4.67.5':
+    resolution: {integrity: sha512-FdWPeulkH+FH+rBzyKBD59tuVN4tRKvmVilSJm0OcnoYsQTpd9UhD78OaPcsYqo2fX6hCwAG0YIjrDLoQ3pR3g==}
+    cpu: [arm]
+    os: [android]
+
+  '@rollup/rollup-android-arm64@4.67.5':
+    resolution: {integrity: sha512-bmkDuBGnF+Ji8atWlXcNjxIC52ShCbdXPbW7Nz0lRoCFi1lrvsO0XobJY36kTj7duc1vOmGJeQ9vtVuGjnzihA==}
+    cpu: [arm64]
+    os: [android]
+
+  '@rollup/rollup-android-x64@4.67.5':
+    resolution: {integrity: sha512-6KvVfnqPTqmbQ5YobMl9HYiSymAsCt8HlNeriDshwJQcveXwT+vIMW6wGL1RcJbXH9+r3BAJvjab5+rIgtSEqA==}
+    cpu: [x64]
+    os: [android]
+
+  '@rollup/rollup-darwin-arm64@4.67.5':
+    resolution: {integrity: sha512-SNamgdrY69oPdBrQV9fKGo6PSmKcVhyn0D0jnDmcqgRJvrxQGGBhtekOHTUJ7TCgkW0nE8XMH9v2CwXmjNIzVA==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@rollup/rollup-darwin-x64@4.67.5':
+    resolution: {integrity: sha512-HNdl34thiJtHDSglogjmAlQ296L8cc8zA7ODH65nnmY7E+jPP55WEbh8J5583rWFLMjxyYywxZuwPOwRvEi5tA==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@rollup/rollup-freebsd-arm64@4.67.5':
+    resolution: {integrity: sha512-WvZYtANNswfHzSNHaJAoG6Ipfc4SyZIDYuIjFdHv07xXxhHl+Xx8o+bNDWsxpOADFT2mKXcXVrfOG4OlWTLo8Q==}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@rollup/rollup-freebsd-x64@4.67.5':
+    resolution: {integrity: sha512-EUU+Wgf9NyHekceC+DxAdm42f6bq2ezH/7DEb/JnCb8SAUhG2xph5BiVukxplWal2m5pP8CzJZE+FLwRopywjQ==}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.67.5':
+    resolution: {integrity: sha512-dgU1CvUuE2IkaJBAeuAbxpDdMHAdZBT5oth5u22DtuoMg4gXXYr2+Sm30RigPJG1F2VEoiGJUziGLuIh9+jPxA==}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm-musleabihf@4.67.5':
+    resolution: {integrity: sha512-rdgJcJig2VngYlcTCRQw2BA8kLWM6QwDFe9uLFo81lya0jPC1r8wZu92sAvTUIv48GWlkRVi6/fpsa5CoFSKoQ==}
+    cpu: [arm]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-arm64-gnu@4.67.5':
+    resolution: {integrity: sha512-+jtCiQnrEFHWAUAcUpP7wppb1p6HKOgBP3mXx75cEYpDF2Y5MEqBky2HNz+osFuVx5uX44gnMTmbe5RJauzn6A==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm64-musl@4.67.5':
+    resolution: {integrity: sha512-heflX7ENhL8eHmLcUxDEHCSK8CKLyPprysb2JGoSTh5Ip7nHrzIRc+2USyxul3nxi98H8pGxJIEDEpURlS1X5A==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-loong64-gnu@4.67.5':
+    resolution: {integrity: sha512-rRtNU0zBfRHkJ8pscHN9qRtUYLq8Rao4EX7aVWvKcFmJUANkCCXFRU7ovNBEMhpEONyUEB/3DRVDXwDDguSfHA==}
+    cpu: [loong64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-loong64-musl@4.67.5':
+    resolution: {integrity: sha512-/6auyi9yj8MD/Q+8P+f2ay57jQ7a0j7Yp1JK86pecH6ZnRViQZ5GOjoRSjGArFeTSHFznRbM2QPQcBgDPyNMfQ==}
+    cpu: [loong64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-ppc64-gnu@4.67.5':
+    resolution: {integrity: sha512-Hsc0vlt+vPvqzUkHKffWKf3YE/yexrY+yeFf7IsUvDC6PV7YGCsA/gJvPa+WJdgxGJnwuoO1HmLkbahaLKabYQ==}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-ppc64-musl@4.67.5':
+    resolution: {integrity: sha512-Jx5TMMSPJwHY7F/JwPdS6nuBwny2ECRK+EsAJV5HZrfiddeSS6ZrohkLkFZm3rjqBUuQ4FPX5gGMtakUuQO6fQ==}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-riscv64-gnu@4.67.5':
+    resolution: {integrity: sha512-fGWcq2lo/XCqAkBLOokg66D/z+MUouryB8i+QdU9RR6O5knG7rucc0WFPqWYL1Wk/wuiJouDfmyBxmZKRM9bag==}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-riscv64-musl@4.67.5':
+    resolution: {integrity: sha512-JwO/Ys3jybc0UW3V5XO2CMnp0RhvoczDDQTgAEE1IctT+HRY3W5parIIWLUpmAK4gHSeeYJofm2iQPFA62K6ag==}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-s390x-gnu@4.67.5':
+    resolution: {integrity: sha512-vVD4iqel6J6A1xAJlb4mxEO9i1wOJ4/SkcKoDKAuK/wqEBPOA7/P5dDyZvNx46ocHwOUwzo3WjhTdKqET9kEAg==}
+    cpu: [s390x]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-gnu@4.67.5':
+    resolution: {integrity: sha512-HmEfNh5Cdd/NHxgqwMzKnjlvicQQA+D+w+vQXDJSV0x/aHiQSjVwh1Th5RoA7QsMieIaxibt/5iJq68BraTSUg==}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-musl@4.67.5':
+    resolution: {integrity: sha512-NfmALpbe009nYzZ2QqlSW6nzbGddXpqk3nwualrIr+4QflQCTGMzt3QKK20yARiAnSV9fJdM5WNwIYUf31x3uA==}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-openbsd-x64@4.67.5':
+    resolution: {integrity: sha512-ngcE7Udpy/iJ3lexVhYcbpUV2OJIakRGus3snc6K52BnAAJetDBG1Ku5Eh3CECtnW3mpBYWg5VCD4tTaOXKOVQ==}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@rollup/rollup-openharmony-arm64@4.67.5':
+    resolution: {integrity: sha512-DZ01A2EdTYX8xVeGoGiWkJBRWDh+/BOflwGPVsBay43PDxcq8ez0xuovZnPnk1xjE3CdI0pdZXxsJp/Xjmf7uQ==}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@rollup/rollup-win32-arm64-msvc@4.67.5':
+    resolution: {integrity: sha512-nJKiMKDO140DUCEcQZl+f4HiGPMBxrW2webjuWJ2E3qPeoruHf2zByml4TP52lA1zDi6L4I92D/5stxeA/6pig==}
+    cpu: [arm64]
+    os: [win32]
+
+  '@rollup/rollup-win32-ia32-msvc@4.67.5':
+    resolution: {integrity: sha512-ykLkBCf+Kls24QmqM3IRf1T4sGMh0rXPyymtj+hXlWKPJnXbXraHqUUsKtOIxqLasjT1i7DhfQGAaZYeiHbSOw==}
+    cpu: [ia32]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-gnu@4.67.5':
+    resolution: {integrity: sha512-NiFPUFw8cHhkmBlL1k1eaz+5AXXm89rztj+DQdDuQ7VGPM0+ADFhezQ2KcBMF50UEFzGUr0+DVjVND743XwKWQ==}
+    cpu: [x64]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-msvc@4.67.5':
+    resolution: {integrity: sha512-Khj/vrKCpdbiAWYbJAeSy4D5iep+qwW/86lNoKvdleJYGxgbRE7h87+5d5T9D7gt8NcqhNFpreRgIg8T4bLzKQ==}
+    cpu: [x64]
+    os: [win32]
+
+  '@sxzz/popperjs-es@2.11.8':
+    resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
+
+  '@types/esrecurse@4.3.1':
+    resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
+
+  '@types/estree@1.0.10':
+    resolution: {integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==}
+
+  '@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.27':
+    resolution: {integrity: sha512-VenMRv/eKNrspX9v3H4dbmjsrnCOYdYBG3kxXKk3dG6X+urBGcj2Prww3xw1U5K4FV/mE4h5GbWd0szEKf+9ew==}
+
+  '@types/web-bluetooth@0.0.21':
+    resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
+
+  '@typescript-eslint/eslint-plugin@8.72.1':
+    resolution: {integrity: sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      '@typescript-eslint/parser': ^8.72.1
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/parser@8.72.1':
+    resolution: {integrity: sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/project-service@8.72.1':
+    resolution: {integrity: sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/scope-manager@8.72.1':
+    resolution: {integrity: sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/tsconfig-utils@8.72.1':
+    resolution: {integrity: sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/type-utils@8.72.1':
+    resolution: {integrity: sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/types@8.72.1':
+    resolution: {integrity: sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/typescript-estree@8.72.1':
+    resolution: {integrity: sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/utils@8.72.1':
+    resolution: {integrity: sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  '@typescript-eslint/visitor-keys@8.72.1':
+    resolution: {integrity: sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@vitejs/plugin-vue@6.0.10':
+    resolution: {integrity: sha512-b80aUQh3n+qEzzZm5oQp3rT2QImeCx58K6eFl/RkJ3d65KW0kTuv5lNsLRVAjbW9YRUpo/wzmvzMgV34bTAItA==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    peerDependencies:
+      vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+      vue: ^3.2.25 || ^3.6.0-rc.10
+
+  '@volar/language-core@2.4.30':
+    resolution: {integrity: sha512-73fjwEV8wat6aOZ6U/y/hqtE30ocrIWECg67b0txGczOMNw+MffLmvJ5fnFfN9drVUEYZzLdaNo6VAy6G6avMw==}
+
+  '@volar/language-service@2.4.30':
+    resolution: {integrity: sha512-w/xDsASiqC71ggCOm7sTDXDNk/l72XEptodeRC68tgZ0e5QM4hM3zAqBGC7mBctvhrpYwKfMG+t9xcUo2hEFtA==}
+    peerDependencies:
+      typescript: <7
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@volar/source-map@2.4.30':
+    resolution: {integrity: sha512-zym1ob/KwDDyN+0jGVtxwbxJSRRnUvv5HspCtz3yEfVVdCG4/dzxJktMGPAgemqRuVs81O0o/y0q3qOUErR8kg==}
+
+  '@volar/typescript@2.4.30':
+    resolution: {integrity: sha512-/DgA5tgYFPfrLFuSjIMXMGHk1LQv2+P8eny6+BhGNzFJELWIW7UZlc56ZLIRFhLsPpMfIFxM8/HvOPBggrjTZg==}
+    peerDependencies:
+      typescript: <7
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@vue/compiler-core@3.5.45':
+    resolution: {integrity: sha512-XGHJrM6ufZARAc/w+tyOswStw9tdKiqCKBScI4gGaNxD+aaGu/HQWgKJ+JItjRLPqenQ7iE7G8AJC7T5ew1p+g==}
+
+  '@vue/compiler-dom@3.5.45':
+    resolution: {integrity: sha512-LbP2d19GSOYGPmvMBBCVJVlv7TKtjM1Cfeyu3cYia720pknZP+1cLX7H5MVatvruT6MuJhiPuNbQBW/MDn/EwA==}
+
+  '@vue/compiler-sfc@3.5.45':
+    resolution: {integrity: sha512-MKFjb2UX0M/l/jhfzqMLrI8VM4pfvFKG3pqFZkSZFHRLtNld/O4RtUPZFWSwhro/14qP88UQEkWzHxAZAQ0tsw==}
+
+  '@vue/compiler-ssr@3.5.45':
+    resolution: {integrity: sha512-RF5sbZOfYz5AK00tOJUn2PwFXR2fS1u2HVzI8D9VF9rGAFxuGBqg2bB1kJ3ZHMprqSltXnT9MZE1IsqjSM8oew==}
+
+  '@vue/devtools-api@6.6.4':
+    resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
+
+  '@vue/language-core@3.3.14':
+    resolution: {integrity: sha512-kRB+3hHg0c+BdD1XLAvwjVMQd5pC05zijSlPDRI3ERvC/s04JlRs5upy2KjeMg/9m9oD5zuMVh9yVvkKfZqoJg==}
+
+  '@vue/reactivity@3.5.45':
+    resolution: {integrity: sha512-WqkyubNWpI4OYBOipSrjIssMdJO8K1i9Zts/Wm8Er10/0msOEwCyWWWvZnAI7WW3kQsTong6DJp8Cbb+vd4cPA==}
+
+  '@vue/runtime-core@3.5.45':
+    resolution: {integrity: sha512-bzIO3+FABjpj85uIKSc+Qnj36/sxDNI3tcH9i6w6xKA1k6e0ejv0G0jMCfeJBvK3yGL5XbJ3DMZj+ClqyCHFYw==}
+
+  '@vue/runtime-dom@3.5.45':
+    resolution: {integrity: sha512-ZvRTFMDgfqTDYWvJfoVpQTJx1RN5M6l7Pyd6wfdY2NaXzbPQVh9ezeQYFbNPP6bIcRVn2SqSSR25N/jSW69BFw==}
+
+  '@vue/server-renderer@3.5.45':
+    resolution: {integrity: sha512-1fNXUHhhsWf4EVL4XixbGBU5mKdiIXJAB82feHRTyPPo6ZUEYWIJuEeUl3mVV6QDBp50HJOX4obLhxYXlrjrzA==}
+
+  '@vue/shared@3.5.45':
+    resolution: {integrity: sha512-ue9fgHmVNPlP7iW3+cxoBX/Cnlw53dHlVSw0JKGY6z1lObg8pHoj928M2nzG4/yVsMZ3NaRZ2s2z61uC16NDwQ==}
+
+  '@vueuse/core@15.0.0':
+    resolution: {integrity: sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==}
+    engines: {node: '>=22'}
+    peerDependencies:
+      vue: ^3.5.0
+
+  '@vueuse/metadata@15.0.0':
+    resolution: {integrity: sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==}
+    engines: {node: '>=22'}
+
+  '@vueuse/shared@15.0.0':
+    resolution: {integrity: sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==}
+    engines: {node: '>=22'}
+    peerDependencies:
+      vue: ^3.5.0
+
+  acorn-jsx@5.3.2:
+    resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+  acorn@8.19.0:
+    resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
+  agent-base@6.0.2:
+    resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+    engines: {node: '>= 6.0.0'}
+
+  ajv@6.15.0:
+    resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+  alien-signals@3.2.1:
+    resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
+
+  ansi-styles@4.3.0:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  argparse@2.0.1:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+  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.21.0:
+    resolution: {integrity: sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==}
+
+  balanced-match@1.0.2:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+  balanced-match@4.0.4:
+    resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+    engines: {node: 18 || 20 || >=22}
+
+  boolbase@1.0.0:
+    resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+  brace-expansion@1.1.21:
+    resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
+
+  brace-expansion@5.0.12:
+    resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
+    engines: {node: 20 || >=22}
+
+  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'}
+
+  chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
+  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==}
+
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
+  concat-map@0.0.1:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+  cross-spawn@7.0.6:
+    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+    engines: {node: '>= 8'}
+
+  cssesc@3.0.0:
+    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+    engines: {node: '>=4'}
+    hasBin: true
+
+  csstype@3.2.3:
+    resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+  dayjs@1.11.23:
+    resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==}
+
+  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==}
+
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
+  dunder-proto@1.0.1:
+    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+    engines: {node: '>= 0.4'}
+
+  element-plus@2.14.7:
+    resolution: {integrity: sha512-lrG7pZyCS+mpCNVpzxxyu5r35+T6V0hzkAeIwn7HA6to772XSTHKuYCZG51OfzlqaxGbNegLxs650zAF1j43gA==}
+    peerDependencies:
+      vue: ^3.3.7
+
+  entities@7.0.1:
+    resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+    engines: {node: '>=0.12'}
+
+  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-object-atoms@1.1.2:
+    resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
+    engines: {node: '>= 0.4'}
+
+  es-set-tostringtag@2.1.0:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
+  esbuild@0.25.12:
+    resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  escape-string-regexp@4.0.0:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+    engines: {node: '>=10'}
+
+  eslint-config-prettier@10.1.8:
+    resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
+    hasBin: true
+    peerDependencies:
+      eslint: '>=7.0.0'
+
+  eslint-plugin-vue@10.12.0:
+    resolution: {integrity: sha512-YsrHHtMaxjWqmxr/Kv9yXUfakUlbTCWtIBvr9eounzj6ZsGJdz1I4UuTIHkhzKHes793Cwo3PGTcvsoBqNyzCA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
+      '@typescript-eslint/parser': ^7.0.0 || ^8.0.0
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      vue-eslint-parser: ^10.3.0
+    peerDependenciesMeta:
+      '@stylistic/eslint-plugin':
+        optional: true
+      '@typescript-eslint/parser':
+        optional: true
+
+  eslint-scope@8.4.0:
+    resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  eslint-scope@9.1.2:
+    resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
+    engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+  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-visitor-keys@5.0.1:
+    resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+    engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+  eslint@9.39.4:
+    resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==}
+    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@11.2.0:
+    resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
+    engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+  esquery@1.7.0:
+    resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+    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==}
+
+  esutils@2.0.3:
+    resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+    engines: {node: '>=0.10.0'}
+
+  fast-deep-equal@3.1.3:
+    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+  fast-json-stable-stringify@2.1.0:
+    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+  fast-levenshtein@2.0.6:
+    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+  fdir@6.5.0:
+    resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+    engines: {node: '>=12.0.0'}
+    peerDependencies:
+      picomatch: ^3 || ^4
+    peerDependenciesMeta:
+      picomatch:
+        optional: true
+
+  file-entry-cache@8.0.0:
+    resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+    engines: {node: '>=16.0.0'}
+
+  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'}
+
+  flatted@3.4.4:
+    resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
+
+  follow-redirects@1.16.2:
+    resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
+  form-data@4.0.6:
+    resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
+    engines: {node: '>= 6'}
+
+  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==}
+
+  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'}
+
+  glob-parent@6.0.2:
+    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+    engines: {node: '>=10.13.0'}
+
+  globals@14.0.0:
+    resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+    engines: {node: '>=18'}
+
+  gopd@1.2.0:
+    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+    engines: {node: '>= 0.4'}
+
+  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.4:
+    resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
+    engines: {node: '>= 0.4'}
+
+  https-proxy-agent@5.0.1:
+    resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+    engines: {node: '>= 6'}
+
+  ignore@5.3.2:
+    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+    engines: {node: '>= 4'}
+
+  ignore@7.0.14:
+    resolution: {integrity: sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==}
+    engines: {node: '>= 4'}
+
+  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'}
+
+  is-extglob@2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+
+  is-glob@4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+
+  isexe@2.0.0:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+  js-yaml@4.3.2:
+    resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
+    hasBin: true
+
+  json-buffer@3.0.1:
+    resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+  json-schema-traverse@0.4.1:
+    resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+  json-stable-stringify-without-jsonify@1.0.1:
+    resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+  keyv@4.5.4:
+    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+  levn@0.4.1:
+    resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+    engines: {node: '>= 0.8.0'}
+
+  locate-path@6.0.0:
+    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+    engines: {node: '>=10'}
+
+  lodash-es@4.18.1:
+    resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
+
+  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@4.18.1:
+    resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
+
+  magic-string@0.30.21:
+    resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+  math-intrinsics@1.1.0:
+    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+    engines: {node: '>= 0.4'}
+
+  memoize-one@6.0.0:
+    resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+
+  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@10.2.6:
+    resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
+    engines: {node: 18 || 20 || >=22}
+
+  minimatch@3.1.5:
+    resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+
+  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.20:
+    resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  natural-compare@1.4.0:
+    resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+  normalize-wheel-es@1.2.0:
+    resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
+
+  nth-check@2.1.1:
+    resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+  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'}
+
+  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'}
+
+  picocolors@1.1.1:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+  picomatch@4.0.7:
+    resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+    engines: {node: '>=12'}
+
+  pinia@2.3.1:
+    resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==}
+    peerDependencies:
+      typescript: '>=4.4.4'
+      vue: ^2.7.0 || ^3.5.11
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  postcss-selector-parser@7.1.7:
+    resolution: {integrity: sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==}
+    engines: {node: '>=4'}
+
+  postcss@8.5.32:
+    resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==}
+    engines: {node: ^10 || ^12 || >=14}
+
+  prelude-ls@1.2.1:
+    resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+    engines: {node: '>= 0.8.0'}
+
+  prettier@3.9.10:
+    resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
+    engines: {node: '>=14'}
+    hasBin: true
+
+  proxy-from-env@2.1.0:
+    resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
+    engines: {node: '>=10'}
+
+  punycode@2.3.1:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+    engines: {node: '>=6'}
+
+  resolve-from@4.0.0:
+    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+    engines: {node: '>=4'}
+
+  rollup@4.67.5:
+    resolution: {integrity: sha512-3Ve5VH1hacCVutdpbmu/htg4kz+JCOIWgy4Q3hjHPYGHeHeIElNHJ0LcJ3rCfIsd5G7qOl5k5mwpzGMj9kdHPA==}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+    hasBin: true
+
+  semver@7.8.6:
+    resolution: {integrity: sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==}
+    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'}
+
+  source-map-js@1.2.2:
+    resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
+    engines: {node: '>=0.10.0'}
+
+  strip-json-comments@3.1.1:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+    engines: {node: '>=8'}
+
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  tinyglobby@0.2.17:
+    resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+    engines: {node: '>=12.0.0'}
+
+  ts-api-utils@2.5.0:
+    resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+    engines: {node: '>=18.12'}
+    peerDependencies:
+      typescript: '>=4.8.4'
+
+  type-check@0.4.0:
+    resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+    engines: {node: '>= 0.8.0'}
+
+  typescript-eslint@8.72.1:
+    resolution: {integrity: sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+      typescript: '>=4.8.4 <6.1.0'
+
+  typescript@5.8.3:
+    resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  uri-js@4.4.1:
+    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+  util-deprecate@1.0.2:
+    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+  vite@6.4.5:
+    resolution: {integrity: sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==}
+    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+      jiti: '>=1.21.0'
+      less: '*'
+      lightningcss: ^1.21.0
+      sass: '*'
+      sass-embedded: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.16.0
+      tsx: ^4.8.1
+      yaml: ^2.4.2
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      jiti:
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+      tsx:
+        optional: true
+      yaml:
+        optional: true
+
+  vscode-jsonrpc@10.0.0:
+    resolution: {integrity: sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA==}
+    engines: {node: '>=14.17.0'}
+
+  vscode-languageserver-protocol@3.18.5:
+    resolution: {integrity: sha512-FW4fBMzZ99N8xeob4py0l050wgCoRgTpEq5Xcsb9lM1bc29UKGR/wSV+4knaqFxssFHajy7OzNiKP/m3U1ZEuA==}
+
+  vscode-languageserver-textdocument@1.0.16:
+    resolution: {integrity: sha512-qeNEuOi9a3qkqRf7Gfez2WL2VwlBTLCNekHBGO9kxPVUp9XuY15yfTJ9jwvOwKI0I2MhUUfZq95NSp+LK1CLQg==}
+
+  vscode-languageserver-types@3.18.5:
+    resolution: {integrity: sha512-kPd6+JQNz3HMJvjKlqUxERSlZHAntl/aGpsk7ImwIh0ewDKDcjgAEyVSYciPiOJD3ftbK9IrfiqUeTZLSbCizw==}
+
+  vscode-uri@3.2.0:
+    resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==}
+
+  vue-component-type-helpers@3.3.14:
+    resolution: {integrity: sha512-Pq3+OnbCiPCHk0R7KSZrx+j/XXaUn2kSQ1epxq60w8CPbByLjC1EJgV51n4WY851sTXUVzsNMuitywdkdVyKjA==}
+
+  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-eslint-parser@10.4.1:
+    resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+
+  vue-router@4.6.4:
+    resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
+    peerDependencies:
+      vue: ^3.5.0
+
+  vue-tsc@3.3.14:
+    resolution: {integrity: sha512-URAu2H9AuMZMg9GCgnqbaHHHd8pBZRAwNV2e2yYS8VDsoTVXHiOSo663OS6DkEvauQ2yixn5TPFJ9WV9QIspkA==}
+    hasBin: true
+    peerDependencies:
+      typescript: '>=5.0.0'
+
+  vue@3.5.45:
+    resolution: {integrity: sha512-3cvsib8m9c0vB24+MDKsgcW4+Da4xaBl9Z/5Tbm2pmSIsRPxcil4cdXjXlVsmJ2g64XJo5h+IRd7rsdGT9pZaw==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: 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'}
+
+  xml-name-validator@5.0.0:
+    resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+    engines: {node: '>=18'}
+
+  yocto-queue@0.1.0:
+    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+    engines: {node: '>=10'}
+
+snapshots:
+
+  '@babel/helper-string-parser@7.29.13': {}
+
+  '@babel/helper-validator-identifier@7.29.7': {}
+
+  '@babel/parser@7.29.13':
+    dependencies:
+      '@babel/types': 7.29.11
+
+  '@babel/types@7.29.11':
+    dependencies:
+      '@babel/helper-string-parser': 7.29.13
+      '@babel/helper-validator-identifier': 7.29.7
+
+  '@ctrl/tinycolor@4.2.2': {}
+
+  '@element-plus/icons-vue@2.3.2(vue@3.5.45(typescript@5.8.3))':
+    dependencies:
+      vue: 3.5.45(typescript@5.8.3)
+
+  '@esbuild/aix-ppc64@0.25.12':
+    optional: true
+
+  '@esbuild/android-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/android-arm@0.25.12':
+    optional: true
+
+  '@esbuild/android-x64@0.25.12':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/darwin-x64@0.25.12':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-arm@0.25.12':
+    optional: true
+
+  '@esbuild/linux-ia32@0.25.12':
+    optional: true
+
+  '@esbuild/linux-loong64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.25.12':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-s390x@0.25.12':
+    optional: true
+
+  '@esbuild/linux-x64@0.25.12':
+    optional: true
+
+  '@esbuild/netbsd-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.25.12':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.25.12':
+    optional: true
+
+  '@esbuild/openharmony-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/sunos-x64@0.25.12':
+    optional: true
+
+  '@esbuild/win32-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/win32-ia32@0.25.12':
+    optional: true
+
+  '@esbuild/win32-x64@0.25.12':
+    optional: true
+
+  '@eslint-community/eslint-utils@4.10.1(eslint@9.39.4)':
+    dependencies:
+      eslint: 9.39.4
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/regexpp@4.12.2': {}
+
+  '@eslint/config-array@0.21.2':
+    dependencies:
+      '@eslint/object-schema': 2.1.7
+      debug: 4.4.3
+      minimatch: 3.1.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/config-helpers@0.4.2':
+    dependencies:
+      '@eslint/core': 0.17.0
+
+  '@eslint/core@0.17.0':
+    dependencies:
+      '@types/json-schema': 7.0.15
+
+  '@eslint/eslintrc@3.3.7':
+    dependencies:
+      ajv: 6.15.0
+      debug: 4.4.3
+      espree: 10.4.0
+      globals: 14.0.0
+      ignore: 5.3.2
+      import-fresh: 3.3.1
+      js-yaml: 4.3.2
+      minimatch: 3.1.5
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/js@9.39.4': {}
+
+  '@eslint/object-schema@2.1.7': {}
+
+  '@eslint/plugin-kit@0.4.1':
+    dependencies:
+      '@eslint/core': 0.17.0
+      levn: 0.4.1
+
+  '@floating-ui/core@1.8.0':
+    dependencies:
+      '@floating-ui/utils': 0.2.12
+
+  '@floating-ui/dom@1.8.0':
+    dependencies:
+      '@floating-ui/core': 1.8.0
+      '@floating-ui/utils': 0.2.12
+
+  '@floating-ui/utils@0.2.12': {}
+
+  '@humanfs/core@0.19.2':
+    dependencies:
+      '@humanfs/types': 0.15.0
+
+  '@humanfs/node@0.16.8':
+    dependencies:
+      '@humanfs/core': 0.19.2
+      '@humanfs/types': 0.15.0
+      '@humanwhocodes/retry': 0.4.3
+
+  '@humanfs/types@0.15.0': {}
+
+  '@humanwhocodes/module-importer@1.0.1': {}
+
+  '@humanwhocodes/retry@0.4.3': {}
+
+  '@jridgewell/sourcemap-codec@1.6.0': {}
+
+  '@rolldown/pluginutils@1.0.1': {}
+
+  '@rollup/rollup-android-arm-eabi@4.67.5':
+    optional: true
+
+  '@rollup/rollup-android-arm64@4.67.5':
+    optional: true
+
+  '@rollup/rollup-android-x64@4.67.5':
+    optional: true
+
+  '@rollup/rollup-darwin-arm64@4.67.5':
+    optional: true
+
+  '@rollup/rollup-darwin-x64@4.67.5':
+    optional: true
+
+  '@rollup/rollup-freebsd-arm64@4.67.5':
+    optional: true
+
+  '@rollup/rollup-freebsd-x64@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-arm-musleabihf@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-gnu@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-musl@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-loong64-gnu@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-loong64-musl@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-ppc64-gnu@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-ppc64-musl@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-gnu@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-musl@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-s390x-gnu@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-x64-gnu@4.67.5':
+    optional: true
+
+  '@rollup/rollup-linux-x64-musl@4.67.5':
+    optional: true
+
+  '@rollup/rollup-openbsd-x64@4.67.5':
+    optional: true
+
+  '@rollup/rollup-openharmony-arm64@4.67.5':
+    optional: true
+
+  '@rollup/rollup-win32-arm64-msvc@4.67.5':
+    optional: true
+
+  '@rollup/rollup-win32-ia32-msvc@4.67.5':
+    optional: true
+
+  '@rollup/rollup-win32-x64-gnu@4.67.5':
+    optional: true
+
+  '@rollup/rollup-win32-x64-msvc@4.67.5':
+    optional: true
+
+  '@sxzz/popperjs-es@2.11.8': {}
+
+  '@types/esrecurse@4.3.1': {}
+
+  '@types/estree@1.0.10': {}
+
+  '@types/json-schema@7.0.15': {}
+
+  '@types/lodash-es@4.17.12':
+    dependencies:
+      '@types/lodash': 4.17.27
+
+  '@types/lodash@4.17.27': {}
+
+  '@types/web-bluetooth@0.0.21': {}
+
+  '@typescript-eslint/eslint-plugin@8.72.1(@typescript-eslint/parser@8.72.1(eslint@9.39.4)(typescript@5.8.3))(eslint@9.39.4)(typescript@5.8.3)':
+    dependencies:
+      '@eslint-community/regexpp': 4.12.2
+      '@typescript-eslint/parser': 8.72.1(eslint@9.39.4)(typescript@5.8.3)
+      '@typescript-eslint/scope-manager': 8.72.1
+      '@typescript-eslint/type-utils': 8.72.1(eslint@9.39.4)(typescript@5.8.3)
+      '@typescript-eslint/utils': 8.72.1(eslint@9.39.4)(typescript@5.8.3)
+      '@typescript-eslint/visitor-keys': 8.72.1
+      eslint: 9.39.4
+      ignore: 7.0.14
+      natural-compare: 1.4.0
+      ts-api-utils: 2.5.0(typescript@5.8.3)
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/parser@8.72.1(eslint@9.39.4)(typescript@5.8.3)':
+    dependencies:
+      '@typescript-eslint/scope-manager': 8.72.1
+      '@typescript-eslint/types': 8.72.1
+      '@typescript-eslint/typescript-estree': 8.72.1(typescript@5.8.3)
+      '@typescript-eslint/visitor-keys': 8.72.1
+      debug: 4.4.3
+      eslint: 9.39.4
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/project-service@8.72.1(typescript@5.8.3)':
+    dependencies:
+      '@typescript-eslint/tsconfig-utils': 8.72.1(typescript@5.8.3)
+      '@typescript-eslint/types': 8.72.1
+      debug: 4.4.3
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/scope-manager@8.72.1':
+    dependencies:
+      '@typescript-eslint/types': 8.72.1
+      '@typescript-eslint/visitor-keys': 8.72.1
+
+  '@typescript-eslint/tsconfig-utils@8.72.1(typescript@5.8.3)':
+    dependencies:
+      typescript: 5.8.3
+
+  '@typescript-eslint/type-utils@8.72.1(eslint@9.39.4)(typescript@5.8.3)':
+    dependencies:
+      '@typescript-eslint/types': 8.72.1
+      '@typescript-eslint/typescript-estree': 8.72.1(typescript@5.8.3)
+      '@typescript-eslint/utils': 8.72.1(eslint@9.39.4)(typescript@5.8.3)
+      debug: 4.4.3
+      eslint: 9.39.4
+      ts-api-utils: 2.5.0(typescript@5.8.3)
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/types@8.72.1': {}
+
+  '@typescript-eslint/typescript-estree@8.72.1(typescript@5.8.3)':
+    dependencies:
+      '@typescript-eslint/project-service': 8.72.1(typescript@5.8.3)
+      '@typescript-eslint/tsconfig-utils': 8.72.1(typescript@5.8.3)
+      '@typescript-eslint/types': 8.72.1
+      '@typescript-eslint/visitor-keys': 8.72.1
+      debug: 4.4.3
+      minimatch: 10.2.6
+      semver: 7.8.6
+      tinyglobby: 0.2.17
+      ts-api-utils: 2.5.0(typescript@5.8.3)
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/utils@8.72.1(eslint@9.39.4)(typescript@5.8.3)':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.4)
+      '@typescript-eslint/scope-manager': 8.72.1
+      '@typescript-eslint/types': 8.72.1
+      '@typescript-eslint/typescript-estree': 8.72.1(typescript@5.8.3)
+      eslint: 9.39.4
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/visitor-keys@8.72.1':
+    dependencies:
+      '@typescript-eslint/types': 8.72.1
+      eslint-visitor-keys: 5.0.1
+
+  '@vitejs/plugin-vue@6.0.10(vite@6.4.5)(vue@3.5.45(typescript@5.8.3))':
+    dependencies:
+      '@rolldown/pluginutils': 1.0.1
+      vite: 6.4.5
+      vue: 3.5.45(typescript@5.8.3)
+
+  '@volar/language-core@2.4.30':
+    dependencies:
+      '@volar/source-map': 2.4.30
+
+  '@volar/language-service@2.4.30(typescript@5.8.3)':
+    dependencies:
+      '@volar/language-core': 2.4.30
+      vscode-languageserver-protocol: 3.18.5
+      vscode-languageserver-textdocument: 1.0.16
+      vscode-uri: 3.2.0
+    optionalDependencies:
+      typescript: 5.8.3
+
+  '@volar/source-map@2.4.30': {}
+
+  '@volar/typescript@2.4.30(typescript@5.8.3)':
+    dependencies:
+      '@volar/language-core': 2.4.30
+      '@volar/language-service': 2.4.30(typescript@5.8.3)
+      path-browserify: 1.0.1
+      vscode-uri: 3.2.0
+    optionalDependencies:
+      typescript: 5.8.3
+
+  '@vue/compiler-core@3.5.45':
+    dependencies:
+      '@babel/parser': 7.29.13
+      '@vue/shared': 3.5.45
+      entities: 7.0.1
+      estree-walker: 2.0.2
+      source-map-js: 1.2.2
+
+  '@vue/compiler-dom@3.5.45':
+    dependencies:
+      '@vue/compiler-core': 3.5.45
+      '@vue/shared': 3.5.45
+
+  '@vue/compiler-sfc@3.5.45':
+    dependencies:
+      '@babel/parser': 7.29.13
+      '@vue/compiler-core': 3.5.45
+      '@vue/compiler-dom': 3.5.45
+      '@vue/compiler-ssr': 3.5.45
+      '@vue/shared': 3.5.45
+      estree-walker: 2.0.2
+      magic-string: 0.30.21
+      postcss: 8.5.32
+      source-map-js: 1.2.2
+
+  '@vue/compiler-ssr@3.5.45':
+    dependencies:
+      '@vue/compiler-dom': 3.5.45
+      '@vue/shared': 3.5.45
+
+  '@vue/devtools-api@6.6.4': {}
+
+  '@vue/language-core@3.3.14':
+    dependencies:
+      '@volar/language-core': 2.4.30
+      '@vue/compiler-dom': 3.5.45
+      '@vue/shared': 3.5.45
+      alien-signals: 3.2.1
+      muggle-string: 0.4.1
+      path-browserify: 1.0.1
+      picomatch: 4.0.7
+
+  '@vue/reactivity@3.5.45':
+    dependencies:
+      '@vue/shared': 3.5.45
+
+  '@vue/runtime-core@3.5.45':
+    dependencies:
+      '@vue/reactivity': 3.5.45
+      '@vue/shared': 3.5.45
+
+  '@vue/runtime-dom@3.5.45':
+    dependencies:
+      '@vue/reactivity': 3.5.45
+      '@vue/runtime-core': 3.5.45
+      '@vue/shared': 3.5.45
+      csstype: 3.2.3
+
+  '@vue/server-renderer@3.5.45':
+    dependencies:
+      '@vue/compiler-ssr': 3.5.45
+      '@vue/runtime-dom': 3.5.45
+      '@vue/shared': 3.5.45
+
+  '@vue/shared@3.5.45': {}
+
+  '@vueuse/core@15.0.0(vue@3.5.45(typescript@5.8.3))':
+    dependencies:
+      '@types/web-bluetooth': 0.0.21
+      '@vueuse/metadata': 15.0.0
+      '@vueuse/shared': 15.0.0(vue@3.5.45(typescript@5.8.3))
+      vue: 3.5.45(typescript@5.8.3)
+
+  '@vueuse/metadata@15.0.0': {}
+
+  '@vueuse/shared@15.0.0(vue@3.5.45(typescript@5.8.3))':
+    dependencies:
+      vue: 3.5.45(typescript@5.8.3)
+
+  acorn-jsx@5.3.2(acorn@8.19.0):
+    dependencies:
+      acorn: 8.19.0
+
+  acorn@8.19.0: {}
+
+  agent-base@6.0.2:
+    dependencies:
+      debug: 4.4.3
+    transitivePeerDependencies:
+      - supports-color
+
+  ajv@6.15.0:
+    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
+
+  alien-signals@3.2.1: {}
+
+  ansi-styles@4.3.0:
+    dependencies:
+      color-convert: 2.0.1
+
+  argparse@2.0.1: {}
+
+  async-validator@4.2.5: {}
+
+  asynckit@0.4.0: {}
+
+  axios@1.21.0:
+    dependencies:
+      follow-redirects: 1.16.2
+      form-data: 4.0.6
+      https-proxy-agent: 5.0.1
+      proxy-from-env: 2.1.0
+    transitivePeerDependencies:
+      - debug
+      - supports-color
+
+  balanced-match@1.0.2: {}
+
+  balanced-match@4.0.4: {}
+
+  boolbase@1.0.0: {}
+
+  brace-expansion@1.1.21:
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+
+  brace-expansion@5.0.12:
+    dependencies:
+      balanced-match: 4.0.4
+
+  call-bind-apply-helpers@1.0.2:
+    dependencies:
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+
+  callsites@3.1.0: {}
+
+  chalk@4.1.2:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  color-convert@2.0.1:
+    dependencies:
+      color-name: 1.1.4
+
+  color-name@1.1.4: {}
+
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
+  concat-map@0.0.1: {}
+
+  cross-spawn@7.0.6:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
+  cssesc@3.0.0: {}
+
+  csstype@3.2.3: {}
+
+  dayjs@1.11.23: {}
+
+  debug@4.4.3:
+    dependencies:
+      ms: 2.1.3
+
+  deep-is@0.1.4: {}
+
+  delayed-stream@1.0.0: {}
+
+  dunder-proto@1.0.1:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
+  element-plus@2.14.7(vue@3.5.45(typescript@5.8.3)):
+    dependencies:
+      '@ctrl/tinycolor': 4.2.2
+      '@element-plus/icons-vue': 2.3.2(vue@3.5.45(typescript@5.8.3))
+      '@floating-ui/dom': 1.8.0
+      '@popperjs/core': '@sxzz/popperjs-es@2.11.8'
+      '@types/lodash': 4.17.27
+      '@types/lodash-es': 4.17.12
+      '@vueuse/core': 15.0.0(vue@3.5.45(typescript@5.8.3))
+      async-validator: 4.2.5
+      dayjs: 1.11.23
+      lodash: 4.18.1
+      lodash-es: 4.18.1
+      lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1)
+      memoize-one: 6.0.0
+      normalize-wheel-es: 1.2.0
+      vue: 3.5.45(typescript@5.8.3)
+      vue-component-type-helpers: 3.3.14
+
+  entities@7.0.1: {}
+
+  es-define-property@1.0.1: {}
+
+  es-errors@1.3.0: {}
+
+  es-object-atoms@1.1.2:
+    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.4
+
+  esbuild@0.25.12:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.25.12
+      '@esbuild/android-arm': 0.25.12
+      '@esbuild/android-arm64': 0.25.12
+      '@esbuild/android-x64': 0.25.12
+      '@esbuild/darwin-arm64': 0.25.12
+      '@esbuild/darwin-x64': 0.25.12
+      '@esbuild/freebsd-arm64': 0.25.12
+      '@esbuild/freebsd-x64': 0.25.12
+      '@esbuild/linux-arm': 0.25.12
+      '@esbuild/linux-arm64': 0.25.12
+      '@esbuild/linux-ia32': 0.25.12
+      '@esbuild/linux-loong64': 0.25.12
+      '@esbuild/linux-mips64el': 0.25.12
+      '@esbuild/linux-ppc64': 0.25.12
+      '@esbuild/linux-riscv64': 0.25.12
+      '@esbuild/linux-s390x': 0.25.12
+      '@esbuild/linux-x64': 0.25.12
+      '@esbuild/netbsd-arm64': 0.25.12
+      '@esbuild/netbsd-x64': 0.25.12
+      '@esbuild/openbsd-arm64': 0.25.12
+      '@esbuild/openbsd-x64': 0.25.12
+      '@esbuild/openharmony-arm64': 0.25.12
+      '@esbuild/sunos-x64': 0.25.12
+      '@esbuild/win32-arm64': 0.25.12
+      '@esbuild/win32-ia32': 0.25.12
+      '@esbuild/win32-x64': 0.25.12
+
+  escape-string-regexp@4.0.0: {}
+
+  eslint-config-prettier@10.1.8(eslint@9.39.4):
+    dependencies:
+      eslint: 9.39.4
+
+  eslint-plugin-vue@10.12.0(@typescript-eslint/parser@8.72.1(eslint@9.39.4)(typescript@5.8.3))(eslint@9.39.4)(vue-eslint-parser@10.4.1(eslint@9.39.4)):
+    dependencies:
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.4)
+      eslint: 9.39.4
+      natural-compare: 1.4.0
+      nth-check: 2.1.1
+      postcss-selector-parser: 7.1.7
+      semver: 7.8.6
+      vue-eslint-parser: 10.4.1(eslint@9.39.4)
+      xml-name-validator: 5.0.0
+    optionalDependencies:
+      '@typescript-eslint/parser': 8.72.1(eslint@9.39.4)(typescript@5.8.3)
+
+  eslint-scope@8.4.0:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  eslint-scope@9.1.2:
+    dependencies:
+      '@types/esrecurse': 4.3.1
+      '@types/estree': 1.0.10
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  eslint-visitor-keys@3.4.3: {}
+
+  eslint-visitor-keys@4.2.1: {}
+
+  eslint-visitor-keys@5.0.1: {}
+
+  eslint@9.39.4:
+    dependencies:
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.4)
+      '@eslint-community/regexpp': 4.12.2
+      '@eslint/config-array': 0.21.2
+      '@eslint/config-helpers': 0.4.2
+      '@eslint/core': 0.17.0
+      '@eslint/eslintrc': 3.3.7
+      '@eslint/js': 9.39.4
+      '@eslint/plugin-kit': 0.4.1
+      '@humanfs/node': 0.16.8
+      '@humanwhocodes/module-importer': 1.0.1
+      '@humanwhocodes/retry': 0.4.3
+      '@types/estree': 1.0.10
+      ajv: 6.15.0
+      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.7.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.5
+      natural-compare: 1.4.0
+      optionator: 0.9.4
+    transitivePeerDependencies:
+      - supports-color
+
+  espree@10.4.0:
+    dependencies:
+      acorn: 8.19.0
+      acorn-jsx: 5.3.2(acorn@8.19.0)
+      eslint-visitor-keys: 4.2.1
+
+  espree@11.2.0:
+    dependencies:
+      acorn: 8.19.0
+      acorn-jsx: 5.3.2(acorn@8.19.0)
+      eslint-visitor-keys: 5.0.1
+
+  esquery@1.7.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  esrecurse@4.3.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  estraverse@5.3.0: {}
+
+  estree-walker@2.0.2: {}
+
+  esutils@2.0.3: {}
+
+  fast-deep-equal@3.1.3: {}
+
+  fast-json-stable-stringify@2.1.0: {}
+
+  fast-levenshtein@2.0.6: {}
+
+  fdir@6.5.0(picomatch@4.0.7):
+    optionalDependencies:
+      picomatch: 4.0.7
+
+  file-entry-cache@8.0.0:
+    dependencies:
+      flat-cache: 4.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.4.4
+      keyv: 4.5.4
+
+  flatted@3.4.4: {}
+
+  follow-redirects@1.16.2: {}
+
+  form-data@4.0.6:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      hasown: 2.0.4
+      mime-types: 2.1.35
+
+  fsevents@2.3.3:
+    optional: true
+
+  function-bind@1.1.2: {}
+
+  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.2
+      function-bind: 1.1.2
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      has-symbols: 1.1.0
+      hasown: 2.0.4
+      math-intrinsics: 1.1.0
+
+  get-proto@1.0.1:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-object-atoms: 1.1.2
+
+  glob-parent@6.0.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  globals@14.0.0: {}
+
+  gopd@1.2.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.4:
+    dependencies:
+      function-bind: 1.1.2
+
+  https-proxy-agent@5.0.1:
+    dependencies:
+      agent-base: 6.0.2
+      debug: 4.4.3
+    transitivePeerDependencies:
+      - supports-color
+
+  ignore@5.3.2: {}
+
+  ignore@7.0.14: {}
+
+  import-fresh@3.3.1:
+    dependencies:
+      parent-module: 1.0.1
+      resolve-from: 4.0.0
+
+  imurmurhash@0.1.4: {}
+
+  is-extglob@2.1.1: {}
+
+  is-glob@4.0.3:
+    dependencies:
+      is-extglob: 2.1.1
+
+  isexe@2.0.0: {}
+
+  js-yaml@4.3.2:
+    dependencies:
+      argparse: 2.0.1
+
+  json-buffer@3.0.1: {}
+
+  json-schema-traverse@0.4.1: {}
+
+  json-stable-stringify-without-jsonify@1.0.1: {}
+
+  keyv@4.5.4:
+    dependencies:
+      json-buffer: 3.0.1
+
+  levn@0.4.1:
+    dependencies:
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+
+  locate-path@6.0.0:
+    dependencies:
+      p-locate: 5.0.0
+
+  lodash-es@4.18.1: {}
+
+  lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1):
+    dependencies:
+      '@types/lodash-es': 4.17.12
+      lodash: 4.18.1
+      lodash-es: 4.18.1
+
+  lodash.merge@4.6.2: {}
+
+  lodash@4.18.1: {}
+
+  magic-string@0.30.21:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.6.0
+
+  math-intrinsics@1.1.0: {}
+
+  memoize-one@6.0.0: {}
+
+  mime-db@1.52.0: {}
+
+  mime-types@2.1.35:
+    dependencies:
+      mime-db: 1.52.0
+
+  minimatch@10.2.6:
+    dependencies:
+      brace-expansion: 5.0.12
+
+  minimatch@3.1.5:
+    dependencies:
+      brace-expansion: 1.1.21
+
+  ms@2.1.3: {}
+
+  muggle-string@0.4.1: {}
+
+  nanoid@3.3.20: {}
+
+  natural-compare@1.4.0: {}
+
+  normalize-wheel-es@1.2.0: {}
+
+  nth-check@2.1.1:
+    dependencies:
+      boolbase: 1.0.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
+
+  path-browserify@1.0.1: {}
+
+  path-exists@4.0.0: {}
+
+  path-key@3.1.1: {}
+
+  picocolors@1.1.1: {}
+
+  picomatch@4.0.7: {}
+
+  pinia@2.3.1(typescript@5.8.3)(vue@3.5.45(typescript@5.8.3)):
+    dependencies:
+      '@vue/devtools-api': 6.6.4
+      vue: 3.5.45(typescript@5.8.3)
+      vue-demi: 0.14.10(vue@3.5.45(typescript@5.8.3))
+    optionalDependencies:
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+
+  postcss-selector-parser@7.1.7:
+    dependencies:
+      cssesc: 3.0.0
+      util-deprecate: 1.0.2
+
+  postcss@8.5.32:
+    dependencies:
+      nanoid: 3.3.20
+      picocolors: 1.1.1
+      source-map-js: 1.2.2
+
+  prelude-ls@1.2.1: {}
+
+  prettier@3.9.10: {}
+
+  proxy-from-env@2.1.0: {}
+
+  punycode@2.3.1: {}
+
+  resolve-from@4.0.0: {}
+
+  rollup@4.67.5:
+    dependencies:
+      '@types/estree': 1.0.10
+    optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.67.5
+      '@rollup/rollup-android-arm64': 4.67.5
+      '@rollup/rollup-android-x64': 4.67.5
+      '@rollup/rollup-darwin-arm64': 4.67.5
+      '@rollup/rollup-darwin-x64': 4.67.5
+      '@rollup/rollup-freebsd-arm64': 4.67.5
+      '@rollup/rollup-freebsd-x64': 4.67.5
+      '@rollup/rollup-linux-arm-gnueabihf': 4.67.5
+      '@rollup/rollup-linux-arm-musleabihf': 4.67.5
+      '@rollup/rollup-linux-arm64-gnu': 4.67.5
+      '@rollup/rollup-linux-arm64-musl': 4.67.5
+      '@rollup/rollup-linux-loong64-gnu': 4.67.5
+      '@rollup/rollup-linux-loong64-musl': 4.67.5
+      '@rollup/rollup-linux-ppc64-gnu': 4.67.5
+      '@rollup/rollup-linux-ppc64-musl': 4.67.5
+      '@rollup/rollup-linux-riscv64-gnu': 4.67.5
+      '@rollup/rollup-linux-riscv64-musl': 4.67.5
+      '@rollup/rollup-linux-s390x-gnu': 4.67.5
+      '@rollup/rollup-linux-x64-gnu': 4.67.5
+      '@rollup/rollup-linux-x64-musl': 4.67.5
+      '@rollup/rollup-openbsd-x64': 4.67.5
+      '@rollup/rollup-openharmony-arm64': 4.67.5
+      '@rollup/rollup-win32-arm64-msvc': 4.67.5
+      '@rollup/rollup-win32-ia32-msvc': 4.67.5
+      '@rollup/rollup-win32-x64-gnu': 4.67.5
+      '@rollup/rollup-win32-x64-msvc': 4.67.5
+      fsevents: 2.3.3
+
+  semver@7.8.6: {}
+
+  shebang-command@2.0.0:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  shebang-regex@3.0.0: {}
+
+  source-map-js@1.2.2: {}
+
+  strip-json-comments@3.1.1: {}
+
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  tinyglobby@0.2.17:
+    dependencies:
+      fdir: 6.5.0(picomatch@4.0.7)
+      picomatch: 4.0.7
+
+  ts-api-utils@2.5.0(typescript@5.8.3):
+    dependencies:
+      typescript: 5.8.3
+
+  type-check@0.4.0:
+    dependencies:
+      prelude-ls: 1.2.1
+
+  typescript-eslint@8.72.1(eslint@9.39.4)(typescript@5.8.3):
+    dependencies:
+      '@typescript-eslint/eslint-plugin': 8.72.1(@typescript-eslint/parser@8.72.1(eslint@9.39.4)(typescript@5.8.3))(eslint@9.39.4)(typescript@5.8.3)
+      '@typescript-eslint/parser': 8.72.1(eslint@9.39.4)(typescript@5.8.3)
+      '@typescript-eslint/typescript-estree': 8.72.1(typescript@5.8.3)
+      '@typescript-eslint/utils': 8.72.1(eslint@9.39.4)(typescript@5.8.3)
+      eslint: 9.39.4
+      typescript: 5.8.3
+    transitivePeerDependencies:
+      - supports-color
+
+  typescript@5.8.3: {}
+
+  uri-js@4.4.1:
+    dependencies:
+      punycode: 2.3.1
+
+  util-deprecate@1.0.2: {}
+
+  vite@6.4.5:
+    dependencies:
+      esbuild: 0.25.12
+      fdir: 6.5.0(picomatch@4.0.7)
+      picomatch: 4.0.7
+      postcss: 8.5.32
+      rollup: 4.67.5
+      tinyglobby: 0.2.17
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  vscode-jsonrpc@10.0.0: {}
+
+  vscode-languageserver-protocol@3.18.5:
+    dependencies:
+      vscode-jsonrpc: 10.0.0
+      vscode-languageserver-types: 3.18.5
+
+  vscode-languageserver-textdocument@1.0.16: {}
+
+  vscode-languageserver-types@3.18.5: {}
+
+  vscode-uri@3.2.0: {}
+
+  vue-component-type-helpers@3.3.14: {}
+
+  vue-demi@0.14.10(vue@3.5.45(typescript@5.8.3)):
+    dependencies:
+      vue: 3.5.45(typescript@5.8.3)
+
+  vue-eslint-parser@10.4.1(eslint@9.39.4):
+    dependencies:
+      debug: 4.4.3
+      eslint: 9.39.4
+      eslint-scope: 9.1.2
+      eslint-visitor-keys: 5.0.1
+      espree: 11.2.0
+      esquery: 1.7.0
+      semver: 7.8.6
+    transitivePeerDependencies:
+      - supports-color
+
+  vue-router@4.6.4(vue@3.5.45(typescript@5.8.3)):
+    dependencies:
+      '@vue/devtools-api': 6.6.4
+      vue: 3.5.45(typescript@5.8.3)
+
+  vue-tsc@3.3.14(typescript@5.8.3):
+    dependencies:
+      '@volar/typescript': 2.4.30(typescript@5.8.3)
+      '@vue/language-core': 3.3.14
+      typescript: 5.8.3
+
+  vue@3.5.45(typescript@5.8.3):
+    dependencies:
+      '@vue/compiler-dom': 3.5.45
+      '@vue/compiler-sfc': 3.5.45
+      '@vue/runtime-dom': 3.5.45
+      '@vue/server-renderer': 3.5.45
+      '@vue/shared': 3.5.45
+    optionalDependencies:
+      typescript: 5.8.3
+
+  which@2.0.2:
+    dependencies:
+      isexe: 2.0.0
+
+  word-wrap@1.2.5: {}
+
+  xml-name-validator@5.0.0: {}
+
+  yocto-queue@0.1.0: {}

+ 9 - 0
prettier.config.js

@@ -0,0 +1,9 @@
+// Prettier 3 配置(与 ESLint 通过 eslint-config-prettier 互斥,避免双写冲突)
+export default {
+  semi: true,
+  singleQuote: true,
+  printWidth: 120,
+  tabWidth: 2,
+  trailingComma: 'all',
+  endOfLine: 'lf',
+};

+ 91 - 0
scripts/gen-shims.cjs

@@ -0,0 +1,91 @@
+/**
+ * gen-shims.cjs —— 重新生成 shims/*.ts 的转发清单(框架升级五步流程的辅助脚本,README §1.5)。
+ *
+ * 用法:node scripts/gen-shims.cjs
+ * 原理:从 node_modules 安装的框架包(与 vendor 版本表同版本)读运行时公开 API,
+ *       逐一生成为 `export const X = V.X;` 静态转发行。
+ *
+ * 类型策略(关键,决定 V 的静态类型):
+ *   V 收窄为 ESM 命名空间类型(import type * as XNS from 'pkg')。
+ *   原因与限制:vue 3.5 起 **CJS 运行时导出 ≠ ESM 类型声明**——
+ *     ssrUtils / withAsyncContext / mergeDefaults 等键在运行时(require/UMD 全局)存在,
+ *     但 ESM 声明文件未导出它们。若按 `typeof globalThis.Vue`(= ESM 类型)收窄,
+ *     这些键的转发行会 TS 报"Property does not exist"。
+ *   处理:对"运行时存在但 ESM 类型未声明"的键,用下方 EXCLUDE_* 清单显式跳过——
+ *     这些键是 SSR/compat 边缘工具,纯浏览器客户端业务用不到,SFC 编译器产物也不会
+ *     import 它们(编译器只 import openBlock/createVNode 等 runtime-core 导出,全在 ESM 类型里)。
+ *     跳过它们 = shims 只转发"ESM 类型已声明的公开 API",每个转发行类型精确、vue-tsc 零报错。
+ *   维护纪律:升级框架(README §1.5)后重跑 pnpm typecheck,若出现新的
+ *     "Property 'X' does not exist"报错,把 X 补进对应 EXCLUDE 清单再重新生成本脚本。
+ *
+ * 静态逐一转发(而非 export * / default namespace):
+ *   ① 每个导出的值在模块顶层从 UMD 全局取值——dist 不含框架本体;
+ *   ② 模板编译器产物 import 的任何具名导出都能命中。
+ *
+ * 纪律:shims/ 是全项目唯一允许出现框架模块名的目录;本脚本是 shims 的唯一事实来源。
+ */
+const fs = require('fs');
+const path = require('path');
+
+/**
+ * 运行时导出但 ESM 类型未声明的键(按包名)。
+ * vue 3.5:以下 10 个为 SSR/compat 边缘工具,浏览器客户端用不到,跳过。
+ * 升级框架后按 pnpm typecheck 报错增量维护本清单。
+ */
+const EXCLUDE = {
+  vue: new Set([
+    'ssrUtils',
+    'withAsyncContext',
+    'mergeDefaults',
+    'mergeModels',
+    'resolveFilter',
+    'compatUtils',
+    'ErrorTypeStrings',
+    'assertNumber',
+    'createPropsRestProxy',
+    'initDirectivesForSSR',
+  ]),
+  'vue-router': new Set(),
+  pinia: new Set(),
+};
+
+function gen(pkgName, globalName, outFile) {
+  const ns = require(pkgName);
+  const keys = Object.keys(ns);
+  const validId = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
+  const excluded = EXCLUDE[pkgName] ?? new Set();
+  const exportable = keys.filter((k) => validId.test(k) && !k.startsWith('__') && !excluded.has(k));
+
+  const header = `/**
+ * shims/${path.basename(outFile)} —— '${pkgName}' 模块名 → UMD 全局 ${globalName} 的唯一胶水层(README §3.3-1)。
+ *
+ * 存在理由:SFC 模板编译器(@vitejs/plugin-vue)产物内部会生成
+ * 'import { X } from "${pkgName}"' 这类模块名 import,vite define 无法改写模块名(只处理裸标识符),
+ * 故 vite.config 的 alias 将 '${pkgName}' 重定向到本文件。
+ *
+ * 关键实现(保证 dist 不含框架运行时):
+ *   - import type 仅取 ESM 命名空间类型(编译期擦除,不进产物);
+ *   - 运行时值全部取自 globalThis.${globalName}(index.html 引入的 UMD 全局);
+ *   - 编译后只剩"取全局 + 转发"代码,框架本体不进 dist。
+ *
+ * 本文件由 scripts/gen-shims.cjs 生成(导出清单来自当前安装版本,跳过 ESM 类型未声明的 SSR/compat 边缘键);
+ * 框架升级(README §1.5 五步流程)时须重新生成并按 typecheck 报错维护排除清单。
+ * 纪律:shims/ 是全项目唯一允许出现框架模块名的目录。
+ */
+import type * as ${globalName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_NS from '${pkgName}';
+
+/** 运行时框架全局(index.html 按序注入;经 unknown 收窄为 ESM 命名空间类型) */
+const V: typeof ${globalName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_NS = (globalThis as unknown as { ${globalName}: typeof ${globalName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_NS }).${globalName};
+
+`;
+  const body = exportable
+    .map((k) => `// eslint-disable-next-line import/no-mutable-exports\nexport const ${k} = V.${k};`)
+    .join('\n');
+  fs.writeFileSync(outFile, header + body + '\n');
+  console.log(`${outFile}: ${exportable.length} exports (excluded ${excluded.size})`);
+}
+
+const root = path.join(__dirname, '..');
+gen('vue', 'Vue', path.join(root, 'shims', 'vue.ts'));
+gen('vue-router', 'VueRouter', path.join(root, 'shims', 'vue-router.ts'));
+gen('pinia', 'Pinia', path.join(root, 'shims', 'pinia.ts'));

+ 55 - 0
shims/pinia.ts

@@ -0,0 +1,55 @@
+/**
+ * shims/pinia.ts —— 'pinia' 模块名 → UMD 全局 Pinia 的唯一胶水层(README §3.3-1)。
+ *
+ * 存在理由:SFC 模板编译器(@vitejs/plugin-vue)产物内部会生成
+ * 'import { X } from "pinia"' 这类模块名 import,vite define 无法改写模块名(只处理裸标识符),
+ * 故 vite.config 的 alias 将 'pinia' 重定向到本文件。
+ *
+ * 关键实现(保证 dist 不含框架运行时):
+ *   - import type 仅取 ESM 命名空间类型(编译期擦除,不进产物);
+ *   - 运行时值全部取自 globalThis.Pinia(index.html 引入的 UMD 全局);
+ *   - 编译后只剩"取全局 + 转发"代码,框架本体不进 dist。
+ *
+ * 本文件由 scripts/gen-shims.cjs 生成(导出清单来自当前安装版本,跳过 ESM 类型未声明的 SSR/compat 边缘键);
+ * 框架升级(README §1.5 五步流程)时须重新生成并按 typecheck 报错维护排除清单。
+ * 纪律:shims/ 是全项目唯一允许出现框架模块名的目录。
+ */
+import type * as PINIA_NS from 'pinia';
+
+/** 运行时框架全局(index.html 按序注入;经 unknown 收窄为 ESM 命名空间类型) */
+const V: typeof PINIA_NS = (globalThis as unknown as { Pinia: typeof PINIA_NS }).Pinia;
+
+// eslint-disable-next-line import/no-mutable-exports
+export const MutationType = V.MutationType;
+// eslint-disable-next-line import/no-mutable-exports
+export const PiniaVuePlugin = V.PiniaVuePlugin;
+// eslint-disable-next-line import/no-mutable-exports
+export const acceptHMRUpdate = V.acceptHMRUpdate;
+// eslint-disable-next-line import/no-mutable-exports
+export const createPinia = V.createPinia;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineStore = V.defineStore;
+// eslint-disable-next-line import/no-mutable-exports
+export const disposePinia = V.disposePinia;
+// eslint-disable-next-line import/no-mutable-exports
+export const getActivePinia = V.getActivePinia;
+// eslint-disable-next-line import/no-mutable-exports
+export const mapActions = V.mapActions;
+// eslint-disable-next-line import/no-mutable-exports
+export const mapGetters = V.mapGetters;
+// eslint-disable-next-line import/no-mutable-exports
+export const mapState = V.mapState;
+// eslint-disable-next-line import/no-mutable-exports
+export const mapStores = V.mapStores;
+// eslint-disable-next-line import/no-mutable-exports
+export const mapWritableState = V.mapWritableState;
+// eslint-disable-next-line import/no-mutable-exports
+export const setActivePinia = V.setActivePinia;
+// eslint-disable-next-line import/no-mutable-exports
+export const setMapStoreSuffix = V.setMapStoreSuffix;
+// eslint-disable-next-line import/no-mutable-exports
+export const shouldHydrate = V.shouldHydrate;
+// eslint-disable-next-line import/no-mutable-exports
+export const skipHydrate = V.skipHydrate;
+// eslint-disable-next-line import/no-mutable-exports
+export const storeToRefs = V.storeToRefs;

+ 67 - 0
shims/vue-router.ts

@@ -0,0 +1,67 @@
+/**
+ * shims/vue-router.ts —— 'vue-router' 模块名 → UMD 全局 VueRouter 的唯一胶水层(README §3.3-1)。
+ *
+ * 存在理由:SFC 模板编译器(@vitejs/plugin-vue)产物内部会生成
+ * 'import { X } from "vue-router"' 这类模块名 import,vite define 无法改写模块名(只处理裸标识符),
+ * 故 vite.config 的 alias 将 'vue-router' 重定向到本文件。
+ *
+ * 关键实现(保证 dist 不含框架运行时):
+ *   - import type 仅取 ESM 命名空间类型(编译期擦除,不进产物);
+ *   - 运行时值全部取自 globalThis.VueRouter(index.html 引入的 UMD 全局);
+ *   - 编译后只剩"取全局 + 转发"代码,框架本体不进 dist。
+ *
+ * 本文件由 scripts/gen-shims.cjs 生成(导出清单来自当前安装版本,跳过 ESM 类型未声明的 SSR/compat 边缘键);
+ * 框架升级(README §1.5 五步流程)时须重新生成并按 typecheck 报错维护排除清单。
+ * 纪律:shims/ 是全项目唯一允许出现框架模块名的目录。
+ */
+import type * as VUEROUTER_NS from 'vue-router';
+
+/** 运行时框架全局(index.html 按序注入;经 unknown 收窄为 ESM 命名空间类型) */
+const V: typeof VUEROUTER_NS = (globalThis as unknown as { VueRouter: typeof VUEROUTER_NS }).VueRouter;
+
+// eslint-disable-next-line import/no-mutable-exports
+export const NavigationFailureType = V.NavigationFailureType;
+// eslint-disable-next-line import/no-mutable-exports
+export const RouterLink = V.RouterLink;
+// eslint-disable-next-line import/no-mutable-exports
+export const RouterView = V.RouterView;
+// eslint-disable-next-line import/no-mutable-exports
+export const START_LOCATION = V.START_LOCATION;
+// eslint-disable-next-line import/no-mutable-exports
+export const createMemoryHistory = V.createMemoryHistory;
+// eslint-disable-next-line import/no-mutable-exports
+export const createRouter = V.createRouter;
+// eslint-disable-next-line import/no-mutable-exports
+export const createRouterMatcher = V.createRouterMatcher;
+// eslint-disable-next-line import/no-mutable-exports
+export const createWebHashHistory = V.createWebHashHistory;
+// eslint-disable-next-line import/no-mutable-exports
+export const createWebHistory = V.createWebHistory;
+// eslint-disable-next-line import/no-mutable-exports
+export const isNavigationFailure = V.isNavigationFailure;
+// eslint-disable-next-line import/no-mutable-exports
+export const loadRouteLocation = V.loadRouteLocation;
+// eslint-disable-next-line import/no-mutable-exports
+export const matchedRouteKey = V.matchedRouteKey;
+// eslint-disable-next-line import/no-mutable-exports
+export const onBeforeRouteLeave = V.onBeforeRouteLeave;
+// eslint-disable-next-line import/no-mutable-exports
+export const onBeforeRouteUpdate = V.onBeforeRouteUpdate;
+// eslint-disable-next-line import/no-mutable-exports
+export const parseQuery = V.parseQuery;
+// eslint-disable-next-line import/no-mutable-exports
+export const routeLocationKey = V.routeLocationKey;
+// eslint-disable-next-line import/no-mutable-exports
+export const routerKey = V.routerKey;
+// eslint-disable-next-line import/no-mutable-exports
+export const routerViewLocationKey = V.routerViewLocationKey;
+// eslint-disable-next-line import/no-mutable-exports
+export const stringifyQuery = V.stringifyQuery;
+// eslint-disable-next-line import/no-mutable-exports
+export const useLink = V.useLink;
+// eslint-disable-next-line import/no-mutable-exports
+export const useRoute = V.useRoute;
+// eslint-disable-next-line import/no-mutable-exports
+export const useRouter = V.useRouter;
+// eslint-disable-next-line import/no-mutable-exports
+export const viewDepthKey = V.viewDepthKey;

+ 343 - 0
shims/vue.ts

@@ -0,0 +1,343 @@
+/**
+ * shims/vue.ts —— 'vue' 模块名 → UMD 全局 Vue 的唯一胶水层(README §3.3-1)。
+ *
+ * 存在理由:SFC 模板编译器(@vitejs/plugin-vue)产物内部会生成
+ * 'import { X } from "vue"' 这类模块名 import,vite define 无法改写模块名(只处理裸标识符),
+ * 故 vite.config 的 alias 将 'vue' 重定向到本文件。
+ *
+ * 关键实现(保证 dist 不含框架运行时):
+ *   - import type 仅取 ESM 命名空间类型(编译期擦除,不进产物);
+ *   - 运行时值全部取自 globalThis.Vue(index.html 引入的 UMD 全局);
+ *   - 编译后只剩"取全局 + 转发"代码,框架本体不进 dist。
+ *
+ * 本文件由 scripts/gen-shims.cjs 生成(导出清单来自当前安装版本,跳过 ESM 类型未声明的 SSR/compat 边缘键);
+ * 框架升级(README §1.5 五步流程)时须重新生成并按 typecheck 报错维护排除清单。
+ * 纪律:shims/ 是全项目唯一允许出现框架模块名的目录。
+ */
+import type * as VUE_NS from 'vue';
+
+/** 运行时框架全局(index.html 按序注入;经 unknown 收窄为 ESM 命名空间类型) */
+const V: typeof VUE_NS = (globalThis as unknown as { Vue: typeof VUE_NS }).Vue;
+
+// eslint-disable-next-line import/no-mutable-exports
+export const compile = V.compile;
+// eslint-disable-next-line import/no-mutable-exports
+export const Transition = V.Transition;
+// eslint-disable-next-line import/no-mutable-exports
+export const TransitionGroup = V.TransitionGroup;
+// eslint-disable-next-line import/no-mutable-exports
+export const VueElement = V.VueElement;
+// eslint-disable-next-line import/no-mutable-exports
+export const createApp = V.createApp;
+// eslint-disable-next-line import/no-mutable-exports
+export const createSSRApp = V.createSSRApp;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineCustomElement = V.defineCustomElement;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineSSRCustomElement = V.defineSSRCustomElement;
+// eslint-disable-next-line import/no-mutable-exports
+export const hydrate = V.hydrate;
+// eslint-disable-next-line import/no-mutable-exports
+export const nodeOps = V.nodeOps;
+// eslint-disable-next-line import/no-mutable-exports
+export const patchProp = V.patchProp;
+// eslint-disable-next-line import/no-mutable-exports
+export const render = V.render;
+// eslint-disable-next-line import/no-mutable-exports
+export const useCssModule = V.useCssModule;
+// eslint-disable-next-line import/no-mutable-exports
+export const useCssVars = V.useCssVars;
+// eslint-disable-next-line import/no-mutable-exports
+export const useHost = V.useHost;
+// eslint-disable-next-line import/no-mutable-exports
+export const useShadowRoot = V.useShadowRoot;
+// eslint-disable-next-line import/no-mutable-exports
+export const vModelCheckbox = V.vModelCheckbox;
+// eslint-disable-next-line import/no-mutable-exports
+export const vModelDynamic = V.vModelDynamic;
+// eslint-disable-next-line import/no-mutable-exports
+export const vModelRadio = V.vModelRadio;
+// eslint-disable-next-line import/no-mutable-exports
+export const vModelSelect = V.vModelSelect;
+// eslint-disable-next-line import/no-mutable-exports
+export const vModelText = V.vModelText;
+// eslint-disable-next-line import/no-mutable-exports
+export const vShow = V.vShow;
+// eslint-disable-next-line import/no-mutable-exports
+export const withKeys = V.withKeys;
+// eslint-disable-next-line import/no-mutable-exports
+export const withModifiers = V.withModifiers;
+// eslint-disable-next-line import/no-mutable-exports
+export const EffectScope = V.EffectScope;
+// eslint-disable-next-line import/no-mutable-exports
+export const ReactiveEffect = V.ReactiveEffect;
+// eslint-disable-next-line import/no-mutable-exports
+export const TrackOpTypes = V.TrackOpTypes;
+// eslint-disable-next-line import/no-mutable-exports
+export const TriggerOpTypes = V.TriggerOpTypes;
+// eslint-disable-next-line import/no-mutable-exports
+export const customRef = V.customRef;
+// eslint-disable-next-line import/no-mutable-exports
+export const effect = V.effect;
+// eslint-disable-next-line import/no-mutable-exports
+export const effectScope = V.effectScope;
+// eslint-disable-next-line import/no-mutable-exports
+export const getCurrentScope = V.getCurrentScope;
+// eslint-disable-next-line import/no-mutable-exports
+export const getCurrentWatcher = V.getCurrentWatcher;
+// eslint-disable-next-line import/no-mutable-exports
+export const isProxy = V.isProxy;
+// eslint-disable-next-line import/no-mutable-exports
+export const isReactive = V.isReactive;
+// eslint-disable-next-line import/no-mutable-exports
+export const isReadonly = V.isReadonly;
+// eslint-disable-next-line import/no-mutable-exports
+export const isRef = V.isRef;
+// eslint-disable-next-line import/no-mutable-exports
+export const isShallow = V.isShallow;
+// eslint-disable-next-line import/no-mutable-exports
+export const markRaw = V.markRaw;
+// eslint-disable-next-line import/no-mutable-exports
+export const onScopeDispose = V.onScopeDispose;
+// eslint-disable-next-line import/no-mutable-exports
+export const onWatcherCleanup = V.onWatcherCleanup;
+// eslint-disable-next-line import/no-mutable-exports
+export const proxyRefs = V.proxyRefs;
+// eslint-disable-next-line import/no-mutable-exports
+export const reactive = V.reactive;
+// eslint-disable-next-line import/no-mutable-exports
+export const readonly = V.readonly;
+// eslint-disable-next-line import/no-mutable-exports
+export const ref = V.ref;
+// eslint-disable-next-line import/no-mutable-exports
+export const shallowReactive = V.shallowReactive;
+// eslint-disable-next-line import/no-mutable-exports
+export const shallowReadonly = V.shallowReadonly;
+// eslint-disable-next-line import/no-mutable-exports
+export const shallowRef = V.shallowRef;
+// eslint-disable-next-line import/no-mutable-exports
+export const stop = V.stop;
+// eslint-disable-next-line import/no-mutable-exports
+export const toRaw = V.toRaw;
+// eslint-disable-next-line import/no-mutable-exports
+export const toRef = V.toRef;
+// eslint-disable-next-line import/no-mutable-exports
+export const toRefs = V.toRefs;
+// eslint-disable-next-line import/no-mutable-exports
+export const toValue = V.toValue;
+// eslint-disable-next-line import/no-mutable-exports
+export const triggerRef = V.triggerRef;
+// eslint-disable-next-line import/no-mutable-exports
+export const unref = V.unref;
+// eslint-disable-next-line import/no-mutable-exports
+export const camelize = V.camelize;
+// eslint-disable-next-line import/no-mutable-exports
+export const capitalize = V.capitalize;
+// eslint-disable-next-line import/no-mutable-exports
+export const normalizeClass = V.normalizeClass;
+// eslint-disable-next-line import/no-mutable-exports
+export const normalizeProps = V.normalizeProps;
+// eslint-disable-next-line import/no-mutable-exports
+export const normalizeStyle = V.normalizeStyle;
+// eslint-disable-next-line import/no-mutable-exports
+export const toDisplayString = V.toDisplayString;
+// eslint-disable-next-line import/no-mutable-exports
+export const toHandlerKey = V.toHandlerKey;
+// eslint-disable-next-line import/no-mutable-exports
+export const BaseTransition = V.BaseTransition;
+// eslint-disable-next-line import/no-mutable-exports
+export const BaseTransitionPropsValidators = V.BaseTransitionPropsValidators;
+// eslint-disable-next-line import/no-mutable-exports
+export const Comment = V.Comment;
+// eslint-disable-next-line import/no-mutable-exports
+export const DeprecationTypes = V.DeprecationTypes;
+// eslint-disable-next-line import/no-mutable-exports
+export const ErrorCodes = V.ErrorCodes;
+// eslint-disable-next-line import/no-mutable-exports
+export const Fragment = V.Fragment;
+// eslint-disable-next-line import/no-mutable-exports
+export const KeepAlive = V.KeepAlive;
+// eslint-disable-next-line import/no-mutable-exports
+export const Static = V.Static;
+// eslint-disable-next-line import/no-mutable-exports
+export const Suspense = V.Suspense;
+// eslint-disable-next-line import/no-mutable-exports
+export const Teleport = V.Teleport;
+// eslint-disable-next-line import/no-mutable-exports
+export const Text = V.Text;
+// eslint-disable-next-line import/no-mutable-exports
+export const callWithAsyncErrorHandling = V.callWithAsyncErrorHandling;
+// eslint-disable-next-line import/no-mutable-exports
+export const callWithErrorHandling = V.callWithErrorHandling;
+// eslint-disable-next-line import/no-mutable-exports
+export const cloneVNode = V.cloneVNode;
+// eslint-disable-next-line import/no-mutable-exports
+export const computed = V.computed;
+// eslint-disable-next-line import/no-mutable-exports
+export const createBlock = V.createBlock;
+// eslint-disable-next-line import/no-mutable-exports
+export const createCommentVNode = V.createCommentVNode;
+// eslint-disable-next-line import/no-mutable-exports
+export const createElementBlock = V.createElementBlock;
+// eslint-disable-next-line import/no-mutable-exports
+export const createElementVNode = V.createElementVNode;
+// eslint-disable-next-line import/no-mutable-exports
+export const createHydrationRenderer = V.createHydrationRenderer;
+// eslint-disable-next-line import/no-mutable-exports
+export const createRenderer = V.createRenderer;
+// eslint-disable-next-line import/no-mutable-exports
+export const createSlots = V.createSlots;
+// eslint-disable-next-line import/no-mutable-exports
+export const createStaticVNode = V.createStaticVNode;
+// eslint-disable-next-line import/no-mutable-exports
+export const createTextVNode = V.createTextVNode;
+// eslint-disable-next-line import/no-mutable-exports
+export const createVNode = V.createVNode;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineAsyncComponent = V.defineAsyncComponent;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineComponent = V.defineComponent;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineEmits = V.defineEmits;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineExpose = V.defineExpose;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineModel = V.defineModel;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineOptions = V.defineOptions;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineProps = V.defineProps;
+// eslint-disable-next-line import/no-mutable-exports
+export const defineSlots = V.defineSlots;
+// eslint-disable-next-line import/no-mutable-exports
+export const devtools = V.devtools;
+// eslint-disable-next-line import/no-mutable-exports
+export const getCurrentInstance = V.getCurrentInstance;
+// eslint-disable-next-line import/no-mutable-exports
+export const getTransitionRawChildren = V.getTransitionRawChildren;
+// eslint-disable-next-line import/no-mutable-exports
+export const guardReactiveProps = V.guardReactiveProps;
+// eslint-disable-next-line import/no-mutable-exports
+export const h = V.h;
+// eslint-disable-next-line import/no-mutable-exports
+export const handleError = V.handleError;
+// eslint-disable-next-line import/no-mutable-exports
+export const hasInjectionContext = V.hasInjectionContext;
+// eslint-disable-next-line import/no-mutable-exports
+export const hydrateOnIdle = V.hydrateOnIdle;
+// eslint-disable-next-line import/no-mutable-exports
+export const hydrateOnInteraction = V.hydrateOnInteraction;
+// eslint-disable-next-line import/no-mutable-exports
+export const hydrateOnMediaQuery = V.hydrateOnMediaQuery;
+// eslint-disable-next-line import/no-mutable-exports
+export const hydrateOnVisible = V.hydrateOnVisible;
+// eslint-disable-next-line import/no-mutable-exports
+export const initCustomFormatter = V.initCustomFormatter;
+// eslint-disable-next-line import/no-mutable-exports
+export const inject = V.inject;
+// eslint-disable-next-line import/no-mutable-exports
+export const isMemoSame = V.isMemoSame;
+// eslint-disable-next-line import/no-mutable-exports
+export const isRuntimeOnly = V.isRuntimeOnly;
+// eslint-disable-next-line import/no-mutable-exports
+export const isVNode = V.isVNode;
+// eslint-disable-next-line import/no-mutable-exports
+export const mergeProps = V.mergeProps;
+// eslint-disable-next-line import/no-mutable-exports
+export const nextTick = V.nextTick;
+// eslint-disable-next-line import/no-mutable-exports
+export const onActivated = V.onActivated;
+// eslint-disable-next-line import/no-mutable-exports
+export const onBeforeMount = V.onBeforeMount;
+// eslint-disable-next-line import/no-mutable-exports
+export const onBeforeUnmount = V.onBeforeUnmount;
+// eslint-disable-next-line import/no-mutable-exports
+export const onBeforeUpdate = V.onBeforeUpdate;
+// eslint-disable-next-line import/no-mutable-exports
+export const onDeactivated = V.onDeactivated;
+// eslint-disable-next-line import/no-mutable-exports
+export const onErrorCaptured = V.onErrorCaptured;
+// eslint-disable-next-line import/no-mutable-exports
+export const onMounted = V.onMounted;
+// eslint-disable-next-line import/no-mutable-exports
+export const onRenderTracked = V.onRenderTracked;
+// eslint-disable-next-line import/no-mutable-exports
+export const onRenderTriggered = V.onRenderTriggered;
+// eslint-disable-next-line import/no-mutable-exports
+export const onServerPrefetch = V.onServerPrefetch;
+// eslint-disable-next-line import/no-mutable-exports
+export const onUnmounted = V.onUnmounted;
+// eslint-disable-next-line import/no-mutable-exports
+export const onUpdated = V.onUpdated;
+// eslint-disable-next-line import/no-mutable-exports
+export const openBlock = V.openBlock;
+// eslint-disable-next-line import/no-mutable-exports
+export const popScopeId = V.popScopeId;
+// eslint-disable-next-line import/no-mutable-exports
+export const provide = V.provide;
+// eslint-disable-next-line import/no-mutable-exports
+export const pushScopeId = V.pushScopeId;
+// eslint-disable-next-line import/no-mutable-exports
+export const queuePostFlushCb = V.queuePostFlushCb;
+// eslint-disable-next-line import/no-mutable-exports
+export const registerRuntimeCompiler = V.registerRuntimeCompiler;
+// eslint-disable-next-line import/no-mutable-exports
+export const renderList = V.renderList;
+// eslint-disable-next-line import/no-mutable-exports
+export const renderSlot = V.renderSlot;
+// eslint-disable-next-line import/no-mutable-exports
+export const resolveComponent = V.resolveComponent;
+// eslint-disable-next-line import/no-mutable-exports
+export const resolveDirective = V.resolveDirective;
+// eslint-disable-next-line import/no-mutable-exports
+export const resolveDynamicComponent = V.resolveDynamicComponent;
+// eslint-disable-next-line import/no-mutable-exports
+export const resolveTransitionHooks = V.resolveTransitionHooks;
+// eslint-disable-next-line import/no-mutable-exports
+export const setBlockTracking = V.setBlockTracking;
+// eslint-disable-next-line import/no-mutable-exports
+export const setDevtoolsHook = V.setDevtoolsHook;
+// eslint-disable-next-line import/no-mutable-exports
+export const setTransitionHooks = V.setTransitionHooks;
+// eslint-disable-next-line import/no-mutable-exports
+export const ssrContextKey = V.ssrContextKey;
+// eslint-disable-next-line import/no-mutable-exports
+export const toHandlers = V.toHandlers;
+// eslint-disable-next-line import/no-mutable-exports
+export const transformVNodeArgs = V.transformVNodeArgs;
+// eslint-disable-next-line import/no-mutable-exports
+export const useAttrs = V.useAttrs;
+// eslint-disable-next-line import/no-mutable-exports
+export const useId = V.useId;
+// eslint-disable-next-line import/no-mutable-exports
+export const useModel = V.useModel;
+// eslint-disable-next-line import/no-mutable-exports
+export const useSSRContext = V.useSSRContext;
+// eslint-disable-next-line import/no-mutable-exports
+export const useSlots = V.useSlots;
+// eslint-disable-next-line import/no-mutable-exports
+export const useTemplateRef = V.useTemplateRef;
+// eslint-disable-next-line import/no-mutable-exports
+export const useTransitionState = V.useTransitionState;
+// eslint-disable-next-line import/no-mutable-exports
+export const version = V.version;
+// eslint-disable-next-line import/no-mutable-exports
+export const warn = V.warn;
+// eslint-disable-next-line import/no-mutable-exports
+export const watch = V.watch;
+// eslint-disable-next-line import/no-mutable-exports
+export const watchEffect = V.watchEffect;
+// eslint-disable-next-line import/no-mutable-exports
+export const watchPostEffect = V.watchPostEffect;
+// eslint-disable-next-line import/no-mutable-exports
+export const watchSyncEffect = V.watchSyncEffect;
+// eslint-disable-next-line import/no-mutable-exports
+export const withCtx = V.withCtx;
+// eslint-disable-next-line import/no-mutable-exports
+export const withDefaults = V.withDefaults;
+// eslint-disable-next-line import/no-mutable-exports
+export const withDirectives = V.withDirectives;
+// eslint-disable-next-line import/no-mutable-exports
+export const withMemo = V.withMemo;
+// eslint-disable-next-line import/no-mutable-exports
+export const withScopeId = V.withScopeId;

+ 37 - 0
src/App.vue

@@ -0,0 +1,37 @@
+<script setup lang="ts">
+/**
+ * App.vue —— 根组件(README §1.4 默认简体中文:el-config-provider 声明式 locale 注入)。
+ *
+ * 职责:
+ *   - el-config-provider 包住整个应用(声明式组件读中文 locale);
+ *   - 路由出口(router-view);
+ *   - 不持有业务状态(状态全部在 store,本组件零逻辑)。
+ */
+/** locale 从 UMD 全局取(setup 顶层声明 → 模板可直接引用;globals.d.ts 已声明 ElementPlusLocaleZhCn) */
+const locale = ElementPlusLocaleZhCn;
+</script>
+
+<template>
+  <!-- 声明式 locale 注入(README §1.4 双注入之一;命令式注入在 main.ts app.use 完成) -->
+  <el-config-provider :locale="locale">
+    <router-view />
+  </el-config-provider>
+</template>
+
+<style>
+/* 全局基础样式(非 scoped;页面级局部样式留在各组件 <style scoped>) */
+* {
+  box-sizing: border-box;
+}
+html,
+body {
+  margin: 0;
+  padding: 0;
+  height: 100%;
+}
+#app {
+  height: 100%;
+  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
+  color: var(--app-text-primary);
+}
+</style>

+ 81 - 0
src/api/auth/login.ts

@@ -0,0 +1,81 @@
+/**
+ * login.ts —— 登录/登出接口封装(auth 域;对应后端 identity 模块 ADR-014 SSO 最小可行)。
+ *
+ * 一域一目录、一资源一文件纪律(README §3.2):本文件只导出登录相关请求函数 + 对应 DTO。
+ *
+ * 典型使用(login store 内):
+ *
+ *   import { login } from '@/api/auth/login';
+ *   const res = await login({ username, password });
+ *   if (res.code !== 200) return; // 错误文案 res.message 已由 http 层 toast
+ *   this.accessToken = res.data.accessToken;
+ */
+import { http } from '@/api/http';
+import type { R } from '@/types/api';
+
+/** 登录请求体(后端 username/password 单一口径,兼容期旧端点别名映射在后端完成) */
+export interface LoginReq {
+  /** 用户名(必填) */
+  username: string;
+  /** 密码(必填;传输层 TLS 保护,前端不明文落盘) */
+  password: string;
+}
+
+/** 登录成功返回的用户凭证(后端 ADR-014:access 30min + refresh 30 天) */
+export interface LoginResult {
+  /** canonical_user_id(ULID,VARCHAR(26),ADR-013) */
+  id: string;
+  /** 用户名 */
+  username: string;
+  /** 真实姓名 */
+  realName: string;
+  /** 角色码数组(7 角色字典;市场 role 9 数据保留但登录入口暂禁用) */
+  roles: number[];
+  /** 所属机构 id(作用域 institution,登录响应即下发) */
+  institutionId: number;
+  /** 所属校区 id(0 = 机构级,无校区约束) */
+  campusId: number;
+  /** access token(RS256 JWT,30 分钟过期) */
+  accessToken: string;
+  /** refresh token(不透明串,30 天过期) */
+  refreshToken: string;
+  /** access 过期时间(Unix 毫秒) */
+  expiresAt: number;
+  /** RBAC 权限码数组(`域.资源.动作` 三段式;菜单/按钮显隐唯一依据) */
+  permissions: string[];
+}
+
+/**
+ * 登录(POST /api/auth/login)。
+ *
+ * 入参:
+ *   - req LoginReq:username/password 必填;字段缺失由前端表单 rules 拦截,
+ *     后端兜底 2001(参数缺失)。
+ *
+ * 出参:
+ *   - Promise<R<LoginResult>>:
+ *     - code=200:data 为凭证;调用方写入 login store;
+ *     - code=1001:未登录语义不复用于此(登录接口本身);账号密码错误返回 1005 段(后端定义);
+ *     - code=5001:系统繁忙,展示 message 即可。
+ *
+ * 异常场景:
+ *   - 网络层故障(断网/超时):Promise reject,调用方 catch 提示"网络连接异常"。
+ */
+export function login(req: LoginReq): Promise<R<LoginResult>> {
+  return http.post<R<LoginResult>>('/api/auth/login', req).then((r) => r.data);
+}
+
+/**
+ * 登出(POST /api/auth/logout):后端吊销当前 access 的 jti 并轮换 refresh。
+ *
+ * 入参:
+ *   - refreshToken string:当前 refresh token(请求体携带,后端据此失效整条链)。
+ *
+ * 出参:
+ *   - Promise<R<undefined>>:code=200 即成功(data 省略);调用方随后清本地 store 跳登录页。
+ * 异常场景:
+ *   - token 已失效(1002):按成功处理(幂等语义),前端同样清本地态。
+ */
+export function logout(refreshToken: string): Promise<R<undefined>> {
+  return http.post<R<undefined>>('/api/auth/logout', { refreshToken }).then((r) => r.data);
+}

+ 166 - 0
src/api/http.ts

@@ -0,0 +1,166 @@
+/**
+ * http.ts —— axios 实例 + 统一信封处理 + 401 refresh 单例重放(README §3.4)。
+ *
+ * 契约纪律(与后端 response 包一一对应):
+ *   - HTTP 恒 200,判成功只认 body `code === 200`;业务错误**不** reject(Promise 正常 resolve),
+ *     由调用方读 `res.code / res.message` 决策——杜绝"拦截器 reject 吞掉业务错误码"的旧系统坑;
+ *   - 仅**网络层**故障(断网/超时/CORS)才 reject,catch 里统一提示"网络连接异常";
+ *   - 401 场景(code 1001/1002):refresh 单例(并发多个 1001 只刷一次,Promise 复用),
+ *     刷新成功重放原请求;刷新失败 → 清登录态 → 跳登录页(README §3.4 守卫链)。
+ *
+ * 典型使用(各域 API 文件内):
+ *
+ *   import { http } from '@/api/http';
+ *   export async function listInstitutions(params: PageQuery) {
+ *     const res = await http.get<R<PageData<InstitutionDTO>>>('/api/org/institutions', { params });
+ *     return res.data; // 调用方自行判断 res.code(或页面统一 try/catch)
+ *   }
+ */
+import { clearTokens, getAccessToken, getRefreshToken, setTokens } from '@/identity/token-store';
+import type { R } from '@/types/api';
+
+/** 登录态失效相关错误码(与后端 errors 包 1xxx 段对齐) */
+const UNAUTHORIZED_CODES: readonly number[] = [1001, 1002];
+
+/** 错误 toast 去重表:同一 message 1s 内不重复弹(README §3.4) */
+const toastDedup = new Map<string, number>();
+const TOAST_DEDUP_MS = 1000;
+
+/**
+ * 弹业务错误提示(1s 内同文案去重)。
+ *
+ * 入参:
+ *   - message string:后端 message(已脱敏);空文案时兜底"系统繁忙"。
+ *
+ * 出参:无。
+ * 异常场景:
+ *   - ElementPlus 未挂载(极端早启动):降级 console.warn,不抛错。
+ */
+function toastBusinessError(message: string): void {
+  const text = message || '系统繁忙';
+  const now = Date.now();
+  const last = toastDedup.get(text);
+  if (last !== undefined && now - last < TOAST_DEDUP_MS) return;
+  toastDedup.set(text, now);
+  const ElMessage = (globalThis.ElementPlus ?? {}).ElMessage;
+  if (ElMessage) ElMessage.error(text);
+  else console.warn('[biz-error]', text);
+}
+
+/** 并发 401 共享的刷新 Promise(单例核心;未发起时 undefined) */
+let refreshing: Promise<boolean> | undefined;
+
+/**
+ * 跳登录页并清空登录态(刷新失败 / 401 不可恢复时调用)。
+ *
+ * 入参:无。
+ *
+ * 出参:无。
+ * 异常场景:
+ *   - 页面正在跳转中的重复调用:location 赋值幂等。
+ */
+function redirectToLogin(): void {
+  clearTokens();
+  window.location.href = '/login';
+}
+
+/**
+ * 刷新 access token(单例,README §3.4:并发 401 只刷一次,Promise 复用)。
+ *
+ * 入参:无(refresh token 经 token-store 读取,全项目唯一物理存取点)。
+ *
+ * 出参:
+ *   - Promise<boolean>:true = 刷新成功(三件套已经 setTokens 写回);
+ *     false = 刷新失败(已 clearTokens + 跳登录页)。
+ * 异常场景:
+ *   - 无 refresh token / refresh 接口非 200 / 网络故障:返回 false 并触发跳登录。
+ */
+export function refreshAccessToken(): Promise<boolean> {
+  if (!refreshing) {
+    refreshing = (async () => {
+      const refreshToken = getRefreshToken();
+      if (!refreshToken) return false;
+      try {
+        // refresh 接口不走信封拦截器(避免 401 递归):用裸 axios
+        const { data } = await axios.post<R<{ accessToken: string; refreshToken: string; expiresAt: number }>>(
+          '/api/auth/refresh',
+          { refreshToken },
+        );
+        if (data.code === 200 && data.data) {
+          setTokens({
+            access: data.data.accessToken,
+            refresh: data.data.refreshToken,
+            expiresAt: data.data.expiresAt,
+          });
+          return true;
+        }
+        return false;
+      } catch {
+        return false;
+      } finally {
+        // 单例释放:本次刷新结果已定型,后续 401 重新发起
+        setTimeout(() => {
+          refreshing = undefined;
+        }, 0);
+      }
+    })();
+  }
+  return refreshing;
+}
+
+/** axios 实例(UMD 全局 axios 的 create;baseURL 由 .env 的 VITE_API_BASE 注入,默认空 = 同源) */
+export const http = axios.create({
+  baseURL: import.meta.env.VITE_API_BASE ?? '',
+  timeout: 30_000,
+  headers: { 'Content-Type': 'application/json' },
+});
+
+/**
+ * 请求拦截器:注入 access token。
+ *
+ * 入参:axios 内部 config(由 axios 传递)。
+ * 出参:
+ *   - config:注入 Authorization 后原样返回;无 token 时不注入(后端按未登录 1001 处理)。
+ * 异常场景:
+ *   - token 读取异常(storage 被禁用):getAccessToken 返回 null,按未登录处理(fail-closed)。
+ */
+http.interceptors.request.use((config) => {
+  const token = getAccessToken();
+  if (token) config.headers.Authorization = `Bearer ${token}`;
+  return config;
+});
+
+/**
+ * 响应拦截器:401 → refresh 单例 → 重放原请求;其余一律放行(业务错误码交给调用方)。
+ *
+ * 入参:axios 响应对象(HTTP 200 的 body)。
+ * 出参:
+ *   - 原响应(含业务错误的 200 响应也原样返回,调用方判 code);
+ *   - 401 且刷新失败:redirect 登录页后返回 never(实际不产生 resolve)。
+ * 异常场景:
+ *   - 网络层错误(err 无 response):直接 reject,由调用方 catch 提示"网络连接异常"。
+ */
+http.interceptors.response.use(
+  (response) => {
+    const body = response.data as R;
+    if (body && body.code !== 200 && !UNAUTHORIZED_CODES.includes(body.code)) {
+      // 统一 toast(README §3.4:错误文案直显后端 message,1s 去重防刷屏)
+      toastBusinessError(body.message);
+    }
+    if (body && UNAUTHORIZED_CODES.includes(body.code)) {
+      return refreshAccessToken().then((ok) => {
+        if (!ok) {
+          redirectToLogin();
+          return Promise.reject(new Error('登录态失效'));
+        }
+        // 重放:重放时不再触发拦截逻辑(Authorization 已由请求拦截器用新 token 填充)
+        return http.request(response.config);
+      });
+    }
+    return response;
+  },
+  (error) => {
+    // 网络层错误(断网/超时/非 2xx 状态):透传 reject
+    return Promise.reject(error);
+  },
+);

+ 73 - 0
src/api/org/institution.ts

@@ -0,0 +1,73 @@
+/**
+ * institution.ts —— 机构(institution)接口封装(org 域;README §3.2 一资源一文件)。
+ *
+ * 本页为"可编译参考样例"的示范域:org 域是身份地基(ADR-006/007 多租户作用域源头),
+ * 列表页示范 useListPage + PageData 统一分页解构(README §3.4)的完整链路。
+ *
+ * 典型使用(views/org/institution/list.vue 内):
+ *
+ *   const { rows, total, page, pageSize, loading, fetchList } = useListPage<InstitutionDTO>({
+ *     fetcher: (p, ps) => listInstitutions({ ...filters, page: p, pageSize: ps }),
+ *   });
+ *   onMounted(() => { fetchList(); });
+ */
+import { http } from '@/api/http';
+import type { PageR } from '@/types/api';
+
+/** 机构 DTO(字段与后端 org 域 Institution 出参逐字段对齐,camelCase) */
+export interface InstitutionDTO {
+  /** 机构 id(int,旧系统 school 概念归一) */
+  id: number;
+  /** 机构名称 */
+  name: string;
+  /** 统一社会信用代码(可空 → 后端空串) */
+  creditCode: string;
+  /** 状态:1 启用 / 0 停用 */
+  status: number;
+  /** 创建时间(RFC3339,后端 BaseModel.CreatedAt) */
+  createdAt: string;
+}
+
+/** 机构列表筛选条件(作用域字段由 useScope().toQuery() 并入,见 README §3.3-4) */
+export interface InstitutionFilter {
+  /** 机构名称模糊搜索(可空) */
+  keyword?: string;
+  /** 状态筛选(可空 = 全部) */
+  status?: number;
+}
+
+/** 机构列表完整查询参数 = 作用域 + 筛选 + 分页(作用域缺失时后端返回 1004 越界拒绝) */
+export type InstitutionQuery = {
+  /** 机构 id(作用域第一级,useScope().toQuery() 注入) */
+  institutionId: number;
+  /** 校区 id(作用域第二级;0/缺省 = 机构级,toQuery 会省略该键) */
+  campusId?: number;
+} & InstitutionFilter & {
+  /** 页码(缺省 1) */
+  page?: number;
+  /** 每页条数(缺省 10,上限 200) */
+  pageSize?: number;
+};
+
+/**
+ * 机构分页列表(GET /api/org/institutions)。
+ *
+ * 入参:
+ *   - params InstitutionQuery:
+ *     - institutionId:作用域必填(useScope().toQuery() 注入,页面禁自拼);
+ *     - campusId:可空(机构级时省略);
+ *     - page:页码,缺省 1(后端 ParsePage 兜底);
+ *     - pageSize:每页条数,缺省 10,上限 200;
+ *     - keyword/status:可选筛选,缺省省略。
+ *
+ * 出参:
+ *   - Promise<PageR<InstitutionDTO>>(即 R<PageData<InstitutionDTO>>):
+ *     - code=200:data.list 为当前页、data.total 为总数(useListPage 统一解构);
+ *     - code=1004:作用域越界(token 作用域与请求参数冲突)——页面应提示并校正 scope。
+ *
+ * 异常场景:
+ *   - 网络层故障:Promise reject(调用方 catch 提示"网络连接异常")。
+ */
+export function listInstitutions(params: InstitutionQuery): Promise<PageR<InstitutionDTO>> {
+  return http.get<PageR<InstitutionDTO>>('/api/org/institutions', { params }).then((r) => r.data);
+}

+ 148 - 0
src/components/ListPage.vue

@@ -0,0 +1,148 @@
+<script setup lang="ts" generic="T extends object">
+/**
+ * ListPage.vue —— 列表页收口组件(README §3.4 可复用资产:表格 + 分页 + 筛选 + 三态)。
+ *
+ * 泛型组件(Vue 3.5 特性):T 为行数据类型(各域 DTO),由页面传入的 rows 自动推断,
+ * columns 的 formatter/prop 随之强类型——新增列、改列无需 any 断言。
+ *
+ * 设计要点:
+ *   - 列定义**数据驱动**(columns prop 数组),新增列不改模板结构(README §3.3-6);
+ *   - 三态(loading/空态/错误态)内置,页面异步区域不允许自写(README §3.3-8);
+ *   - 与 useListPage 配套:页面把 lp.{rows,total,page,pageSize,loading} 绑进来,
+ *     分页器变更经 v-model 写回 lp.page/lp.pageSize,watch 自动重载。
+ *
+ * 典型使用(配合 useListPage):
+ *
+ *   <ListPage
+ *     v-model:page="lp.page"
+ *     v-model:page-size="lp.pageSize"
+ *     :rows="lp.rows"
+ *     :total="lp.total"
+ *     :loading="lp.loading"
+ *     :error="lp.error"
+ *     :columns="[
+ *       { prop: 'name', label: '机构名称', minWidth: 200 },
+ *       { prop: 'creditCode', label: '信用代码' },
+ *       { prop: 'status', label: '状态', formatter: (row) => (row.status === 1 ? '启用' : '停用') },
+ *     ]"
+ *   >
+ *     <template #toolbar><el-input v-model="filters.keyword" placeholder="搜索机构名称" /></template>
+ *   </ListPage>
+ */
+import type { ListColumn } from '@/components/list-column';
+
+interface Props {
+  /** 当前页数据(useListPage.rows;类型 T 由调用方 rows 推断) */
+  rows: T[];
+  /** 总记录数(useListPage.total) */
+  total: number;
+  /** 当前页码(v-model) */
+  page: number;
+  /** 每页条数(v-model) */
+  pageSize: number;
+  /** 加载中(useListPage.loading) */
+  loading?: boolean;
+  /** 错误文案(useListPage.error;非空时展示错误态) */
+  error?: string;
+  /** 列定义数组(必填;formatter 入参类型 = T) */
+  columns: ListColumn<T>[];
+  /** 行 key(缺省 'id') */
+  rowKey?: string;
+  /** 每页条数候选(缺省 10/20/50/100) */
+  pageSizes?: number[];
+}
+
+withDefaults(defineProps<Props>(), {
+  loading: false,
+  error: '',
+  rowKey: 'id',
+  pageSizes: () => [10, 20, 50, 100],
+});
+
+const emit = defineEmits<{
+  /** 页码变更(分页器点击) */
+  (e: 'update:page', page: number): void;
+  /** 每页条数变更 */
+  (e: 'update:pageSize', pageSize: number): void;
+  /** 工具区"搜索"按钮(页面重载入口) */
+  (e: 'search'): void;
+  /** 行点击(类型 = T) */
+  (e: 'row-click', row: T): void;
+}>();
+</script>
+
+<template>
+  <div class="list-page">
+    <!-- 工具区(筛选控件由页面插槽提供;右侧固定"搜索"按钮) -->
+    <div class="list-page__toolbar">
+      <slot name="toolbar" />
+      <el-button type="primary" @click="emit('search')">搜索</el-button>
+    </div>
+
+    <!-- 错误态(后端 message 直显,README §3.3-8:不二次包装文案) -->
+    <el-alert v-if="error" class="list-page__error" :title="error" type="error" show-icon />
+
+    <el-table
+      v-else
+      :data="rows"
+      :loading="loading"
+      :row-key="rowKey"
+      border
+      @row-click="(row: T) => emit('row-click', row)"
+    >
+      <el-table-column
+        v-for="col in columns"
+        :key="col.prop"
+        :prop="col.prop"
+        :label="col.label"
+        :min-width="col.minWidth"
+        :fixed="col.fixed"
+      >
+        <!-- row 经 unknown 双重断言到 T:el-table 插槽行类型宽松,此处是模板内安全收窄的唯一定点 -->
+        <template v-if="col.formatter" #default="{ row }">
+          <span>{{ col.formatter(row as unknown as T) }}</span>
+        </template>
+      </el-table-column>
+      <template #empty>
+        <span class="list-page__empty">暂无数据</span>
+      </template>
+    </el-table>
+
+    <!-- 分页器(el-pagination;v-model 写回 useListPage 的 page/pageSize 触发重载) -->
+    <el-pagination
+      class="list-page__pager"
+      :current-page="page"
+      :page-size="pageSize"
+      :total="total"
+      :page-sizes="pageSizes"
+      layout="total, sizes, prev, pager, next, jumper"
+      background
+      @current-change="(p: number) => emit('update:page', p)"
+      @size-change="(s: number) => emit('update:pageSize', s)"
+    />
+  </div>
+</template>
+
+<style scoped>
+/* 色值/间距一律引全局 token(README §2 styles 目录约束) */
+.list-page {
+  display: flex;
+  flex-direction: column;
+  gap: var(--app-spacing-md);
+}
+.list-page__toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: var(--app-spacing-sm);
+}
+.list-page__error {
+  width: 100%;
+}
+.list-page__pager {
+  justify-content: flex-end;
+}
+.list-page__empty {
+  color: var(--app-text-secondary);
+}
+</style>

+ 24 - 0
src/components/list-column.ts

@@ -0,0 +1,24 @@
+/**
+ * list-column.ts —— ListPage 的列定义类型(独立文件,便于页面/组件双向引用)。
+ *
+ * 与 ListPage.vue 配套(README §3.3-6 表格列数据驱动)。
+ */
+
+/**
+ * 列定义(数据驱动:新增列不改模板结构)。
+ *
+ * 泛型参数:
+ *   - T:行数据类型(各域 DTO);缺省 Record<string, unknown> 用于非泛型场景。
+ */
+export interface ListColumn<T = Record<string, unknown>> {
+  /** 字段 key(与 row 属性同名,camelCase 对齐后端 DTO) */
+  prop: keyof T & string;
+  /** 列头文案 */
+  label: string;
+  /** 列最小宽度(px,可空) */
+  minWidth?: number;
+  /** 列固定(el-table fixed) */
+  fixed?: 'left' | 'right';
+  /** 单元格格式化(可空;返回字符串直接插值,样式化场景改用 Badge 组件 + 插槽) */
+  formatter?: (row: T) => string;
+}

+ 125 - 0
src/composables/use-list-page.ts

@@ -0,0 +1,125 @@
+/**
+ * use-list-page.ts —— 列表页统一规范 composable(README §3.2"列表页一律复用 useListPage")。
+ *
+ * 收口内容(页面不得自写以下状态/逻辑):
+ *   - 分页五元组(page / pageSize / total / rows / loading);
+ *   - **统一分页解构**:fetcher 返回 PageR<T>,本函数统一解 `res.data.{list,total,page,pageSize,pages}`
+ *     (README §3.4 统一分页契约——消灭旧系统"两套读法"的坑);
+ *   - 页码/每页条数变更后的自动重载(watch → fetchList)。
+ *
+ * 作用域纪律(README §3.3-4):fetcher 内必须把 `useScope().toQuery()` 并入请求参数
+ * (institutionId/campusId 只从 scope 取,页面禁自拼数值)——本 composable 不代劳,
+ * 因为各域接口的参数结构不同,由 fetcher 就近组装最清晰。
+ *
+ * 典型使用(views/org/institution/list.vue):
+ *
+ *   const scope = useScope();
+ *   const filters = reactive<InstitutionFilter>({ keyword: '' });
+ *   const lp = useListPage<InstitutionDTO>({
+ *     fetcher: (page, pageSize) => listInstitutions({ ...scope.toQuery(), ...filters, page, pageSize }),
+ *   });
+ *   onMounted(() => lp.fetchList());
+ *   // 模板:<list-page :rows="lp.rows" :total="lp.total" v-model:page="lp.page"
+ *   //         v-model:page-size="lp.pageSize" :loading="lp.loading" @search="onSearch" />
+ */
+import type { Ref } from 'vue';
+import type { PageData, PageR } from '@/types/api';
+
+/** useListPage 入参 */
+export interface UseListPageOptions<T> {
+  /**
+   * 数据获取函数(一资源一文件里的分页接口)。
+   * 入参 page/pageSize 由本 composable 提供(已含作用域合并);
+   * 返回 PageR<T>(= R<PageData<T>>,后端统一分页信封)。
+   */
+  fetcher: (page: number, pageSize: number) => Promise<PageR<T>>;
+  /** 初始页码(缺省 1) */
+  initialPage?: number;
+  /** 初始每页条数(缺省 10;上限 200 由后端钳制,前端不重复钳) */
+  initialPageSize?: number;
+}
+
+/** useListPage 返回的列表状态面 */
+export interface ListPageState<T> {
+  /** 当前页数据(请求失败时兜底为 []) */
+  rows: Ref<T[]>;
+  /** 总记录数(分页器"共 N 条") */
+  total: Ref<number>;
+  /** 当前页码(从 1 起) */
+  page: Ref<number>;
+  /** 每页条数 */
+  pageSize: Ref<number>;
+  /** 加载中(模板 :loading 绑定) */
+  loading: Ref<boolean>;
+  /** 错误文案(后端 message;空串 = 无错误) */
+  error: Ref<string>;
+  /** 执行一次拉取(页码/筛选变化后调用;内部置 loading) */
+  fetchList: () => Promise<void>;
+}
+
+/**
+ * 创建列表页状态(见文件头"典型使用")。
+ *
+ * 入参:
+ *   - options UseListPageOptions<T>:fetcher 必填(返回统一分页信封);initialPage/initialPageSize 可选。
+ *
+ * 出参:
+ *   - ListPageState<T>:rows/total/page/pageSize/loading/error 响应式 + fetchList 动作。
+ *
+ * 异常场景:
+ *   - fetcher resolve 但 code !== 200:rows 置空、error 记 message(页面展示错误态),不抛;
+ *   - fetcher reject(网络层):同上,error 记"网络连接异常"(README §3.4 三态铁律)。
+ */
+export function useListPage<T>(options: UseListPageOptions<T>): ListPageState<T> {
+  // 断言说明:泛型 T 下 ref() 推导为 Ref<UnwrapRefSimple<T>[]>,无法证明与 Ref<T[]> 等价;
+  // 业务 DTO 均无响应式嵌套需求,断言安全(DTO 若含 Ref 字段会在此处被类型系统拦截)。
+  const rows = ref([]) as Ref<T[]>;
+  const total = ref(0);
+  const page = ref(options.initialPage ?? 1);
+  const pageSize = ref(options.initialPageSize ?? 10);
+  const loading = ref(false);
+  const error = ref('');
+
+  /**
+   * 执行一次分页拉取。
+   *
+   * 入参:无(page/pageSize 取当前响应式值;作用域自动并入)。
+   *
+   * 出参:
+   *   - Promise<void>:恒 resolve(错误经 error 响应式暴露,不 reject——调用方不必 try/catch)。
+   * 异常场景:
+   *   - 并发点击:loading 期间重入直接忽略(防重复请求)。
+   */
+  async function fetchList(): Promise<void> {
+    if (loading.value) return;
+    loading.value = true;
+    error.value = '';
+    try {
+      // 作用域自动注入(README §3.3-4):institutionId/campusId 只从 scope 取
+      const res = await options.fetcher(page.value, pageSize.value);
+      const body: PageData<T> | undefined = res.data;
+      if (res.code === 200 && body) {
+        // 统一解构(README §3.4):list/total 恒在 data 内,前端只此一种读法
+        rows.value = body.list ?? [];
+        total.value = body.total ?? 0;
+      } else {
+        rows.value = [];
+        total.value = 0;
+        error.value = res.message || '查询失败';
+      }
+    } catch {
+      rows.value = [];
+      total.value = 0;
+      error.value = '网络连接异常';
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  // 页码/每页条数变更自动重载(分页器 v-model 绑定即触发)
+  watch([page, pageSize], () => {
+    void fetchList();
+  });
+
+  return { rows, total, page, pageSize, loading, error, fetchList };
+}

+ 63 - 0
src/directives/perm.ts

@@ -0,0 +1,63 @@
+/**
+ * perm.ts —— 按钮级权限指令 v-perm(README §3.1 权限码三段式 + §3.3-7 禁 role==N)。
+ *
+ * 用法(模板):
+ *
+ *   <el-button v-perm="'academic.fee.refund'" :disabled="!scope.ready">退款</el-button>
+ *
+ * 行为:权限码缺失时**直接移除元素**(不是 disabled)——避免"看得见点不动"的误导;
+ * 未登录/权限集为空时全部移除(fail-closed)。
+ *
+ * 典型使用(注册入口,见 main.ts):
+ *
+ *   import { perm } from '@/directives/perm';
+ *   app.directive('perm', perm);
+ */
+import type { Directive } from 'vue';
+import { useLogin } from '@/store/login';
+
+/** 权限指令绑定值类型:单个权限码字符串,或需同时满足的数组 */
+type PermValue = string | string[];
+
+/**
+ * 权限指令(mounted + updated 双钩子:登录态/权限集可能在挂载后变化,须复查)。
+ *
+ * 入参(由 Vue 注入):
+ *   - el HTMLElement:指令作用元素。
+ *   - binding:value 为权限码(string)或权限码数组(string[],AND 语义)。
+ *
+ * 出参:无(不通过时移除 el)。
+ * 异常场景:
+ *   - binding.value 为空/非字符串数组:视为"无条件显示"(调用方 bug,console.warn 提示)。
+ *   - pinia 尚未激活(el 在 app 挂载外使用):useLogin 抛错——本指令只允许在已装 pinia 的组件内用。
+ */
+export const perm: Directive<HTMLElement, PermValue> = {
+  mounted: (el, binding) => applyPerm(el, binding.value),
+  updated: (el, binding) => applyPerm(el, binding.value),
+};
+
+/**
+ * 实际应用权限判定(mounted/updated 共用)。
+ *
+ * 入参:
+ *   - el HTMLElement:目标元素。
+ *   - value PermValue:权限码或权限码数组(AND 语义)。
+ *
+ * 出参:无(判定失败时 el.remove())。
+ * 异常场景:
+ *   - value 非法(空串/空数组/非字符串):保留元素并 console.warn(避免误删)。
+ */
+function applyPerm(el: HTMLElement, value: PermValue): void {
+  if (typeof value !== 'string' && !Array.isArray(value)) {
+    console.warn('[v-perm] 绑定值必须为权限码字符串或字符串数组', value);
+    return;
+  }
+  const perms: string[] = typeof value === 'string' ? [value] : value;
+  if (perms.length === 0 || perms.every((p) => !p)) {
+    console.warn('[v-perm] 空权限码按无条件显示处理', value);
+    return;
+  }
+  const login = useLogin();
+  const ok = perms.every((p) => login.hasPerm(p));
+  if (!ok) el.remove();
+}

+ 110 - 0
src/identity/token-store.ts

@@ -0,0 +1,110 @@
+/**
+ * token-store.ts —— token 的**唯一物理存取点**(README §3.2"唯一 token 存取点"的落地实现)。
+ *
+ * 为什么独立成模块而不是放 pinia store:
+ *   http.ts 的 401 拦截器/刷新逻辑在**任何组件运行前**(甚至 pinia 实例创建前)就可能触发,
+ *   pinia store 此时不可用;而 token 存取是**基础设施行为**(非响应式 UI 状态)。
+ *   因此物理存取收口到本轻量模块,`store/login.ts` 只是它的**响应式视图**(UI 侧读)。
+ *
+ * 纪律:
+ *   - 全项目**只有**本文件读写 localStorage 的 token key(http.ts 与 store/login.ts 均经本模块);
+ *   - 页面/组件**禁止**直接 localStorage.getItem('access_token') 之类的散写;
+ *   - 登出/刷新失败必须调用 clear(),不得只清一半。
+ *
+ * 典型使用:
+ *
+ *   import { setTokens, getAccessToken, clearTokens } from '@/identity/token-store';
+ *   setTokens({ access: data.accessToken, refresh: data.refreshToken, expiresAt: data.expiresAt });
+ *   const token = getAccessToken(); // http 请求拦截器内
+ */
+
+/** localStorage key 常量(唯一合法 key 名;散写字符串 key 视为 bug) */
+const KEYS = {
+  /** access token(RS256 JWT,30min) */
+  ACCESS: 'access_token',
+  /** refresh token(不透明串,30 天) */
+  REFRESH: 'refresh_token',
+  /** access 过期时间(Unix 毫秒字符串) */
+  EXPIRES_AT: 'expires_at',
+} as const;
+
+/** setTokens 入参:登录/刷新成功后一次性写入三件套 */
+export interface TokenSet {
+  /** access token */
+  access: string;
+  /** refresh token */
+  refresh: string;
+  /** access 过期时间(Unix 毫秒) */
+  expiresAt: number;
+}
+
+/**
+ * 写入 token 三件套(登录成功 / 刷新成功后调用)。
+ *
+ * 入参:
+ *   - set TokenSet:access/refresh 必填非空;expiresAt 为 Unix 毫秒。
+ *
+ * 出参:无。
+ * 异常场景:
+ *   - localStorage 被浏览器禁用(隐私模式极端情况):抛 QuotaExceededError 由调用方 catch,
+ *     业务上应提示"浏览器禁止存储,无法登录"。
+ */
+export function setTokens(set: TokenSet): void {
+  localStorage.setItem(KEYS.ACCESS, set.access);
+  localStorage.setItem(KEYS.REFRESH, set.refresh);
+  localStorage.setItem(KEYS.EXPIRES_AT, String(set.expiresAt));
+}
+
+/**
+ * 读取 access token。
+ *
+ * 入参:无。
+ *
+ * 出参:
+ *   - string | null:未登录(或已 clear)时返回 null;调用方判空后再拼 Authorization。
+ */
+export function getAccessToken(): string | null {
+  return localStorage.getItem(KEYS.ACCESS);
+}
+
+/**
+ * 读取 refresh token(refresh 单例流程专用)。
+ *
+ * 入参:无。
+ *
+ * 出参:
+ *   - string | null:无 refresh(未登录/已失效被 clear)返回 null → 刷新流程直接判失败跳登录。
+ */
+export function getRefreshToken(): string | null {
+  return localStorage.getItem(KEYS.REFRESH);
+}
+
+/**
+ * 读取 access 过期时间(守卫判断"是否该静默刷新"用)。
+ *
+ * 入参:无。
+ *
+ * 出参:
+ *   - number | null:无记录/记录非法(非数字字符串)返回 null,按"已过期"处理(fail-closed)。
+ */
+export function getExpiresAt(): number | null {
+  const raw = localStorage.getItem(KEYS.EXPIRES_AT);
+  if (!raw) return null;
+  const n = Number(raw);
+  return Number.isFinite(n) ? n : null;
+}
+
+/**
+ * 清空 token 三件套(登出成功 / 刷新失败 / 401 无法恢复时调用)。
+ *
+ * 入参:无。
+ *
+ * 出参:无。
+ * 异常场景:
+ *   - 重复调用幂等(localStorage.removeItem 对不存在的 key 无副作用)。
+ */
+export function clearTokens(): void {
+  localStorage.removeItem(KEYS.ACCESS);
+  localStorage.removeItem(KEYS.REFRESH);
+  localStorage.removeItem(KEYS.EXPIRES_AT);
+}

+ 60 - 0
src/main.ts

@@ -0,0 +1,60 @@
+/**
+ * main.ts —— 业务装配入口(README §2:取 UMD 全局创建应用,不 import 框架运行时)。
+ *
+ * 装配顺序(与 index.html 的 vendor 加载顺序对应):
+ *   1. 取 Vue UMD 全局(裸标识符经 vite define 注入为 Vue.*;createApp 显式走 Vue.createApp);
+ *   2. Pinia:createPinia(裸标识符注入 Pinia.createPinia)→ app.use;
+ *   3. 路由:buildRouter + setupGuards(守卫链:登录态→静默刷新→公开白名单→作用域就绪);
+ *   4. Element Plus:app.use(ElementPlus, { locale: zhCn })——命令式 ElMessageBox/Notification
+ *      读此 locale(README §1.4 双注入之一;声明式注入在 App.vue 的 el-config-provider);
+ *   5. 权限指令:v-perm(README §3.1 权限码三段式);
+ *   6. 登录态 hydrate(页面刷新后从 token-store 恢复 UI 状态,守卫链第①步即可用);
+ *   7. mount('#app')。
+ *
+ * 异常场景:
+ *   - UMD 全局缺失(index.html 漏引/vendor 404):typeof Vue === 'undefined' 时 fail-fast 白屏提示,
+ *     便于区分"框架没加载"与"业务代码 bug"。
+ */
+import { buildRouter, setupGuards } from '@/router';
+import { useLogin } from '@/store/login';
+import { perm } from '@/directives/perm';
+import App from '@/App.vue';
+
+/** UMD 全局缺失提示(开发期定位"vendor 没加载"用;生产 nginx 侧应监控 404) */
+function failMissingGlobal(name: string): never {
+  document.getElementById('app')?.replaceWith(
+    Object.assign(document.createElement('pre'), {
+      style: 'padding:24px;color:#c0392b;font-size:14px',
+      textContent: `[框架未加载] 全局 ${name} 不存在——请检查 index.html 的 vendor 引用与 OSS/静态资源可达性。`,
+    }),
+  );
+  throw new Error(`missing UMD global: ${name}`);
+}
+
+if (typeof Vue === 'undefined') failMissingGlobal('Vue');
+if (typeof Pinia === 'undefined') failMissingGlobal('Pinia');
+if (typeof VueRouter === 'undefined') failMissingGlobal('VueRouter');
+if (typeof ElementPlus === 'undefined') failMissingGlobal('ElementPlus');
+
+// 1. 应用实例(裸标识符 createApp 由 vite define 注入为 Vue.createApp)
+const app = createApp(App);
+
+// 2. Pinia(状态:login + scope)
+app.use(createPinia());
+
+// 3. 路由 + 守卫链
+const router = buildRouter();
+setupGuards(router);
+app.use(router);
+
+// 4. Element Plus 命令式 locale 注入(ElMessageBox/ElNotification 等命令式 API 读此处)
+app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
+
+// 5. 权限指令(按钮级 v-perm,权限码 `域.资源.动作`)
+app.directive('perm', perm);
+
+// 6. 登录态 hydrate(必须在首次路由解析前:守卫链第①步要读 isLoggedIn)
+useLogin().hydrate();
+
+// 7. 挂载
+app.mount('#app');

+ 133 - 0
src/router/index.ts

@@ -0,0 +1,133 @@
+/**
+ * index.ts —— 路由表 + 守卫链(README §3.3 路由与鉴权 / §3.4 守卫链顺序)。
+ *
+ * 守卫链顺序(全局 beforeEach,按序短路):
+ *   ① 登录态:login.isLoggedIn(有 access 且未过期,提前 30s 判过期);
+ *   ② 刷新:access 过期但有 refresh → 静默 refresh(refreshAccessToken 单例)→ 成功放行 / 失败跳登录;
+ *   ③ 白名单:登录页/SSO 回调等公开路由直接放行(在 Auth 守卫之前,对齐后端"公开路由先注册"纪律);
+ *   ④ 作用域:路由 meta.scopeRequired(默认 true)且 scope 未就绪 → 跳作用域选择页。
+ *
+ * 路由 path 与后端端点对齐(`/{端}/{模块}/{资源}`,README §3.1);市场角色登录入口暂禁用——
+ * 不注册任何市场端路由(后续启用时单独拍板,勿自行加)。
+ */
+import { useLogin } from '@/store/login';
+import { useScope } from '@/store/scope';
+import { refreshAccessToken } from '@/api/http';
+import type { RouteRecordRaw } from 'vue-router';
+
+/** 路由 meta 扩展(业务路由可声明的守卫参数) */
+declare module 'vue-router' {
+  interface RouteMeta {
+    /** 是否需要登录(默认 true;登录页/SSO 回调设 false) */
+    public?: boolean;
+    /** 是否要求作用域就绪(默认 true;登录页/作用域选择页设 false) */
+    scopeRequired?: boolean;
+    /** 路由标题(document.title 同步用,中文) */
+    title?: string;
+  }
+}
+
+/**
+ * 路由表(createRouter 的 routes 入参;vite define 注入为 VueRouter.createRouter)。
+ *
+ * 入参:无。
+ *
+ * 出参:
+ *   - RouteRecordRaw[]:四条记录——登录页(公开)/ 机构列表(org 域示范页)/
+ *     作用域选择页(公开)/ 兜底 404。
+ *
+ * 异常场景:
+ *   - 路径未匹配:通配路由落到 404 视图(本样例中 redirect 到登录页,避免白屏)。
+ */
+export const routes: RouteRecordRaw[] = [
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/login/index.vue'),
+    meta: { public: true, scopeRequired: false, title: '登录' },
+  },
+  {
+    path: '/org/scope',
+    name: 'ScopeSelect',
+    component: () => import('@/views/org/scope-select.vue'),
+    meta: { scopeRequired: false, title: '选择机构' },
+  },
+  {
+    path: '/org/institutions',
+    name: 'InstitutionList',
+    component: () => import('@/views/org/institution/list.vue'),
+    meta: { title: '机构管理' },
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    redirect: '/login',
+  },
+];
+
+/**
+ * 创建路由实例(history 模式;生产由 nginx 按 location 回退 index.html)。
+ *
+ * 入参:无。
+ *
+ * 出参:
+ *   - 路由实例:main.ts 装配用。
+ *
+ * 异常场景:
+ *   - 非法 URL(带未编码字符):createWebHistory 内部兜底,不抛。
+ */
+export function buildRouter() {
+  return createRouter({
+    history: createWebHistory(),
+    routes,
+  });
+}
+
+/**
+ * 全局前置守卫(挂载后生效;README §3.3 守卫链)。
+ *
+ * 入参(由 vue-router 注入):
+ *   - to:目标路由(含 meta)。
+ *   - from:来源路由(本样例未使用,保留签名完整性)。
+ *
+ * 出参:
+ *   - 省略返回值 = 放行;返回 false / 跳转 = 拦截。
+ *
+ * 异常场景:
+ *   - 守卫内 pinia 未激活(极端早启动):useLogin 抛错 → 由全局 Recover 思路兜底(本仓为纯前端,
+ *     表现为控制台报错 + 页面不跳转;main.ts 保证 app.use(pinia) 早于 router.push 首帧)。
+ */
+export function setupGuards(router: ReturnType<typeof buildRouter>): void {
+  router.beforeEach(async (to, _from) => {
+    const login = useLogin();
+    const scope = useScope();
+
+    // ③ 白名单(放在登录判断前:未登录访问公开页直接放行)
+    if (to.meta.public) {
+      return true;
+    }
+
+    // ① 登录态
+    if (!login.isLoggedIn) {
+      // ② 有 refresh 则静默刷新(单例;并发多路由切换只刷一次)
+      const refreshed = await refreshAccessToken();
+      if (refreshed && login.accessToken) {
+        login.hydrate();
+        return true;
+      }
+      // 刷新失败:清本地态(refresh 失败时 http 层已 clear,此处幂等兜底)后跳登录
+      return { name: 'Login' };
+    }
+
+    // ④ 作用域就绪
+    if (to.meta.scopeRequired !== false && !scope.ready) {
+      return { name: 'ScopeSelect' };
+    }
+
+    return true;
+  });
+
+  // 路由标题同步(document.title 中文,README §1.4 默认简体中文)
+  router.afterEach((to) => {
+    document.title = to.meta.title ? `${to.meta.title} · 统一管理平台` : '统一管理平台';
+  });
+}

+ 150 - 0
src/store/login.ts

@@ -0,0 +1,150 @@
+/**
+ * login.ts —— 登录态 store(Pinia setup 风格;README §3.2 状态最小化)。
+ *
+ * 职责边界:
+ *   - 持有**响应式视图**(token/角色/权限码),供组件 `useLogin()` 读取渲染菜单/按钮;
+ *   - token 的**物理存取**全部经 identity/token-store(本文件与 http.ts 都不直接碰 localStorage);
+ *   - 登录/登出动作封装在此(调 api/auth/login.ts → 写 token-store → 同步视图)。
+ *
+ * 典型使用(组件内):
+ *
+ *   const login = useLogin();
+ *   v-perm="'academic.fee.refund'" 指令读 login.hasPerm;
+ *   <template>{{ login.realName }}</template>
+ */
+import { logout as apiLogout, type LoginResult } from '@/api/auth/login';
+import { clearTokens, getAccessToken, getExpiresAt, getRefreshToken, setTokens } from '@/identity/token-store';
+import type { ComputedRef, Ref } from 'vue';
+
+/**
+ * setup store 的返回面类型(defineStore 的 setup 函数返回值;字段为 ref 本体)。
+ * 注:经 `useLogin()` 消费时 Pinia **自动解包**(组件里 login.accessToken 是 string | null),
+ * 本接口仅约束 setup 函数返回,消费方以 useLogin() 推断类型为准。
+ */
+export interface LoginStore {
+  /** access token(响应式视图;物理值以 token-store 为准) */
+  accessToken: Ref<string | null>;
+  /** 过期时间(Unix 毫秒;null = 无记录,按已过期处理) */
+  expiresAt: Ref<number | null>;
+  /** 当前用户真实姓名(未登录为 '') */
+  realName: Ref<string>;
+  /** 角色码数组(7 角色字典;未登录为 []) */
+  roles: Ref<number[]>;
+  /** RBAC 权限码集合(`域.资源.动作`;菜单/按钮显隐唯一依据,README §3.1) */
+  permissions: Ref<Set<string>>;
+  /** 是否已登录(有 access token 且未过期;守卫链第一步) */
+  isLoggedIn: ComputedRef<boolean>;
+  /** 判断是否持有指定权限码(v-perm 指令用) */
+  hasPerm: (perm: string) => boolean;
+  /** 登录成功后的凭证落盘(http 拦截器/组件不得自行写) */
+  applyLogin: (data: LoginResult) => void;
+  /** 执行登出(调后端吊销 → 清本地态) */
+  doLogout: () => Promise<void>;
+  /** 从 token-store 同步一次本地视图(页面刷新后 main.ts 调用,恢复 UI 状态) */
+  hydrate: () => void;
+}
+
+export const useLogin = defineStore('login', (): LoginStore => {
+  const accessToken = ref<string | null>(null);
+  const expiresAt = ref<number | null>(null);
+  const realName = ref('');
+  const roles = ref<number[]>([]);
+  const permissions = ref(new Set<string>());
+
+  /** 已登录判定:有 access token 且未过期(提前 30s 视为过期,避免临界请求撞上 1002) */
+  const isLoggedIn = computed(() => {
+    if (!accessToken.value) return false;
+    const exp = expiresAt.value;
+    return exp === null || Date.now() < exp - 30_000;
+  });
+
+  /**
+   * 判断是否持有指定权限码。
+   *
+   * 入参:
+   *   - perm string:`域.资源.动作` 三段式权限码(如 `academic.fee.refund`)。
+   *
+   * 出参:
+   *   - boolean:未登录恒 false(fail-closed);登录态按 permissions 集合精确匹配。
+   */
+  function hasPerm(perm: string): boolean {
+    return isLoggedIn.value && permissions.value.has(perm);
+  }
+
+  /**
+   * 登录成功凭证落盘(token-store 物理写入 + 响应式视图同步)。
+   *
+   * 入参:
+   *   - data LoginResult:api/auth/login.ts 返回的 data(code=200 时)。
+   *
+   * 出参:无。
+   * 异常场景:
+   *   - data 缺 accessToken:视为后端契约违约,视图不落盘(isLoggedIn 保持 false)。
+   */
+  function applyLogin(data: LoginResult): void {
+    if (!data.accessToken) return;
+    setTokens({ access: data.accessToken, refresh: data.refreshToken, expiresAt: data.expiresAt });
+    accessToken.value = data.accessToken;
+    expiresAt.value = data.expiresAt;
+    realName.value = data.realName;
+    roles.value = data.roles ?? [];
+    permissions.value = new Set(data.permissions ?? []);
+  }
+
+  /**
+   * 执行登出:调后端吊销 → 清本地态(无论后端是否成功,本地态必清——登出不允许卡住)。
+   *
+   * 入参:无。
+   *
+   * 出参:
+   *   - Promise<void>:恒 resolve(失败场景静默,网络错误不阻塞登出)。
+   */
+  async function doLogout(): Promise<void> {
+    const refresh = getRefreshToken();
+    try {
+      if (refresh) await apiLogout(refresh);
+    } catch {
+      // 网络故障/后端错误:不阻塞本地登出(jti 黑名单到 access 30min 后自然失效)
+    }
+    clearTokens();
+    accessToken.value = null;
+    expiresAt.value = null;
+    realName.value = '';
+    roles.value = [];
+    permissions.value = new Set();
+  }
+
+  /**
+   * 从 token-store 同步本地视图(main.ts 启动时调用,恢复刷新前的 UI 状态)。
+   *
+   * 入参:无。
+   *
+   * 出参:无。
+   * 异常场景:
+   *   - 无 token(未登录):视图置空,守卫链引导到登录页。
+   */
+  function hydrate(): void {
+    accessToken.value = getAccessToken();
+    expiresAt.value = getExpiresAt();
+    // 刷新后 roles/permissions 未知(后端未提供"凭 access 取用户中心资料"端点前),
+    // 首次请求 401 触发 refresh 流程;完整恢复依赖后续 /api/auth/me 端点(规划项)。
+    if (!accessToken.value) {
+      realName.value = '';
+      roles.value = [];
+      permissions.value = new Set();
+    }
+  }
+
+  return {
+    accessToken,
+    expiresAt,
+    realName,
+    roles,
+    permissions,
+    isLoggedIn,
+    hasPerm,
+    applyLogin,
+    doLogout,
+    hydrate,
+  };
+});

+ 89 - 0
src/store/scope.ts

@@ -0,0 +1,89 @@
+/**
+ * scope.ts —— 多租户作用域 store(institution/campus;README §3.1 结构铁律 1)。
+ *
+ * 纪律(对齐后端 scope 包"强制作用域注入"):
+ *   - 所有业务页面查询参数里的 institutionId/campusId **只从 useScope() 取**(use-list-page.ts 自动注入),
+ *     页面禁止各自拼参(消灭旧系统控制器隐式 schoolid/campusid 坑);
+ *   - 超管(scope 未就绪)状态下守卫链拦截,页面不得裸查。
+ *
+ * 典型使用:
+ *
+ *   const scope = useScope();
+ *   scope.set({ institutionId: 12, campusId: 3 }); // 作用域选择器组件提交时调用
+ *   const q = scope.toQuery(); // { institutionId: 12, campusId: 3 } → 并入列表请求 query
+ */
+/** 作用域状态(机构必选;校区 0 = 机构级不限制) */
+export interface ScopeState {
+  /** 机构 id(作用域第一级;旧系统 school 概念归一) */
+  institutionId: number;
+  /** 校区 id(作用域第二级;0 = 机构级,查全机构数据) */
+  campusId: number;
+}
+
+import type { ComputedRef, Ref } from 'vue';
+
+/**
+ * setup store 的返回面类型(defineStore 的 setup 函数返回值;字段为 ref 本体)。
+ * 注:经 `useScope()` 消费时 Pinia **自动解包**(组件里 scope.current 是 ScopeState),
+ * 本接口仅约束 setup 函数返回,消费方以 useScope() 推断类型为准。
+ */
+export interface ScopeStore {
+  /** 当前作用域(未设置时 institutionId=0 表示"未就绪") */
+  current: Ref<ScopeState>;
+  /** 作用域是否就绪(institutionId > 0;守卫链第四步检查) */
+  ready: ComputedRef<boolean>;
+  /** 设置作用域(作用域选择器提交 / 登录后按 token 下发初始化) */
+  set: (sc: ScopeState) => void;
+  /** 转为请求 query 片段(useListPage 自动并入;campus=0 时省略 campusId 参数) */
+  toQuery: () => { institutionId: number; campusId?: number };
+  /** 清空(切换机构前 / 登出时调用) */
+  reset: () => void;
+}
+
+export const useScope = defineStore('scope', (): ScopeStore => {
+  const current = ref<ScopeState>({ institutionId: 0, campusId: 0 });
+
+  /** 就绪判定:机构 id 已设置(campus 可为 0 = 机构级) */
+  const ready = computed(() => current.value.institutionId > 0);
+
+  /**
+   * 设置作用域。
+   *
+   * 入参:
+   *   - sc ScopeState:institutionId 必填 > 0;campusId 可 0(机构级)。
+   *
+   * 出参:无。
+   * 异常场景:
+   *   - sc.institutionId ≤ 0:视为非法输入,拒绝设置并 console.error(调用方 bug,单测应覆盖)。
+   */
+  function set(sc: ScopeState): void {
+    if (!sc || sc.institutionId <= 0) {
+      console.error('[scope] 非法作用域:institutionId 必须 > 0', sc);
+      return;
+    }
+    current.value = { institutionId: sc.institutionId, campusId: sc.campusId ?? 0 };
+  }
+
+  /**
+   * 转为请求 query 片段。
+   *
+   * 入参:无。
+   *
+   * 出参:
+   *   - { institutionId, campusId? }:campusId=0 时省略该键(后端 0 语义 = 不过滤,显式省略更清晰)。
+   * 异常场景:
+   *   - 未就绪(institutionId=0):返回 institutionId: 0,后端按 1004 作用域越界拒绝(fail-closed)。
+   */
+  function toQuery(): { institutionId: number; campusId?: number } {
+    const q: { institutionId: number; campusId?: number } = { institutionId: current.value.institutionId };
+    if (current.value.campusId > 0) q.campusId = current.value.campusId;
+    return q;
+  }
+
+  /** 清空作用域(登出 / 切换机构前调用;页面随后被守卫拦截) */
+  function reset(): void {
+    current.value = { institutionId: 0, campusId: 0 };
+  }
+
+  return { current, ready, set, toQuery, reset };
+});

+ 35 - 0
src/styles/variables.scss

@@ -0,0 +1,35 @@
+/*
+ * variables.scss —— 全局设计 token(README §2 styles 目录:组件禁硬编码色值/尺寸)。
+ *
+ * 用法:
+ *   - 业务组件 <style scoped> 内直接 var(--app-xxx) 使用(本文件在 main.ts 前由 index.html 引入,
+ *     或经 Vite 的 CSS preprocessor 全局注入);
+ *   - Element Plus 主题变量覆盖写在 element-plus-theme.scss(同名 token 映射 EP CSS 变量)。
+ * 新增 token 规则:只增不删(旧 token 可能被多页面引用);命名 --app-{类别}-{语义}。
+ */
+:root {
+  /* 文本 */
+  --app-text-primary: #303133;
+  --app-text-secondary: #909399;
+  --app-text-disabled: #c0c4cc;
+
+  /* 背景 */
+  --app-bg-page: #f5f7fa;
+  --app-bg-card: #ffffff;
+
+  /* 间距(4px 基数) */
+  --app-spacing-xs: 4px;
+  --app-spacing-sm: 8px;
+  --app-spacing-md: 16px;
+  --app-spacing-lg: 24px;
+
+  /* 字号 */
+  --app-font-sm: 12px;
+  --app-font-md: 14px;
+  --app-font-lg: 16px;
+
+  /* 圆角/阴影 */
+  --app-radius-sm: 4px;
+  --app-radius-md: 6px;
+  --app-shadow-card: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
+}

+ 59 - 0
src/types/api.d.ts

@@ -0,0 +1,59 @@
+/**
+ * api.d.ts —— 前后端统一契约类型(README §3.4;后端落点 response.go 的 R / PageData,仓库 commit da3fe5b)。
+ *
+ * 纪律(README §3.5):
+ *   - 本文件是 R<T> / PageData<T> 的**唯一定义点**,全仓只 import 不重定义;
+ *   - 各域 DTO 定义在 src/api/{域}/{资源}.ts 内,与本文件字段**逐字段**对齐(camelCase)。
+ */
+
+/**
+ * 统一响应信封(后端 R 结构;所有接口 HTTP 恒 200,真实状态在 body code)。
+ *
+ * 典型 JSON 形态:
+ *   - 单实体成功:{"code":200,"message":"ok","data":{"id":"..."}}
+ *   - 分页成功:  {"code":200,"message":"ok","data":{"list":[...],"total":42,"page":1,"pageSize":10,"pages":5}}
+ *   - 业务失败:  {"code":3001,"message":"记录不存在","success":false}(data 省略)
+ *
+ * 泛型参数:
+ *   - T:业务数据体类型;分页接口传 PageData<T>,单实体接口传实体 DTO,操作类接口传 undefined。
+ */
+export interface R<T = undefined> {
+  /** 业务状态码:200 成功;失败为分段码(1xxx 认证/2xxx 参数/3xxx 数据/4xxx 依赖/5xxx 系统) */
+  code: number;
+  /** 业务数据体(失败时后端省略该字段) */
+  data?: T;
+  /** 面向用户的错误文案(已脱敏;成功恒为 "ok") */
+  message: string;
+  /** 成功标志(与 code 冗余,兼容旧前端判断习惯) */
+  success: boolean;
+}
+
+/**
+ * 统一分页数据体(后端 PageData 结构;前端 useListPage 统一解构对象,README §3.4)。
+ *
+ * 泛型参数:
+ *   - T:list 内单条记录类型(各域 DTO)。
+ */
+export interface PageData<T> {
+  /** 当前页数据切片(后端保证非 null,空页为 []) */
+  list: T[];
+  /** 符合筛选条件的总记录数(分页器"共 N 条") */
+  total: number;
+  /** 当前页码(从 1 起,回显请求 query) */
+  page: number;
+  /** 每页条数(回显请求 query;后端钳制 ≤ 200) */
+  pageSize: number;
+  /** 总页数 = ceil(total / pageSize) */
+  pages: number;
+}
+
+/** 分页响应信封 = R<PageData<T>> 的语法糖 */
+export type PageR<T> = R<PageData<T>>;
+
+/** 分页请求 query 参数(默认 1/10,上限 200,与后端 ParsePage 约定一致) */
+export interface PageQuery {
+  /** 页码,从 1 开始(缺省 1) */
+  page?: number;
+  /** 每页条数(缺省 10,>200 由后端钳制) */
+  pageSize?: number;
+}

+ 67 - 0
src/utils/format.ts

@@ -0,0 +1,67 @@
+/**
+ * format.ts —— 纯函数格式化工具集(README §2 utils 目录:零 Vue 依赖、零 store 依赖)。
+ *
+ * 纪律(README §3.3-10 脱敏):手机号/身份证**直接展示后端已脱敏值**,前端不存明文、
+ * 不二次脱敏;本文件只提供"格式化"(数字千分位、日期显示、状态文案),不做"脱敏"。
+ *
+ * 典型使用:
+ *
+ *   import { formatMoney, formatDateTime, formatStatus } from '@/utils/format';
+ *   formatMoney(1234.5)      // "1,234.50"
+ *   formatDateTime(row.createdAt) // "2026-11-06 08:30"
+ */
+
+/**
+ * 金额格式化(千分位 + 两位小数)。
+ *
+ * 入参:
+ *   - value number:原始金额(元);NaN/负无穷时按 0 处理(后端金额不应出现,防御性兜底)。
+ *
+ * 出参:
+ *   - string:如 "1,234.50";整数值不带小数点后的多余 0("1,235.00" 仍保留两位,财务口径统一两位)。
+ *
+ * 异常场景:
+ *   - value 非有限数:返回 "0.00"(调用方应排查上游数据,不抛错)。
+ */
+export function formatMoney(value: number): string {
+  const n = Number.isFinite(value) ? value : 0;
+  return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+}
+
+/**
+ * 日期时间格式化(RFC3339 → 本地 "YYYY-MM-DD HH:mm")。
+ *
+ * 入参:
+ *   - iso string:后端 RFC3339 时间串(如 "2026-11-06T08:30:00Z");空串/非法串返回 '-'。
+ *
+ * 出参:
+ *   - string:本地时区 "2026-11-06 08:30";解析失败返回 '-'(占位,避免渲染 1970 年)。
+ *
+ * 异常场景:
+ *   - iso 为空/非法:返回 '-'(列表页空时间统一占位,不显示 Invalid Date)。
+ */
+export function formatDateTime(iso: string): string {
+  if (!iso) return '-';
+  const d = new Date(iso);
+  if (Number.isNaN(d.getTime())) return '-';
+  const p = (n: number) => String(n).padStart(2, '0');
+  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
+}
+
+/**
+ * 状态码 → 中文文案(后端 status 枚举 1 启用 / 0 停用)。
+ *
+ * 入参:
+ *   - status number:状态码;未知值返回 '-'(不臆造文案)。
+ *
+ * 出参:
+ *   - string:"启用" / "停用" / "-"。
+ *
+ * 异常场景:
+ *   - status 非 0/1:返回 '-'(后端字典变化时此处是唯一定点,便于全局排查)。
+ */
+export function formatStatus(status: number): string {
+  if (status === 1) return '启用';
+  if (status === 0) return '停用';
+  return '-';
+}

+ 116 - 0
src/views/login/index.vue

@@ -0,0 +1,116 @@
+<script setup lang="ts">
+/**
+ * 登录页(views/login/index.vue;公开路由,守卫 meta.public)。
+ *
+ * 职责:账号密码登录 → 成功则写 token-store + 跳机构列表;失败展示后端 message(直显,不包装)。
+ * SFC 骨架顺序(README §3.2):imports → props/emits(无)→ store 注入 → 状态 → 生命周期 → 方法。
+ */
+import { login } from '@/api/auth/login';
+import { useLogin } from '@/store/login';
+
+const loginStore = useLogin();
+
+/** 表单数据(单一数据源;rules 校验只拦格式,业务错误由后端 code 裁决) */
+const form = reactive({
+  username: '',
+  password: '',
+});
+
+/** 提交中(防重复提交) */
+const submitting = ref(false);
+/** 登录失败文案(后端 message 直显;空串 = 无错误) */
+const errorMsg = ref('');
+
+/** 表单校验规则(显式 rules 对象,README §3.3-5) */
+const rules = {
+  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+};
+
+/**
+ * 提交登录(handle 前缀:用户触发的提交类方法,README §3.1 命名规则)。
+ *
+ * 入参:无(读 form 当前值)。
+ *
+ * 出参:
+ *   - Promise<void>:恒 resolve;成功跳机构列表,失败写 errorMsg。
+ * 异常场景:
+ *   - 表单校验失败:不发请求(ElForm 标红);
+ *   - code !== 200(如 1005 账号密码错误):errorMsg = 后端 message;
+ *   - 网络层 reject:errorMsg = "网络连接异常"。
+ */
+async function handleSubmit(): Promise<void> {
+  if (submitting.value) return;
+  errorMsg.value = '';
+  submitting.value = true;
+  try {
+    const res = await login({ username: form.username, password: form.password });
+    if (res.code === 200 && res.data) {
+      loginStore.applyLogin(res.data);
+      window.location.href = '/org/institutions';
+      return;
+    }
+    errorMsg.value = res.message || '登录失败';
+  } catch {
+    errorMsg.value = '网络连接异常';
+  } finally {
+    submitting.value = false;
+  }
+}
+</script>
+
+<template>
+  <div class="login-page">
+    <el-card class="login-card">
+      <template #header>
+        <span class="login-title">统一管理平台</span>
+      </template>
+
+      <el-alert v-if="errorMsg" :title="errorMsg" type="error" show-icon :closable="false" class="login-error" />
+
+      <el-form :model="form" :rules="rules" label-width="72px" @submit.prevent="handleSubmit">
+        <el-form-item label="用户名" prop="username">
+          <el-input v-model="form.username" placeholder="请输入用户名" autocomplete="username" />
+        </el-form-item>
+        <el-form-item label="密码" prop="password">
+          <el-input
+            v-model="form.password"
+            type="password"
+            placeholder="请输入密码"
+            autocomplete="current-password"
+            show-password
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :loading="submitting" native-type="submit" class="login-submit">
+            登 录
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<style scoped>
+.login-page {
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: var(--app-bg-page);
+}
+.login-card {
+  width: 400px;
+}
+.login-title {
+  font-size: var(--app-font-lg);
+  font-weight: 600;
+  color: var(--app-text-primary);
+}
+.login-error {
+  margin-bottom: var(--app-spacing-md);
+}
+.login-submit {
+  width: 100%;
+}
+</style>

+ 89 - 0
src/views/org/institution/list.vue

@@ -0,0 +1,89 @@
+<script setup lang="ts">
+/**
+ * 机构管理列表页(views/org/institution/list.vue)—— useListPage + ListPage + PageData 统一分页的完整示范。
+ *
+ * 链路(README §3.4 可复用资产):
+ *   本页面 ── fetcher(并入 scope.toQuery()) ──> listInstitutions ──> useListPage 统一解 data.{list,total}
+ *   分页器 v-model ──> lp.page/lp.pageSize ──> useListPage watch 自动重载
+ *
+ * 作用域纪律(README §3.3-4):查询参数里的 institutionId/campusId 只从 useScope().toQuery() 取,
+ * 页面不出现任何硬编码作用域数值。
+ */
+import { listInstitutions, type InstitutionDTO, type InstitutionFilter } from '@/api/org/institution';
+import ListPage from '@/components/ListPage.vue';
+import type { ListColumn } from '@/components/list-column';
+import { useListPage } from '@/composables/use-list-page';
+import { useScope } from '@/store/scope';
+import { formatDateTime, formatStatus } from '@/utils/format';
+
+const scope = useScope();
+
+/** 筛选条件(页面级状态;工具区插槽控件绑定) */
+const filters = reactive<InstitutionFilter>({
+  keyword: '',
+  status: undefined,
+});
+
+/**
+ * 列表状态(useListPage 收口;fetcher 内并入作用域)。
+ * 顶层解构:setup 顶层 ref 在模板中自动解包且可写(v-model:page="page" 合法);
+ * 若整对象返回(lp.page),模板里是未解包的 Ref 对象,v-model 会写入失败。
+ */
+const { rows, total, page, pageSize, loading, error, fetchList } = useListPage<InstitutionDTO>({
+  fetcher: (p, ps) => listInstitutions({ ...scope.toQuery(), ...filters, page: p, pageSize: ps }),
+});
+
+/** 列定义(数据驱动;formatter 返回字符串,README §3.3-6) */
+const columns: ListColumn<InstitutionDTO>[] = [
+  { prop: 'name', label: '机构名称', minWidth: 200 },
+  { prop: 'creditCode', label: '统一社会信用代码', minWidth: 200 },
+  { prop: 'status', label: '状态', formatter: (row) => formatStatus(row.status) },
+  { prop: 'createdAt', label: '创建时间', formatter: (row) => formatDateTime(row.createdAt) },
+];
+
+/** 工具区"搜索"按钮 → 重置到第 1 页重载(筛选变化必须回首页,防越界空页) */
+function onSearch(): void {
+  page.value = 1;
+  void fetchList();
+}
+
+/** 初始加载(作用域已由守卫链保证就绪) */
+onMounted(() => {
+  void fetchList();
+});
+</script>
+
+<template>
+  <div class="institution-list">
+    <ListPage
+      v-model:page="page"
+      v-model:page-size="pageSize"
+      :rows="rows"
+      :total="total"
+      :loading="loading"
+      :error="error"
+      :columns="columns"
+      @search="onSearch"
+    >
+      <template #toolbar>
+        <el-input v-model="filters.keyword" placeholder="搜索机构名称" clearable class="toolbar-input" />
+        <el-select v-model="filters.status" placeholder="状态" clearable class="toolbar-select">
+          <el-option :value="1" label="启用" />
+          <el-option :value="0" label="停用" />
+        </el-select>
+      </template>
+    </ListPage>
+  </div>
+</template>
+
+<style scoped>
+.institution-list {
+  padding: var(--app-spacing-md);
+}
+.toolbar-input {
+  width: 240px;
+}
+.toolbar-select {
+  width: 140px;
+}
+</style>

+ 92 - 0
src/views/org/scope-select.vue

@@ -0,0 +1,92 @@
+<script setup lang="ts">
+/**
+ * 作用域选择页(views/org/scope-select.vue;守卫链第④步的落点,meta.scopeRequired=false)。
+ *
+ * 职责:登录态有效但 scope 未就绪时,由守卫重定向到此页;
+ *       样例阶段仅做"机构就绪"占位交互(真实实现:调 org 域接口拉机构/校区列表联动选择,
+ *       属 org 域开发范围,本样例不实现业务请求——页面结构 + store 写入口已示范)。
+ *
+ * 典型使用(由路由守卫自动进入,页面不主动跳转):
+ *
+ *   scope.set({ institutionId, campusId })  →  scope.ready = true → 守卫放行后续路由
+ */
+import { useLogin } from '@/store/login';
+import { useScope } from '@/store/scope';
+
+const scope = useScope();
+const login = useLogin();
+
+/** 选择中的机构 id(真实实现替换为接口拉取的机构列表选项) */
+const institutionId = ref(0);
+
+/**
+ * 确认选择(handle 前缀:用户触发提交类)。
+ *
+ * 入参:无(读 institutionId 当前值;campusId 样例固定 0 = 机构级)。
+ *
+ * 出参:
+ *   - 无返回值;成功后路由回退到原目标(此处简化为跳机构列表)。
+ * 异常场景:
+ *   - institutionId 未选(=0):scope.set 内部拒绝并 console.error(store 层 fail-fast)。
+ */
+function handleConfirm(): void {
+  if (!institutionId.value) return;
+  scope.set({ institutionId: institutionId.value, campusId: 0 });
+  window.location.href = '/org/institutions';
+}
+
+/** 登出入口(页面右上角;调 login store 统一登出流程) */
+async function handleLogout(): Promise<void> {
+  await login.doLogout();
+  window.location.href = '/login';
+}
+</script>
+
+<template>
+  <div class="scope-page">
+    <el-card class="scope-card">
+      <template #header>
+        <div class="scope-header">
+          <span class="scope-title">选择机构</span>
+          <el-button link type="primary" @click="handleLogout">退出登录</el-button>
+        </div>
+      </template>
+
+      <el-form label-width="72px">
+        <el-form-item label="机构">
+          <el-select v-model="institutionId" placeholder="请选择机构" style="width: 320px">
+            <!-- 样例占位选项;org 域开发时替换为接口数据 -->
+            <el-option :value="1" label="示例机构 A" />
+            <el-option :value="2" label="示例机构 B" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :disabled="!institutionId" @click="handleConfirm">进入</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<style scoped>
+.scope-page {
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: var(--app-bg-page);
+}
+.scope-card {
+  width: 480px;
+}
+.scope-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.scope-title {
+  font-size: var(--app-font-lg);
+  font-weight: 600;
+  color: var(--app-text-primary);
+}
+</style>

+ 29 - 0
tsconfig.json

@@ -0,0 +1,29 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "types": ["vite/client"],
+
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "noImplicitOverride": true,
+    "useDefineForClassFields": true,
+
+    "esModuleInterop": true,
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "skipLibCheck": true,
+    "noEmit": true,
+
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": ["global.d.ts", "src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts", "shims/**/*.ts"],
+  "exclude": ["dist", "vendor", "node_modules"]
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 0
vendor/axios@1.21.0/axios.min.js


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
vendor/element-plus@2.14.7/index.css


Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 0
vendor/element-plus@2.14.7/index.full.min.js


Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 0
vendor/element-plus@2.14.7/locale/zh-cn.min.js


Datei-Diff unterdrückt, da er zu groß ist
+ 5 - 0
vendor/pinia@2.3.1/pinia.iife.prod.js


+ 126 - 0
vendor/vue-demi@0.14.10/vue-demi.iife.js

@@ -0,0 +1,126 @@
+var _VueDemiGlobal = typeof globalThis !== 'undefined' 
+  ? globalThis
+  : typeof global !== 'undefined'
+    ? global
+    : typeof self !== 'undefined'
+      ? self
+      : this
+var VueDemi = (function (VueDemi, Vue, VueCompositionAPI) {
+  if (VueDemi.install) {
+    return VueDemi
+  }
+  if (!Vue) {
+    console.error('[vue-demi] no Vue instance found, please be sure to import `vue` before `vue-demi`.')
+    return VueDemi
+  }
+
+  // Vue 2.7
+  if (Vue.version.slice(0, 4) === '2.7.') {
+    for (var key in Vue) {
+      VueDemi[key] = Vue[key]
+    }
+    VueDemi.isVue2 = true
+    VueDemi.isVue3 = false
+    VueDemi.install = function () {}
+    VueDemi.Vue = Vue
+    VueDemi.Vue2 = Vue
+    VueDemi.version = Vue.version
+    VueDemi.warn = Vue.util.warn
+    VueDemi.hasInjectionContext = function() {
+      return !!VueDemi.getCurrentInstance()
+    }
+    function createApp(rootComponent, rootProps) {
+      var vm
+      var provide = {}
+      var app = {
+        config: Vue.config,
+        use: Vue.use.bind(Vue),
+        mixin: Vue.mixin.bind(Vue),
+        component: Vue.component.bind(Vue),
+        provide: function (key, value) {
+          provide[key] = value
+          return this
+        },
+        directive: function (name, dir) {
+          if (dir) {
+            Vue.directive(name, dir)
+            return app
+          } else {
+            return Vue.directive(name)
+          }
+        },
+        mount: function (el, hydrating) {
+          if (!vm) {
+            vm = new Vue(Object.assign({ propsData: rootProps }, rootComponent, { provide: Object.assign(provide, rootComponent.provide) }))
+            vm.$mount(el, hydrating)
+            return vm
+          } else {
+            return vm
+          }
+        },
+        unmount: function () {
+          if (vm) {
+            vm.$destroy()
+            vm = undefined
+          }
+        },
+      }
+      return app
+    }
+    VueDemi.createApp = createApp
+  }
+  // Vue 2.6.x
+  else if (Vue.version.slice(0, 2) === '2.') {
+    if (VueCompositionAPI) {
+      for (var key in VueCompositionAPI) {
+        VueDemi[key] = VueCompositionAPI[key]
+      }
+      VueDemi.isVue2 = true
+      VueDemi.isVue3 = false
+      VueDemi.install = function () {}
+      VueDemi.Vue = Vue
+      VueDemi.Vue2 = Vue
+      VueDemi.version = Vue.version
+      VueDemi.hasInjectionContext = function() {
+        return !!VueDemi.getCurrentInstance()
+      }
+    } else {
+      console.error('[vue-demi] no VueCompositionAPI instance found, please be sure to import `@vue/composition-api` before `vue-demi`.')
+    }
+  }
+  // Vue 3
+  else if (Vue.version.slice(0, 2) === '3.') {
+    for (var key in Vue) {
+      VueDemi[key] = Vue[key]
+    }
+    VueDemi.isVue2 = false
+    VueDemi.isVue3 = true
+    VueDemi.install = function () {}
+    VueDemi.Vue = Vue
+    VueDemi.Vue2 = undefined
+    VueDemi.version = Vue.version
+    VueDemi.set = function (target, key, val) {
+      if (Array.isArray(target)) {
+        target.length = Math.max(target.length, key)
+        target.splice(key, 1, val)
+        return val
+      }
+      target[key] = val
+      return val
+    }
+    VueDemi.del = function (target, key) {
+      if (Array.isArray(target)) {
+        target.splice(key, 1)
+        return
+      }
+      delete target[key]
+    }
+  } else {
+    console.error('[vue-demi] Vue version ' + Vue.version + ' is unsupported.')
+  }
+  return VueDemi
+})(
+  (_VueDemiGlobal.VueDemi = _VueDemiGlobal.VueDemi || (typeof VueDemi !== 'undefined' ? VueDemi : {})),
+  _VueDemiGlobal.Vue || (typeof Vue !== 'undefined' ? Vue : undefined),
+  _VueDemiGlobal.VueCompositionAPI || (typeof VueCompositionAPI !== 'undefined' ? VueCompositionAPI : undefined)
+);

Datei-Diff unterdrückt, da er zu groß ist
+ 5 - 0
vendor/vue-router@4.6.4/vue-router.global.prod.js


Datei-Diff unterdrückt, da er zu groß ist
+ 4 - 0
vendor/vue@3.5.45/vue.global.prod.js


+ 122 - 0
vite.config.ts

@@ -0,0 +1,122 @@
+import { fileURLToPath, URL } from 'node:url';
+
+import vue from '@vitejs/plugin-vue';
+import { defineConfig } from 'vite';
+
+/**
+ * 框架运行时全走 UMD 全局(README §3.3-1「禁 import 框架」)的构建侧落地:
+ *
+ * ① define 注入:业务 .ts/.vue 代码里的**裸标识符**(ref / computed / defineStore / createRouter …)
+ *    在编译期被替换为 Vue.* / Pinia.* / VueRouter.* 全局访问。
+ *    注意:vite 的 define 按标识符整词替换(带点号前缀的 obj.ref 不替换),
+ *    但**解构/对象键位置不做上下文分析**——因此禁止 `const { ref } = Vue` 这类写法
+ *    (README §3.3-1:要么裸写 ref(),要么显式 Vue.ref())。
+ *
+ * ② alias 指向 shims:SFC 编译器产物形如 `import { openBlock } from 'vue'`,
+ *    经别名重定向到 shims/*.ts(运行时从 UMD 全局转发,类型来自安装的框架 dev 依赖)。
+ *    该层是**唯一允许接触框架模块名**的地方,业务代码一律不写。
+ */
+
+/** 可裸写的 Vue 组合式 API 标识符(编译期注入为 Vue.*) */
+const VUE_APIS: readonly string[] = [
+  'ref',
+  'shallowRef',
+  'reactive',
+  'shallowReactive',
+  'readonly',
+  'shallowReadonly',
+  'computed',
+  'watch',
+  'watchEffect',
+  'watchPostEffect',
+  'watchSyncEffect',
+  'effectScope',
+  'onMounted',
+  'onUnmounted',
+  'onBeforeMount',
+  'onBeforeUnmount',
+  'onUpdated',
+  'onBeforeUpdate',
+  'onActivated',
+  'onDeactivated',
+  'onErrorCaptured',
+  'onRenderTracked',
+  'onRenderTriggered',
+  'onScopeDispose',
+  'nextTick',
+  'h',
+  'inject',
+  'provide',
+  'toRef',
+  'toRefs',
+  'toRaw',
+  'toValue',
+  'markRaw',
+  'isRef',
+  'isReactive',
+  'isReadonly',
+  'isVNode',
+  'customRef',
+  'triggerRef',
+  'unref',
+  'createApp',
+] as const;
+
+/** 可裸写的 Vue Router 标识符(注入为 VueRouter.*) */
+const VUE_ROUTER_APIS: readonly string[] = [
+  'createRouter',
+  'createWebHashHistory',
+  'createWebHistory',
+  'useRoute',
+  'useRouter',
+] as const;
+
+/** 可裸写的 Pinia 标识符(注入为 Pinia.*) */
+const PINIA_APIS: readonly string[] = [
+  'defineStore',
+  'createPinia',
+  'getActivePinia',
+  'setActivePinia',
+  'storeToRefs',
+] as const;
+
+const defineMap: Record<string, string> = {};
+for (const api of VUE_APIS) defineMap[api] = `Vue.${api}`;
+for (const api of VUE_ROUTER_APIS) defineMap[api] = `VueRouter.${api}`;
+for (const api of PINIA_APIS) defineMap[api] = `Pinia.${api}`;
+
+export default defineConfig(({ command }) => ({
+  plugins: [vue()],
+  /**
+   * dev(command === 'serve'):本地把 vendor/ 当 public 根,index.html 的相对路径
+   *   vendor/<库>@<版本>/<文件> 直接命中仓库文件,无需 OSS 即可联调;
+   * build:不拷贝任何框架文件进 dist(框架由 nginx/OSS 在运行时提供),
+   *   dist/ 只含业务产物 —— scripts/check-dist.mjs 对此有门禁。
+   */
+  publicDir: command === 'serve' ? 'vendor' : false,
+  resolve: {
+    alias: {
+      // 框架模块名 → 全局转发胶水层(唯一落点;业务代码禁写这些 import)
+      vue: fileURLToPath(new URL('./shims/vue.ts', import.meta.url)),
+      'vue-router': fileURLToPath(new URL('./shims/vue-router.ts', import.meta.url)),
+      pinia: fileURLToPath(new URL('./shims/pinia.ts', import.meta.url)),
+      // 业务层路径别名
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
+    },
+  },
+  define: defineMap,
+  server: {
+    port: 5173,
+    proxy: {
+      // dev 联调:/api 前缀请求代理到后端 dev 地址(可用环境变量覆盖)
+      '/api': {
+        target: process.env.VITE_API_PROXY_TARGET ?? 'http://127.0.0.1:8080',
+        changeOrigin: true,
+      },
+    },
+  },
+  build: {
+    // 质量门禁:主 chunk 上限(README §3.6);框架误打包时体积会显著超限
+    chunkSizeWarningLimit: 1024,
+  },
+}));

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.