Kaynağa Gözat

feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿)

结构:
- pnpm workspaces:packages/shared(三端共用,源码消费免构建)+ apps/teacher
- 根 tsconfig = shared+shims 项目;apps/teacher/tsconfig.json = 本端独立

资源引入 = UMD 混合方案(Vant 4 官方无 UMD 构建):
- Vue 系 5 项 UMD 自托管 OSS(vendor/ 入库,同 PC 纪律)
- Vant 4 走 npm 编译进 bundle(设计预期,dist 含 Vant 运行时)

teacher 端(4 门禁全绿):
- 8 步装配 + 嵌套路由 + 守卫链 4 步 + 4 页(login/layout/机构/打卡)
- tabTree 权限码下发(仅内存态)
- 微信 per-campus 授权占位

shared 层:
- identity/api/store/composables/directives/components/types 全量
- list-page 泛型 SFC + tabbar-menu 递归渲染

门禁:vue-tsc 0 / vite build 309模块 / eslint 0 / dist grep clean
文档:README 重写 + EXAMPLES.md 新建

affects: teacher(student/parent 待建)
未 push(push 需用户逐次确认)
Q 4 gün önce
ebeveyn
işleme
1aeaab47e4
46 değiştirilmiş dosya ile 6079 ekleme ve 53 silme
  1. 8 0
      .env.example
  2. 269 0
      EXAMPLES.md
  3. 198 53
      README.md
  4. 30 0
      apps/teacher/index.html
  5. 20 0
      apps/teacher/package.json
  6. 13 0
      apps/teacher/src/App.vue
  7. 60 0
      apps/teacher/src/api/academic/checkin.ts
  8. 52 0
      apps/teacher/src/api/org/institution.ts
  9. 75 0
      apps/teacher/src/main.ts
  10. 138 0
      apps/teacher/src/router/index.ts
  11. 34 0
      apps/teacher/src/styles/tokens.css
  12. 134 0
      apps/teacher/src/views/academic/checkin/list.vue
  13. 93 0
      apps/teacher/src/views/layout/index.vue
  14. 266 0
      apps/teacher/src/views/login/index.vue
  15. 83 0
      apps/teacher/src/views/org/institution/list.vue
  16. 22 0
      apps/teacher/tsconfig.json
  17. 83 0
      apps/teacher/vite.config.ts
  18. 83 0
      eslint.config.js
  19. 93 0
      global.d.ts
  20. 32 0
      package.json
  21. 17 0
      packages/shared/package.json
  22. 87 0
      packages/shared/src/api/auth/login.ts
  23. 80 0
      packages/shared/src/api/auth/wechat.ts
  24. 219 0
      packages/shared/src/api/http.ts
  25. 95 0
      packages/shared/src/components/list-page.vue
  26. 60 0
      packages/shared/src/components/tabbar-menu.vue
  27. 123 0
      packages/shared/src/composables/use-list-page.ts
  28. 63 0
      packages/shared/src/directives/perm.ts
  29. 110 0
      packages/shared/src/identity/token-store.ts
  30. 19 0
      packages/shared/src/index.ts
  31. 162 0
      packages/shared/src/store/login.ts
  32. 90 0
      packages/shared/src/store/scope.ts
  33. 88 0
      packages/shared/src/types/api.ts
  34. 2338 0
      pnpm-lock.yaml
  35. 5 0
      pnpm-workspace.yaml
  36. 91 0
      scripts/gen-shims.cjs
  37. 55 0
      shims/pinia.ts
  38. 67 0
      shims/vue-router.ts
  39. 343 0
      shims/vue.ts
  40. 23 0
      tsconfig.base.json
  41. 17 0
      tsconfig.json
  42. 1 0
      vendor/axios@1.21.0/axios.min.js
  43. 5 0
      vendor/pinia@2.3.1/pinia.iife.prod.js
  44. 126 0
      vendor/vue-demi@0.14.10/vue-demi.iife.js
  45. 5 0
      vendor/vue-router@4.6.4/vue-router.global.prod.js
  46. 4 0
      vendor/vue@3.5.45/vue.global.prod.js

+ 8 - 0
.env.example

@@ -0,0 +1,8 @@
+# 移动端环境变量模板(实际 .env.* 走 .gitignore,只入库本模板)。
+# 各端可用 apps/{端}/.env.* 覆盖;dev 走 vite proxy 时 VITE_API_BASE 可省。
+
+# 后端 base(dev 走 vite /api 代理时可省略;build 部署同源时为空)
+VITE_API_BASE=
+
+# 家长端微信 per-campus 授权(campus → appid 映射由后端下发,此处仅占位)
+VITE_WECHAT_APPID=

+ 269 - 0
EXAMPLES.md

@@ -0,0 +1,269 @@
+# 移动端前端代码样例索引(三段式注释 + 范式摘录)
+
+> 本文件是移动端仓库的**代码范式参考**(与 PC 端 `EXAMPLES.md` 同构)。
+> 所有 TS 文件采用**三段式注释**:① 入参 / ② 出参 / ③ 异常场景;SFC 骨架顺序:imports → store → 状态 → 生命周期 → 方法。
+> 文件索引列出当前已落地文件的职责 + 关键范式,新增文件照此范式写。
+
+## 1. 三段式注释模板
+
+```ts
+/**
+ * 文件头(一句话说清职责 + 所属域/层)
+ *
+ * 典型使用(调用方文件路径 + 代码片段):
+ *   ...
+ */
+
+/**
+ * 函数/方法注释。
+ *
+ * 入参:
+ *   - paramName Type:说明(必填/可选/默认值)。
+ * 出参:
+ *   - ReturnType:说明(成功/失败各返回什么)。
+ * 异常场景:
+ *   - 条件 A:行为 X(静默/抛错/降级/跳登录)。
+ */
+function xxx(param: Type): ReturnType { ... }
+```
+
+**规则**:
+- 入参/出参**必须**写(哪怕是"无");异常场景**必须**写(至少一条);
+- 禁止"见文档"式空注释;禁止复制粘贴函数名当注释;
+- SFC 内:`<script setup>` 顶部文件头 + 每个方法三段式(可省略"无参/无出参"但异常场景不可省);
+- 模板注释用 `<!-- ... -->`,只写"为什么这么布局/绑定",不重复 props/emit 类型。
+
+## 2. 文件索引(当前已落地)
+
+### packages/shared/src/(三端共用)
+
+| 文件 | 职责 | 关键范式 |
+|---|---|---|
+| `types/api.ts` | 契约类型唯一定义点 | `R<T>` 信封 / `PageData<T>`(list,total,page,pageSize,pages)/ `PageR<T>` / `PageQuery` / `MenuNode`(code+label+path?+icon?+children?) |
+| `identity/token-store.ts` | token 物理存取(唯一 localStorage 读写点) | `KEYS` 常量 + `setTokens`/`getAccessToken`/`getRefreshToken`/`getExpiresAt`/`clearTokens`;`getExpiresAt` 用 `Number.isFinite` 收窄(fail-closed) |
+| `api/http.ts` | axios 实例 + 信封 + 401 refresh 单例 + toast 注入 | `UNAUTHORIZED_CODES` 常量;`refreshAccessToken` 单例(`let refreshing: Promise<boolean>\|undefined`);`setRouterForRedirect` 模块级注入(解循环依赖);`setBusinessErrorToaster` 模块级注入(零 UI 依赖);toast 去重 1s Map |
+| `api/auth/login.ts` | 登录/登出接口 | `LoginResult` 含 `tabTree: MenuNode[]`(移动端 = 底部导航树);`login()` 返回 `R<LoginResult>`;`logout(refreshToken)` 返回 `R<undefined>` |
+| `api/auth/wechat.ts` | 微信 per-campus 授权(**占位**) | `WechatAuthStatus` 状态机 5 值;`authorizeWechat(campusId)` → `R<WechatAuthorizeResult>`;`pollWechatStatus(state)` → `R<WechatStatusResult>`(success 时 `token` 结构同 `LoginResult`);TODO 标注待后端核对 |
+| `store/login.ts` | Pinia setup store(登录态视图) | `tabTree` 仅内存态(不落 localStorage);`isLoggedIn` computed(提前 30s 判过期);`applyLogin`/`doLogout`/`hydrate` 三动作;`hasPerm` 方法 |
+| `store/scope.ts` | Pinia setup store(作用域) | `current` ref(institutionId/campusId);`ready` computed(institutionId > 0);`set` 校验 > 0;`toQuery` 返回 `{institutionId, campusId?}`(campusId=0 省略) |
+| `composables/use-list-page.ts` | 列表页统一规范 composable | 泛型 `useListPage<T>(options)`;`rows = ref([]) as Ref<T[]>`(断言注释说明);`watch([page,pageSize])` 自动重载;`fetchList` 统一解 `res.data.{list,total}`;loading 防重入 |
+| `directives/perm.ts` | v-perm 按钮级权限指令 | `mounted` + `updated` 双钩子(登录态/权限集可能挂载后变化);权限缺失 `el.remove()`(非 disabled);fail-closed |
+| `components/list-page.vue` | 泛型 SFC 列表页(Vue 3.5 `generic="T extends object"`) | `van-pull-refresh` + `van-list`;三态(loading 骨架 / 空态 van-empty / 错误态 van-notice-bar);`finished` computed(rows.length >= total);`#default` slot `:row` 强类型 = T |
+| `components/tabbar-menu.vue` | 底部导航(递归渲染 tabTree) | `van-tabbar` + `van-tabbar-item`;`v-show="item.path"`(无 path 的纯分组不渲染为 tab);`:model-value="route.path"` 高亮;`onTabClick` 走 `router.push`(SPA) |
+| `index.ts` | 桶导出 | 只 `export *`,零运行时逻辑(避免副作用/循环依赖) |
+
+### apps/teacher/src/(老师端)
+
+| 文件 | 职责 | 关键范式 |
+|---|---|---|
+| `main.ts` | 8 步装配入口 | ① 取 UMD 全局 fail-fast → ② Pinia → ③ Router + 守卫 → ④ 注入 http(setRouterForRedirect + setBusinessErrorToaster)→ ⑤ Vant(npm import + app.use)→ ⑥ v-perm → ⑦ hydrate → ⑧ mount;`import 'vant/lib/index.css'` + `import '@/styles/tokens.css'` 在顶部 |
+| `App.vue` | 根组件 | 裸 `<router-view/>`(套壳与否由路由树决定) |
+| `router/index.ts` | 路由表 + 守卫链 | 嵌套路由(layout 父路由 path=/ + children);守卫 4 步(白名单→登录态→刷新→作用域);`declare module 'vue-router'` 扩展 RouteMeta(public/scopeRequired/title);`afterEach` 同步 document.title |
+| `styles/tokens.css` | 全局设计 token | `:root` CSS 自定义属性(`--app-bg-page`/`--app-text-primary`/`--app-spacing-md`/`--app-font-lg`/`--app-tabbar-height`/`--app-nav-bar-height`);plain CSS 非 SCSS |
+| `api/org/institution.ts` | 机构列表接口 | `InstitutionDTO` + `InstitutionFilter extends PageQuery`;`listInstitutions(params)` 返回 `PageR<InstitutionDTO>`;`http.get<T>` + `.then(r => r.data)` |
+| `api/academic/checkin.ts` | 打卡记录接口 | `CheckinRecordDTO` + `CheckinFilter extends PageQuery`(date 必填 + classId 可选);`listCheckinRecords(params)` 返回 `PageR<CheckinRecordDTO>` |
+| `views/login/index.vue` | 双 tab 登录页 | `van-tabs v-model:active`(string 兼容 Vant);账密 tab(van-form + van-field + rules `FieldRule[]`);微信 tab(authorizeWechat + setInterval 1.5s + onUnmounted 清);`showToast` 函数式 API;`activeTab = ref<string>('password')` |
+| `views/layout/index.vue` | 布局壳 | `van-nav-bar`(fixed + placeholder)+ 内容区 `<router-view/>` + `<tabbar-menu :tab-tree="login.tabTree"/>`;`handleLogout` 用 `showConfirmDialog` + `doLogout` + `router.push('/login')`;padding-bottom 给 tabbar 留位 |
+| `views/org/institution/list.vue` | 机构列表页 | **useListPage 解构到顶层**(`const { rows, total, loading, error, page, fetchList } = useListPage<T>({...})`);`van-search` + `<list-page>`(:rows/:total/:loading/:error/@load/@refresh);`#default slot { row }` 强类型 |
+| `views/academic/checkin/list.vue` | 教务打卡列表页 | 原生 date input(today() 取 yyyy-MM-dd);`STATUS_MAP` Record<number, {label, type}>(1-4→正常/迟到/缺勤/请假 + van-tag type);`onDateChange` 显式 `fetchList()`(不等 watch 防抖) |
+
+## 3. 范式摘录
+
+### 3.1 UMD 全局 + define 注入(业务代码不 import 框架)
+
+```ts
+// ✅ 正确:裸标识符(vite define 编译期替换为 Vue.ref / VueRouter.useRouter 等)
+const rows = ref([]);
+const router = useRouter();
+const store = useLogin(); // Pinia.defineStore 注入
+
+// ✅ 正确:import type(编译期擦除,零运行时足迹)
+import type { Ref } from 'vue';
+import type { PageR, PageQuery } from '@unified/shared/types/api';
+import type { FieldRule } from 'vant'; // Vant 类型走 npm(非 UMD 全局)
+
+// ❌ 禁止:值 import 框架(会打进 dist,ESLint 拦截)
+// import { ref, computed } from 'vue';
+// import { createRouter } from 'vue-router';
+// import { defineStore } from 'pinia';
+// import axios from 'axios';
+
+// ✅ Vant 例外:走 npm 编译(非 UMD 全局),main.ts 装配点可值 import
+import Vant from 'vant';
+import { showFailToast, showConfirmDialog, showToast } from 'vant';
+```
+
+### 3.2 useListPage 收口 + 解构到顶层(模板自动解包)
+
+```vue
+<script setup lang="ts">
+import { useListPage } from '@unified/shared/composables/use-list-page';
+import { useScope } from '@unified/shared/store/scope';
+import ListPage from '@unified/shared/components/list-page.vue';
+import { listInstitutions, type InstitutionDTO, type InstitutionFilter } from '@/api/org/institution';
+
+const scope = useScope();
+const filters = reactive<{ keyword: string }>({ keyword: '' });
+
+// ⚠️ 必须解构到顶层(Vue 只自动解包顶层 setup 绑定;lp.rows 不解包 → 模板 :rows="lp.rows" 报 Ref 类型错)
+const { rows, total, loading, error, page, fetchList } = useListPage<InstitutionDTO>({
+  fetcher: (page, pageSize) => {
+    const q: InstitutionFilter = { ...scope.toQuery(), keyword: filters.keyword || undefined, page, pageSize };
+    return listInstitutions(q);
+  },
+});
+
+function onLoad(): void { page.value += 1; }  // 触底加载下一页
+function onRefresh(): void { page.value = 1; } // 下拉刷新重置
+function onSearch(): void { page.value = 1; }  // 搜索重置
+
+onMounted(() => { fetchList(); });
+</script>
+
+<template>
+  <van-search v-model="filters.keyword" @search="onSearch" @clear="onSearch" />
+  <list-page :rows="rows" :total="total" :loading="loading" :error="error" @load="onLoad" @refresh="onRefresh">
+    <template #default="{ row }">
+      <van-cell :title="row.name" :label="`编码 ${row.code}`" />
+      <!-- row 强类型 = InstitutionDTO(泛型 SFC 自动推断) -->
+    </template>
+  </list-page>
+</template>
+```
+
+### 3.3 Vant 表单 rules(FieldRule 类型 + 正确 trigger 值)
+
+```vue
+<script setup lang="ts">
+import { showToast } from 'vant';
+import type { FieldRule } from 'vant';
+
+const form = reactive({ username: '', password: '' });
+
+// ✅ 正确:显式 FieldRule[] 类型 + trigger 为 Vant FieldValidateTrigger('onBlur'|'onChange'|'onSubmit')
+const rules: Record<'username' | 'password', FieldRule[]> = {
+  username: [{ required: true, message: '请输入用户名', trigger: 'onBlur' }],
+  password: [{ required: true, message: '请输入密码', trigger: 'onBlur' }],
+};
+
+async function handleSubmit(): Promise<void> { ... }
+</script>
+
+<template>
+  <van-form @submit="handleSubmit">
+    <van-field v-model="form.username" name="username" label="用户名" :rules="rules.username" />
+    <van-field v-model="form.password" name="password" type="password" label="密码" :rules="rules.password" />
+    <van-button native-type="submit" round block :loading="submitting">登录</van-button>
+  </van-form>
+</template>
+```
+
+### 3.4 http 层零 UI 依赖(toast 注入 + 路由注入)
+
+```ts
+// shared/api/http.ts(底层基础设施,零 UI 依赖)
+let toastHandler: ((message: string) => void) | undefined;
+export function setBusinessErrorToaster(fn: (message: string) => void): void {
+  toastHandler = fn;
+}
+function toastBusinessError(message: string): void {
+  if (toastHandler) toastHandler(message);
+  else console.warn('[biz-error]', message); // 未注入时降级
+}
+
+// apps/teacher/src/main.ts(装配 UI 后注入,方向 UI→基础)
+import { showFailToast } from 'vant';
+import { setBusinessErrorToaster, setRouterForRedirect } from '@unified/shared/api/http';
+setBusinessErrorToaster((m) => showFailToast(m));
+setRouterForRedirect(router); // 解 router→http→router 循环依赖
+```
+
+### 3.5 monorepo 每端独立 tsconfig(@ 路径各端独立)
+
+```jsonc
+// apps/teacher/tsconfig.json(本端独立,extends 根 tsconfig.base.json)
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@": ["./src"],                    // 本端业务层(各端独立)
+      "@/*": ["./src/*"],
+      "@unified/shared": ["../../packages/shared/src"],  // 共享层(源码消费)
+      "@unified/shared/*": ["../../packages/shared/src/*"]
+    }
+  },
+  "include": [
+    "../../global.d.ts",                 // 根 UMD 全局声明
+    "./src/**/*.ts",
+    "./src/**/*.vue",
+    "../../packages/shared/src/**/*.ts", // 共享层源码(类型直达)
+    "../../packages/shared/src/**/*.vue",
+    "../../shims/**/*.ts"                // 胶水层
+  ]
+}
+```
+
+### 3.6 微信授权轮询(占位,onUnmounted 必清定时器)
+
+```vue
+<script setup lang="ts">
+import { authorizeWechat, pollWechatStatus } from '@unified/shared/api/auth/wechat';
+import { useLogin } from '@unified/shared/store/login';
+import { showToast } from 'vant';
+
+const loginStore = useLogin();
+const authUrl = ref('');
+const polling = ref(false);
+let pollTimer: ReturnType<typeof setInterval> | undefined;
+
+function stopPolling(): void {
+  if (pollTimer !== undefined) { clearInterval(pollTimer); pollTimer = undefined; }
+  polling.value = false;
+}
+
+async function handleTabChange(tab: string | number): Promise<void> {
+  stopPolling();
+  if (tab === 'wechat') {
+    try {
+      const auth = await authorizeWechat(campusId);
+      if (auth.code === 200 && auth.data) {
+        authUrl.value = auth.data.url;
+        polling.value = true;
+        pollTimer = setInterval(async () => {
+          try {
+            const s = await pollWechatStatus(auth.data!.state);
+            if (s.code !== 200 || !s.data) return; // 单次静默
+            if (s.data.status === 'success' && s.data.token) {
+              stopPolling();
+              loginStore.applyLogin(s.data.token);
+              await onLoginSuccess(s.data.token);
+            } else if (s.data.status === 'cancel' || s.data.status === 'expire') {
+              stopPolling();
+              authUrl.value = '';
+            }
+          } catch { /* 网络故障静默,下轮重试 */ }
+        }, 1500);
+      }
+    } catch {
+      stopPolling();
+      showToast('微信授权暂未开放'); // 后端未就绪
+    }
+  }
+}
+
+onUnmounted(() => { stopPolling(); }); // 组件卸载必清(防内存泄漏)
+</script>
+```
+
+## 4. 新增文件清单(照此范式写)
+
+| 场景 | 照哪个范式 | 备注 |
+|---|---|---|
+| 新域列表页 | §3.2(useListPage + list-page) | 补 `api/{域}/{资源}.ts` + `views/{域}/{资源}/list.vue` |
+| 新域详情页 | 手动路由 + `api` 接口 | `views/{域}/{资源}/detail.vue` |
+| 新域接口 | `api/org/institution.ts` 范式 | 一资源一文件;DTO + Filter + 请求函数 |
+| 新共享组件 | `components/list-page.vue` 范式 | 泛型 SFC 或普通 SFC;零业务耦合 |
+| 新共享 composable | `composables/use-list-page.ts` 范式 | 泛型 + 返回 Ref 集合 |
+| 新指令 | `directives/perm.ts` 范式 | mounted + updated 双钩子 |
+| student/parent 端 | teacher 端全量复制 | 改 package.json name + vite port + router 路由表 + views 页面 |

+ 198 - 53
README.md

@@ -1,78 +1,185 @@
 # 移动端前端(老师 / 学生 / 家长)
 
-> 单仓多端,**按角色分包**:`teacher` / `student` / `parent` 三端共享 `packages/shared`(身份/登录态、通用组件、接口封装)。
+> pnpm monorepo,**按角色分包**:`teacher` / `student` / `parent` 三端共享 `packages/shared`(身份/登录态、通用组件、接口封装)。
 > 本文档覆盖四块:**技术栈 / 目录结构与用途 / 编码规范 / 构建**。接口契约见工作区文档库 `unified/`、`academic/05-architecture/`、`teaching/05-architecture/`(本仓库不复制契约原文)。
+> 框架层选型(2026-11-06 定稿):**UMD 混合方案**(Vue 系 UMD 自托管 + Vant npm 编译,因 Vant 4 官方无 UMD 构建)。
 
 ## 定位
 
 一个移动端仓服务三个角色端:
 
-| 端 | 主要场景 | 主要域 |
-|---|---|---|
-| 老师端 | 打卡、查看/批改练习、备课查看 | 教务(打卡)+ 教学(练习/备课) |
-| 学生端 | 做题、查看练习记录与分析 | 教学 |
-| 家长端 | 绑定孩子、查看孩子练习/费用 | 教务(bindchild 绑定、费用)+ 教学(练习) |
+| 端 | 主要场景 | 主要域 | 当前状态 |
+|---|---|---|---|
+| 老师端 | 打卡、查看/批改练习、备课查看 | 教务(打卡)+ 教学(练习/备课) | ✅ 落地(4 门禁全绿) |
+| 学生端 | 做题、查看练习记录与分析 | 教学 | ⏳ 待按 teacher 模板建 |
+| 家长端 | 绑定孩子、查看孩子练习/费用 | 教务(bindchild 绑定、费用)+ 教学(练习) | ⏳ 待按 teacher 模板建 |
 
 三端**底层用户统一**(canonical_user_id + 7 角色),故共用 `shared` 的 SSO 接入与登录态,仅按角色分包差异视图。
 
 ## 1. 技术栈
 
-| 关注点 | 选型 | 说明 / 依据 |
+### 1.1 资源引入 = 混合方案(核心选型,2026-11-06 定稿)
+
+| 层 | 内容 | 引入方式 | 版本管理 |
+|---|---|---|---|
+| **框架层(UMD 自托管)** | Vue 3 / vue-demi / Vue Router 4 / Pinia / axios,共 5 项 UMD | `index.html` 按固定顺序引入,源文件**自托管到自有 OSS**(自定义域名 + SSL);dev 经 Vite `publicDir` 本地命中 vendor/ | 版本事实 = `index.html` 头部"vendor 版本表"(钉死次版本)+ 仓库 `vendor/` 源文件(git 入库) |
+| **UI 层(npm 编译)** | **Vant 4**(官方无 UMD 构建,仅 es/lib) | `import Vant from 'vant'` 编译进 bundle(`app.use(Vant)` 全局注册 + `import 'vant/lib/index.css'`) | pnpm-lock.yaml 锁版本(`vant@4.10.2`) |
+| **业务层** | 本项目全部业务代码(TS 编写) | 本地 `pnpm` 工具链编译(Vite/vue-tsc/ESLint/Prettier 为 dev 依赖),产物 `dist/` 经 nginx 部署 | pnpm-lock.yaml 入库 |
+
+- **为何混合(非全 UMD)**:Vant 4 官方仅发布 `es/`(ESM)+ `lib/`(CJS)构建,**无 UMD/IIFE 产物**(设计上只给打包器消费)。故"UMD 自托管"最多做到 Vue 系 5 项;Vant 走 npm 编译进 bundle 属**设计预期**,dist 含 Vant 运行时(与 PC 端"dist 不含 ElementPlus"的差异点,由 Vant 无 UMD 决定)。
+- **开发测试**:`pnpm dev:teacher` 起 Vite 本地服务(publicDir=vendor,框架 UMD 本地命中;HMR 只热更业务代码);`/api` 代理到后端 dev 地址。
+- **构建**:`pnpm build` 打包业务 + Vant → `dist/`;**产物禁止包含 Vue 框架运行时**(质量门禁 grep 抽查,防 alias 配错导致 vue 包被打进 bundle)。
+
+### 1.2 框架层 UMD 资源清单(加载顺序固定,版本以 `index.html` 版本表为准)
+
+| # | 资源 | OSS 路径模板 | 全局变量 | 用途 |
+|---|---|---|---|---|
+| 1 | Vue 3(prod UMD) | `vue/{v}/vue.global.prod.js` | `Vue` | 框架本体 |
+| 2 | vue-demi(IIFE) | `vue-demi/{v}/vue-demi.iife.js` | `VueDemi` | **pinia 桥接层**:pinia IIFE 构建硬依赖 `VueDemi` 全局;漏加载 → pinia 报 `VueDemi is not defined`。与 pinia 同升同降 |
+| 3 | Vue Router 4 | `vue-router/{v}/vue-router.global.prod.js` | `VueRouter` | 路由(按端组织 + 守卫链) |
+| 4 | Pinia | `pinia/{v}/pinia.iife.prod.js` | `Pinia` | 状态(login / scope 两 store) |
+| 5 | axios | `axios/{v}/axios.min.js` | `axios` | HTTP(统一封装于 `shared/api/http`) |
+
+> OSS 根路径固定:`https://<自有OSS域名>/static/vendor/<库名>/<版本号>/<文件名>/`,**只增不删**。
+> 与 PC 端差异:移动端 UMD 仅 5 项(无 Element Plus 系列,UI 库走 Vant npm);vue-demi 同理保留(pinia 桥接层)。
+
+### 1.3 业务层工具链(本地编译)
+
+| 关注点 | 选型 | 理由 / 依据 |
 |---|---|---|
-| 框架 | **Vue 3**(`<script setup>` + Composition API) | 与 PC 端同栈,组件/工具/类型可复用 |
-| 语言 | **TypeScript**(strict) | 契约类型化;`R<T>` 信封泛型与 PC 端一致 |
-| 构建 | **Vite**(每端独立入口) | 按端分包产物;主 chunk < 1024KB |
-| UI | **Vant 4** | 移动交互组件库(默认中文 locale);如混用 Element Plus 组件需双 locale 注入 |
-| 状态 | **Pinia** | 登录态/角色/institution·campus 作用域(shared 内建) |
-| 路由 | **Vue Router 4**(每端独立路由表) | 角色差异用权限码判断 |
-| HTTP | **axios**(统一封装于 `shared/api`) | 统一 `{code, data, message}` 信封处理 |
-| 鉴权 | SSO(最小可行)token + **微信 per-campus 授权**(家长端主入口) | 统一 identity 签发(ADR-014);多 appid 按 campus 绑定 |
-| 工作区 | **pnpm monorepo**(workspaces) | `packages/shared` + `apps/{student,parent,teacher}` |
-| 代码风格 | ESLint + Prettier | 提交前 lint |
+| 构建 | **Vite 6.x** | 业务代码 HMR + 产物构建;框架 UMD 不打包 |
+| 语言 | **TypeScript strict** | 契约类型化:`R<T>` / `PageData<T>` / 各域 DTO;UMD 全局经仓库根 `global.d.ts` 声明类型 |
+| UI 组件库 | **Vant 4**(npm 编译,默认中文) | 移动交互组件库;Vant 4 默认简体中文无需传 locale;`app.use(Vant)` 全局注册 + `import 'vant/lib/index.css'` |
+| 状态 | **Pinia**(UMD 自托管) | `login`(token/角色/权限码/tabTree)+ `scope`(institution/campus 作用域) |
+| 路由 | **Vue Router 4**(UMD 自托管) | 按端组织 + 守卫链(登录态 → 静默刷新 → 白名单 → 作用域就绪) |
+| HTTP | **axios 1.x**(UMD 自托管,封装于 `shared/api/http`) | 统一信封处理 + 401 refresh 单例重放;业务错误**不** reject |
+| 鉴权 | SSO token(access 30min + refresh 30 天,ADR-014)+ **微信 per-campus 授权**(家长端主入口,占位) | 统一 identity 签发;菜单/按钮按**权限码**显隐,禁 `role == N` 硬编码 |
+| 包管理 | **pnpm 10.12.1**(lockfile 入库) | monorepo 工作区管理(packages/shared + apps/{端}) |
+| 代码风格 | ESLint 9 + Prettier 3 | 提交前 lint;`no-explicit-any` 强制 |
+| 样式 | **plain CSS token**(`tokens.css`,`:root` 自定义属性 `--app-*`) | main.ts 早引入;组件 `var(--app-xxx)` 引 token,禁硬编码色值/尺寸。规避 PC 端 SCSS/sass 依赖 |
+| 类型检查 | vue-tsc `--noEmit`(双工程:根 shared + 各端独立) | 质量门禁 0 错误 |
+
+### 1.4 框架升级规则(自托管五步,缺一不可)
+
+1. 下载新版本 UMD 文件 → 2. 覆盖仓库 `vendor/` 对应目录并 commit → 3. 上传 OSS 对应路径 → 4. 改 `index.html` 版本表 → 5. 全量回归(vue-tsc + build + 端点联调)。
+> 版本升级不再受 lockfile 保护(UMD 部分),靠仓库 diff + 回归兜底;发布前 `vendor/` 与 OSS 内容 md5 对账。
+> Vant 升级走 pnpm-lock.yaml 锁版本 + `pnpm up vant`(不走 OSS 自托管)。
+> 升级后重跑 `node scripts/gen-shims.cjs`(重新生成 shims/ 转发清单)。
+
+### 1.5 布局与导航模型(移动端 = 底部 Tabbar + 权限码下发)
+
+- **布局壳**(`views/layout/index.vue`):`van-nav-bar`(顶部)+ 内容区 `<router-view/>`(中)+ `<tabbar-menu :tab-tree="login.tabTree"/>`(底部 van-tabbar)。作为**父路由 component**(嵌套路由),App.vue 保持裸 `<router-view/>` 不改——套壳与否由路由树决定(登录页是顶层路由不套壳;业务域是 layout 的子路由套壳)。
+- **Tabbar 菜单**(`shared/components/tabbar-menu.vue`):递归渲染 `tabTree`(`MenuNode[]` 树),取**顶层节点**渲染为 van-tabbar-item(有 `path` 的渲染为可跳转 tab;有 `children` 的纯分组节点不渲染为 tab——tab 必须可跳转,是 UI 形态约束非权限过滤)。高亮靠 `:model-value="route.path"`(van-tabbar 内部 watch route 自动同步);点击经 `router.push`(SPA 免刷新)。
+- **tabTree 权限码下发**:后端按当前用户角色/权限**过滤后**下发 `tabTree`(`MenuNode[]` 树,契约在 `shared/types/api.ts`),前端原样渲染、**不在前端二次过滤**(避免维护"权限码 → 导航"映射的双份漂移)。`MenuNode.code` 保留权限码(`域.资源.动作`),供按钮级 `v-perm` 与导航项联动复用。
+- **tabTree 不落 localStorage**:`tabTree` 是响应式视图非凭证(与 roles/permissions 一致),刷新后降级空数组、tab 全隐藏,完整恢复依赖后续 `/api/auth/me` 端点(规划项)。
+- **与 PC 端差异**:PC 用 el-aside 侧边菜单(左)+ el-header(上);移动端用 van-nav-bar(上)+ van-tabbar(下),内容区在中。底部 tabbar fixed 需给内容区 padding-bottom 留位。
+
+### 1.6 登录模型(账密真 + 微信 per-campus 授权占位)
+
+- **账号密码**(主通道,已就绪):`POST /api/auth/login` → 凭证落盘(`applyLogin` 写 token-store + 响应式视图)→ `scope.set` 初始化作用域(移动端**无独立作用域选择页**:登录响应直接下发 institutionId/campusId)→ `router.push('/')` 进 layout 首页。
+- **微信 per-campus 授权**(副通道,**占位**):后端端点未就绪,`shared/api/auth/wechat.ts` 为占位接口(路径/字段按通用 per-campus 授权流程假设,联调前与后端核对):
+  - `POST /api/auth/wechat/authorize` 发起授权(按 campusId 取对应 appid 的微信授权 URL + 本地 state);
+  - `GET /api/auth/wechat/status?state=xxx` 轮询状态(`pending/scanning/success/cancel/expire`);
+  - `success` 时返回的 `token` 结构同 `LoginResult`,直接复用 `applyLogin`(不新增冗余登录方法)。
+- **登录页 UI**(`views/login/index.vue`):`van-tabs` 两 tab(账号密码 / 微信授权);切到微信 tab 时发起授权 + `setInterval` 1.5s 轮询,**组件 `onUnmounted` 必清定时器防泄漏**;单次轮询业务错误/网络故障静默跳过(下轮重试),终态(success/cancel/expire)停止;后端未就绪时授权区显"暂未开放"占位。
+- **路由跳转收口**(避免循环依赖):`http.ts` 拦截器无法直接 `import router`(router 守卫用 useLogin→login store,而 http 被 router 守卫依赖,会成 router→http→router 循环),改用**模块级注入** `setRouterForRedirect(router)`(main.ts 装配后注入);未注入时降级 `location.href`。所有登录态跳转(401 跳登录、登出跳登录、登录成功进首页)统一走 `router.push`(SPA,免全页刷新)。
+- **http 层零 UI 依赖(toast 注入)**:`setBusinessErrorToaster(fn)` 由 main.ts 装配 Vant 后注入 `showFailToast`(方向 UI→基础,非基础→UI)。http 层本身不 import Vant。
 
 ## 2. 目录结构与用途
 
 ```
 .
-├── README.md              ← 本文件
-├── package.json           ← 根(workspaces 声明 + 公共脚本/依赖)
-├── pnpm-workspace.yaml    ← pnpm 工作区(packages/* + apps/*)
-├── tsconfig.base.json     ← TS 公共配置(apps/* 继承 + paths 指 shared 源码)
-├── .env.example           ← 环境变量模板(各端 VITE_API_BASE / VITE_WECHAT_* 等)
+├── README.md                  ← 本文件
+├── package.json               ← 根(workspaces 声明 + 公共脚本/依赖:pnpm typecheck/lint/build)
+├── pnpm-workspace.yaml        ← pnpm 工作区(packages/* + apps/*)
+├── tsconfig.base.json         ← TS 公共配置(strict 基线;apps/* 和根 tsconfig 均 extends 此文件)
+├── tsconfig.json              ← 根 tsconfig(shared + shims 项目;paths: @unified/shared → shared/src)
+├── global.d.ts                ← UMD 全局 + vite define 裸标识符的 TS 声明(5 项:Vue/VueDemi/VueRouter/Pinia/axios)
+├── .env.example               ← 环境变量模板(VITE_API_BASE / VITE_WECHAT_APPID)
+├── vendor/                    ← 【框架层 UMD 自托管源文件】git 入库(5 项,与 PC 端同源 md5 一致)
+│   ├── vue@3.5.45/            vue.global.prod.js
+│   ├── vue-demi@0.14.10/      vue-demi.iife.js(pinia 桥接层)
+│   ├── vue-router@4.6.4/      vue-router.global.prod.js
+│   ├── pinia@2.3.1/           pinia.iife.prod.js(依赖 VueDemi 全局)
+│   └── axios@1.21.0/          axios.min.js
+│   (上传 OSS 经 /static/vendor/<库名>/<版本号>/ 暴露;只增不删)
+├── shims/                     ← 【胶水层】'vue'/'vue-router'/'pinia' 模块名 → UMD 全局的唯一转发层
+│   ├── vue.ts                 ←   由 scripts/gen-shims.cjs 生成(vue 161 项导出)
+│   ├── vue-router.ts          ←   同机制(23 项)
+│   └── pinia.ts               ←   同机制(17 项)
+├── scripts/
+│   └── gen-shims.cjs          ← 构建辅助:框架升级五步后重跑(重新生成 shims/ 转发清单)
+├── eslint.config.js           ← ESLint 9 flat config(monorepo 适配;FRAMEWORK_MODULE_SELECTOR 只含 UMD 框架)
 ├── packages/
-│   └── shared/            ← 三端共享(唯一登录态/SSO 实现,见铁律 1)
-│       ├── src/
-│       │   ├── identity/  ←   SSO 接入:token 存取、静默刷新/吊销、微信 per-campus 授权
-│       │   ├── api/       ←   接口封装:统一信封 + 错误码;按域分子目录(academic/teaching/…)
-│       │   ├── components/←   通用组件(作用域感知、列表、表单、Badge 色板)
-│       │   ├── types/     ←   契约类型:R<T>、角色/权限码、各域 DTO
-│       │   └── composables/←  可组合逻辑:useListPage 等
-│       └── package.json   ←   包名 @unified/shared;main 指向 src(按源码消费,免构建)
+│   └── shared/                ← 【三端共享层】源码消费(main → src/index.ts,免构建)
+│       ├── package.json       ←   包名 @unified/shared;devDeps(vue/vue-router/pinia/axios/vant/dayjs)
+│       └── src/
+│           ├── index.ts       ←   桶导出(re-export 全模块)
+│           ├── types/api.ts   ←   契约类型:R<T> / PageData<T> / PageR<T> / PageQuery / MenuNode
+│           ├── identity/
+│           │   └── token-store.ts ← token 物理存取(localStorage 收口)
+│           ├── api/
+│           │   ├── http.ts    ←   axios 实例 + 统一信封 + 401 refresh 单例 + setRouterForRedirect + setBusinessErrorToaster
+│           │   └── auth/
+│           │       ├── login.ts   ← 登录/登出(LoginResult 含 tabTree 字段)
+│           │       └── wechat.ts  ← 微信 per-campus 授权(占位,后端未就绪)
+│           ├── store/
+│           │   ├── login.ts   ←   Pinia setup store(token/角色/权限码/tabTree + applyLogin/doLogout/hydrate)
+│           │   └── scope.ts   ←   Pinia setup store(institution/campus 作用域 + toQuery)
+│           ├── composables/
+│           │   └── use-list-page.ts ← 列表页统一规范(分页五元组 + 统一分页解构 + watch 自动重载)
+│           ├── directives/
+│           │   └── perm.ts    ←   v-perm 按钮级权限指令(权限码三段式,缺失时 remove 元素)
+│           └── components/
+│               ├── list-page.vue    ← 泛型 SFC(generic="T extends object"):van-pull-refresh + van-list + 三态
+│               └── tabbar-menu.vue  ← 底部导航(递归渲染 tabTree,van-tabbar + router.push)
 └── apps/
-    ├── student/           ← 学生端:src/{views,router,api 域扩展}/ + main.ts(独立 Vite 入口)
-    ├── parent/            ← 家长端:同上(微信授权主入口 + bindchild 绑定流)
-    └── teacher/           ← 老师端:同上(规划待建,结构对齐 student/parent)
+    ├── teacher/               ← 【老师端】完整可跑(4 门禁全绿)
+    │   ├── package.json       ←   包名 unified-teacher;devDeps(vite/vant/@vitejs/plugin-vue)
+    │   ├── vite.config.ts     ←   define(VUE_APIS/VUE_ROUTER_APIS/PINIA_APIS)+ publicDir(serve=vendor)
+    │   │                         + alias(vue/vue-router/pinia→shims,@unified/shared→shared/src,@→./src)
+    │   │                         + server port 5174 + /api 代理 + chunkSizeWarningLimit 1024
+    │   ├── tsconfig.json      ←   本端独立 tsconfig(extends 根 base;paths: @→./src + @unified/shared→shared/src)
+    │   │                         + include 本端 src + 根 global.d.ts + shared/src + shims/
+    │   ├── index.html         ←   5 项 UMD 按序引入 + vendor 版本表 + <div id="app"> + /src/main.ts
+    │   └── src/
+    │       ├── main.ts        ←   8 步装配:取 UMD 全局 → Pinia → Router → 注入 http → Vant → v-perm → hydrate → mount
+    │       ├── App.vue        ←   裸 <router-view/>
+    │       ├── router/index.ts ←   路由表(layout 父路由 + 业务域子路由)+ 守卫链 4 步
+    │       ├── styles/tokens.css ← 全局设计 token(:root CSS 自定义属性 --app-*)
+    │       ├── api/           ←   本端域接口(org/institution.ts + academic/checkin.ts)
+    │       └── views/
+    │           ├── login/index.vue      ← 双 tab 登录(van-tabs 账密 + 微信占位)
+    │           ├── layout/index.vue     ← 布局壳(van-nav-bar + router-view + tabbar-menu)
+    │           ├── org/institution/list.vue ← 机构列表(useListPage + list-page + van-search)
+    │           └── academic/checkin/list.vue ← 教务打卡(useListPage + list-page + 日期筛选 + 状态 van-tag)
+    ├── student/               ← 【学生端】待建(按 teacher 模板复制)
+    └── parent/                ← 【家长端】待建(按 teacher 模板复制 + 微信授权主入口 + bindchild)
 ```
 
 **目录用途约定**:
 
 | 目录 | 用途 | 约束 |
 |---|---|---|
-| `packages/shared` | 三端共享层:SSO/登录态、接口封装、通用组件、契约类型 | **唯一** token 存取与 SSO 实现点;零端差异逻辑 |
-| `apps/{端}/src/views` | 该端页面 | 只经 `shared/api` 发起请求;不实现登录 |
+| `packages/shared` | 三端共享层:SSO/登录态、接口封装、通用组件、契约类型、composable、指令 | **唯一** token 存取与 SSO 实现点;零端差异逻辑;改 shared 必须三端 build 通过 |
+| `apps/{端}/src/views` | 该端页面 | 只经 `shared/api` 发起请求;不实现登录(登录页除外) |
 | `apps/{端}/src/router` | 该端独立路由 + 权限码守卫 | 角色差异用权限码,不写 `role == N` |
-| `apps/{端}/.env*` | 该端构建变量(API base、微信 appid 映射) | 只 `.env.example` 入库 |
-| `tsconfig.base.json` | 公共 TS 配置 | apps 经 `paths` 直指 `@unified/shared` 源码(dev 免构建、类型直达) |
+| `apps/{端}/src/api` | 该端域接口扩展(DTO + 请求函数) | 一资源一文件;DTO 与后端逐字段对齐(camelCase) |
+| `apps/{端}/tsconfig.json` | 本端独立 TS 配置 | `@`→本端 src(monorepo 下各端独立);拉入 shared/src + shims + 根 global.d.ts |
+| `vendor/` | 框架层 UMD 源文件版本事实 | 与 OSS 内容一致;只增不删 |
+| `shims/` | 框架模块名 → UMD 全局转发胶水层 | 全仓**唯一**允许出现 `vue`/`vue-router`/`pinia` 模块名的目录;由 gen-shims.cjs 生成,**禁手改** |
+| `global.d.ts` | UMD 全局 + define 裸标识符 TS 声明 | 放**仓库根**(覆盖 shims/ + shared/src + apps/*/src);不声明 Vant 全局(Vant 走 npm 自带类型) |
 
 ## 3. 编码规范
 
 ### 3.1 结构铁律
 
 1. **登录态/SSO 只有一份**,放 `packages/shared/src/identity`;三端不得各自实现登录(对齐"底层用户统一")。
-2. **家长↔学生绑定以教务侧 `bindchild` 为事实源**(对齐清单 §6):家长端进入时按绑定关系拉取孩子数据,不本地存绑定快照。
+2. **家长↔学生绑定以教务侧 `bindchild` 为事实源**:家长端进入时按绑定关系拉取孩子数据,不本地存绑定快照。
 3. **三端路由独立**(各自 `views/` + router),但共用 `shared/components` 与 `shared/api`;角色差异用权限码判断,不写 `role == N` 硬编码。
 4. 接口调用只经 `shared/api/`,三端 `views` 内不散写请求。
 5. **shared 改动影响三端**:改 shared 必须三端各自 `pnpm build` 通过后方可提交(跨端回归门禁)。
+6. **Vant 命令式 API 用函数式**:`import { showToast/showFailToast/showConfirmDialog } from 'vant'`(不可 `import { Toast }`——后者是组件对象 WithInstall/DefineComponent,不可调用)。
 
 ### 3.2 接口与响应契约
 
@@ -88,39 +195,77 @@
 - token 刷新单例:并发过期只触发一次 refresh(Promise 复用),成功后重放原请求。
 - 授权失败/未绑定时引导到对应端登录页,不白屏。
 
-### 3.4 可复用资产与惯例(源自教务 Go 重构实战)
+### 3.4 可复用资产与惯例
 
-- **三端复用模式**:与教务重构一致采用"移动教师/移动家长 Vant + 共享 `shared`";`shared/identity`(SSO 接入、token 存取/刷新/吊销)三端共用一份。
+- **三端复用模式**:`shared/identity`(SSO 接入、token 存取/刷新/吊销)+ `shared/components`(list-page + tabbar-menu)+ `shared/composables`(useListPage)三端共用一份。
 - **locale**:Vant 4 默认中文,不动;如混用 Element Plus 组件需命令式 + 声明式双注入。
-- **列表页规范**:`useListPage<T>` + 列表组件(分页/下拉刷新收口);Badge 色板纯数据零 Vue 依赖(年级学段色系/科目首字简称)。
+- **列表页规范**:`useListPage<T>` + `<list-page>` 泛型 SFC(van-pull-refresh + van-list + 三态);**useListPage 结果须解构到顶层**(`const { rows, total, loading, error, page, fetchList } = useListPage<T>({...})`)——Vue 只自动解包顶层 setup 绑定,嵌套属性(`lp.rows`)不解包。
 - **脱敏展示**:手机号/身份证按后端已脱敏值展示,前端不再二次处理(源头脱敏)。
 
-### 3.5 质量门禁
+### 3.5 命名规则
 
-| 门禁 | 命令 | 基线 |
+| 对象 | 规则 | 示例 |
 |---|---|---|
-| 类型检查 | `vue-tsc --noEmit`(各端 + shared) | 0 错误 |
-| 构建 | `pnpm -r build`(三端) | 各端主 chunk < 1024KB |
-| Lint | `pnpm -r lint` | 0 错误 |
+| 文件 | 组件 `PascalCase.vue`;其余 `kebab-case.ts` | `ListPage.vue`、`use-list-page.ts`、`list-page.vue` |
+| 组件 | 注册名 PascalCase,模板里 kebab-case | `<list-page>` / `<tabbar-menu>` |
+| 组合式函数 | `useXxx` | `useListPage<T>()`、`useScope()` |
+| 变量/函数 | `camelCase` | `institutionId`、`handleSubmit` |
+| 接口/类型 | 名词 PascalCase,DTO 带域前缀 | `InstitutionDTO`、`CheckinRecordDTO` |
+| **API 字段** | **一律 `camelCase`**,与后端 DTO 严格对齐;禁止前端自造映射层 | — |
+| 权限码 | **`域.资源.动作` 三段式**(点分) | `academic.checkin.view` |
+| 事件/方法 | 内部回调 `onXxx`;用户触发的提交类 `handleXxx` | `onLoad`、`handleSubmit` |
+| 禁词 | 禁用 `userid/schoolid/campusid` 类旧系统混用命名 | — |
+
+### 3.6 组件编写
+
+1. **框架运行时全走 UMD 全局(define 注入)**:`apps/*/src` 和 `packages/shared/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`**(编译期擦除、零运行时足迹)。SFC 模板编译器产物内部的模块名 import 经 `vite.config.ts` 的 `alias` 重定向到 `shims/*.ts`(运行时从 `globalThis` 取值转发,框架本体不进 dist)。**Vant 不在 define/alias/eslint 禁值清单内**(走 npm 编译,非 UMD 全局)。
+2. **Props**:显式 `interface` 类型;可选项给默认值;禁内联 `any`。
+3. **状态最小化**:能用 store 的(token/scope/权限码)不本地存;列表状态收口 `useListPage<T>`。
+4. **作用域纪律**:业务查询参数里的 `institutionId/campusId` **只从 `useScope()` 取**,禁止页面自拼。
+5. **表单**:`van-field` + 显式 `rules`(`FieldRule[]` 类型,Vant 的 trigger 是 `'onBlur'|'onChange'|'onSubmit'`);前端规则只拦格式,业务错误由后端 code 裁决。
+6. **权限**:按钮级 `v-perm="'域.资源.动作'"` 自定义指令(读 login store 权限码集);**禁止 `role === N` 硬编码**。
+7. **三态**:所有异步区域必须有 loading + 空态 + 错误态;错误态展示后端 message(已脱敏),不二次包装文案。
+8. **敏感操作**(删除/改密/解绑):`showConfirmDialog` 二次确认 + 操作后刷新。
+9. **脱敏**:手机号/身份证直接展示后端已脱敏值,前端不存明文、不二次处理。
+
+### 3.7 类型纪律(TS strict)
+
+- `any` 全面禁用(vue-tsc + ESLint `no-explicit-any` 双卡);未知结构用 `unknown` + 类型收窄。
+- DTO 与后端字段**逐字段**对齐:新增接口先补 `api/{域}/{资源}.ts` 类型定义再写调用;禁止"先 any 后补类型"。
+- `R<T>`/`PageData<T>` 泛型只在 `shared/types/api.ts` 定义一次,全仓引用。
+- **Vant 类型**:`import type { FieldRule } from 'vant'`(vant 根 `export * from './field'` 已 re-export FieldRule);命令式 API 用函数(`import { showToast } from 'vant'`)。
+
+### 3.8 质量门禁
+
+| 门禁 | 命令 / 做法 | 基线 |
+|---|---|---|
+| 类型检查 | `pnpm typecheck`(vue-tsc --noEmit 双工程:根 tsconfig = shared+shims;apps/teacher/tsconfig.json = 本端) | 0 错误 |
+| 构建 | `pnpm build`(vite,按端) | 0 错误;主 chunk < 1024KB;**dist/ 产物 grep 不得含 Vue 框架运行时本体**(判据:`createBaseVNode`/`queueJob`/`flushJobs`/`createAppContext`/`mountComponent`/`normalizeVNode`/`__VUE_PROD_DEVTOOLS__` 全 0——Vue runtime-core/reactivity 内部标识,prod 压缩仍保留、非公开 API、shims 不引用。⚠️ 不可用公开 API 名如 `openBlock`/`createVNode`/`Teleport`/`Suspense` 当判据:shims 转发层 `E.createVNode` 压缩后必然保留属性名,属设计预期。辅助佐证:dist 主 chunk 231KB = Vant + 业务,非 Vant+全量 Vue) |
+| Lint | `pnpm lint`(eslint .) | 0 错误 |
 | 跨端回归 | 改 shared 后三端全量 build | 全绿方可提交 |
+| vendor 一致性 | 发布前 `vendor/` 与 OSS 路径内容 md5 对账 | 一致方可发布 |
 
-### 3.6 Git 与提交规范
+### 3.9 Git 与提交规范
 
 - **工程纪律**:本地 commit 可执行;**脚手架搭建与 `git push` 前必须经用户确认**(逐次确认)。
 - 提交信息:conventional commits(`feat:` / `fix:` / `chore:` / `docs:` / `refactor:`);影响 shared 的提交在正文标注 `affects: student,parent,teacher`。
-- 提交前自检:三端 build 通过;敏感值未入库(`.env` 走 .gitignore,仅 `.env.example` 入库)。
+- 提交前自检:各端 build 通过;敏感值未入库(`.env` 走 .gitignore,仅 `.env.example` 入库);**vendor 变更必须与 index.html 版本表同步提交**。
 
 ## 4. 构建与本地运行
 
 ```bash
-pnpm install                        # 工作区安装
-pnpm --filter student dev           # 单端开发(student / parent / teacher)
-pnpm -r build                       # 三端全量构建 → apps/*/dist/(部署经 nginx 按端分发)
+pnpm install                  # 工作区安装(pnpm 10.12.1;lockfile 入库)
+pnpm dev:teacher              # 老师端开发(vite dev,port 5174;框架 UMD 经 publicDir=vendor 本地命中;/api 代理到后端 dev)
+pnpm build                    # 三端全量构建 → apps/*/dist/(当前仅 teacher 端有 build script)
+pnpm typecheck                # 双工程类型检查(根 shared + apps/teacher)
+pnpm lint                     # 全仓 ESLint
 ```
 
-**环境变量**(各端 `.env.example` 入库,实际 `.env.*` 不入库):`VITE_API_BASE`(后端 base)、`VITE_WECHAT_APPID`(家长端微信入口)。
+**环境变量**(`.env.example` 入库,实际 `.env.*` 不入库):`VITE_API_BASE`(后端 base)、`VITE_WECHAT_APPID`(家长端微信入口)。
+
+**框架层升级**:执行 §1.4 五步流程。
 
 ## 5. 与文档目录的关系
 
 - 本仓库**只放移动端代码**;家长绑定、费用、练习契约见工作区文档库 `academic/05-architecture/` 与 `teaching/05-architecture/`;学生端契约兼容基线见 `unified/00-planning/cross-system-reconciliation.md` §5。
-- 与 PC 端仓库共享编码惯例(统一信封/脱敏/权限码/commit 规范),差异仅在 UI 库(Vant)与 monorepo 形态。
+- 与 PC 端仓库共享编码惯例(统一信封/统一分页 PageData/脱敏/权限码三段式/commit 规范),差异:PC 端为 UMD 自托管(8 项含 Element Plus)+ 单项目 + SCSS;移动端为 **UMD 混合**(5 项 Vue 系 + Vant npm)+ pnpm monorepo + plain CSS token。

+ 30 - 0
apps/teacher/index.html

@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <title>统一管理平台 · 老师端</title>
+    <!--
+      【框架层挂载点 · vendor 版本表(一处维护,加载顺序固定)】
+      版本事实 = 本表 + 仓库 vendor/ 源文件(git 入库,只增不删;升级走框架升级流程)。
+      加载顺序固定(vue → vue-demi → vue-router → pinia → axios):
+        ① Vue 3(prod UMD)       全局 Vue
+        ② vue-demi(IIFE)        全局 VueDemi(pinia 桥接层,须先于 pinia;纯 Vue3 下等价 re-export Vue)
+        ③ Vue Router 4(prod UMD) 全局 VueRouter
+        ④ Pinia(IIFE prod)      全局 Pinia(依赖 VueDemi 全局,缺则 store 全崩)
+        ⑤ axios(UMD)            全局 axios(默认实例,含 post/get/create)
+      Vant 4 不在此:官方无 UMD 构建,走 npm 编译(main.ts import Vant + app.use(Vant))。
+      生产环境经 nginx/OSS 按 /vendor/<库>@<版本>/<文件> 提供;dev 经 vite publicDir 直接命中仓库 vendor/。
+    -->
+    <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/axios@1.21.0/axios.min.js"></script>
+    <!-- 业务入口(type=module 经 vite 处理;Vant 在此入口内 npm 引入) -->
+    <script type="module" src="/src/main.ts"></script>
+  </head>
+  <body>
+    <div id="app"></div>
+  </body>
+</html>

+ 20 - 0
apps/teacher/package.json

@@ -0,0 +1,20 @@
+{
+  "name": "unified-teacher",
+  "version": "0.1.0",
+  "private": true,
+  "description": "老师端(打卡、查看/批改练习、备课查看)——演示端:账密登录 + Tabbar 权限下发 + 机构列表 + 教务打卡",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@unified/shared": "workspace:*"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^6.0.10",
+    "vant": "4.10.2",
+    "vite": "^6.4.5"
+  }
+}

+ 13 - 0
apps/teacher/src/App.vue

@@ -0,0 +1,13 @@
+<script setup lang="ts">
+/**
+ * App.vue —— 老师端根组件。
+ *
+ * 只放一个 <router-view/>:套壳与否由路由树决定(登录页是顶层路由不套壳;
+ * 业务域是 layout 父路由的子路由,套 van-nav-bar + van-tabbar 壳)。
+ * 与 PC 端 App.vue 一致——不在此做布局,布局责任归 layout 壳。
+ */
+</script>
+
+<template>
+  <router-view />
+</template>

+ 60 - 0
apps/teacher/src/api/academic/checkin.ts

@@ -0,0 +1,60 @@
+/**
+ * checkin.ts —— 考勤打卡接口封装(academic 域;一资源一文件,编码规范)。
+ *
+ * 老师端核心场景:查看本班学生打卡记录、处理未打卡/异常打卡。
+ *
+ * 典型使用(views/academic/checkin/list.vue):
+ *
+ *   const lp = useListPage<CheckinRecordDTO>({
+ *     fetcher: (page, pageSize) => listCheckinRecords({ ...scope.toQuery(), date: filters.date, page, pageSize }),
+ *   });
+ */
+import { http } from '@unified/shared/api/http';
+import type { PageR, PageQuery } from '@unified/shared/types/api';
+
+/** 打卡记录 DTO(与后端 academic 域 CheckinRecord 逐字段对齐,camelCase) */
+export interface CheckinRecordDTO {
+  /** 记录 id */
+  id: string;
+  /** 学生 canonical_user_id(ULID) */
+  studentId: string;
+  /** 学生姓名(后端已脱敏展示名) */
+  studentName: string;
+  /** 班级名称 */
+  className: string;
+  /** 打卡日期(yyyy-MM-dd) */
+  checkinDate: string;
+  /** 打卡时间(HH:mm:ss;未到为 null) */
+  checkinTime: string | null;
+  /** 打卡状态(1 正常 / 2 迟到 / 3 缺勤 / 4 请假) */
+  status: number;
+  /** 备注(请假原因/异常说明,可选) */
+  remark: string | null;
+}
+
+/** 打卡记录查询参数(page/pageSize + 作用域 + 日期) */
+export interface CheckinFilter extends PageQuery {
+  /** 作用域:机构 id(必填,由 useScope().toQuery() 并入) */
+  institutionId: number;
+  /** 作用域:校区 id(0 = 机构级,缺省时后端不过滤) */
+  campusId?: number;
+  /** 打卡日期(yyyy-MM-dd,必填,默认今日由页面注入) */
+  date: string;
+  /** 班级 id(可选;不传查全机构/全校区) */
+  classId?: number;
+}
+
+/**
+ * 打卡记录列表(GET /api/academic/checkin/records,统一分页信封 PageR<CheckinRecordDTO>)。
+ *
+ * 入参:
+ *   - params CheckinFilter:page/pageSize + 作用域 + date(必填)+ classId(可选)。
+ *
+ * 出参:
+ *   - Promise<PageR<CheckinRecordDTO>>:code=200 时 data 为 PageData;useListPage 统一解 list/total。
+ * 异常场景:
+ *   - 作用域越界:后端 3xxx 段 → useListPage 兜底空 + error 展示 message。
+ */
+export function listCheckinRecords(params: CheckinFilter): Promise<PageR<CheckinRecordDTO>> {
+  return http.get<PageR<CheckinRecordDTO>>('/api/academic/checkin/records', { params }).then((r) => r.data);
+}

+ 52 - 0
apps/teacher/src/api/org/institution.ts

@@ -0,0 +1,52 @@
+/**
+ * institution.ts —— 机构列表接口封装(org 域;一资源一文件,编码规范)。
+ *
+ * 典型使用(views/org/institution/list.vue):
+ *
+ *   const lp = useListPage<InstitutionDTO>({
+ *     fetcher: (page, pageSize) => listInstitutions({ ...scope.toQuery(), keyword: filters.keyword, page, pageSize }),
+ *   });
+ */
+import { http } from '@unified/shared/api/http';
+import type { PageR, PageQuery } from '@unified/shared/types/api';
+
+/** 机构 DTO(与后端 org 域 Institution 逐字段对齐,camelCase) */
+export interface InstitutionDTO {
+  /** 机构 id(作用域 institution) */
+  institutionId: number;
+  /** 机构名称 */
+  name: string;
+  /** 机构编码(旧系统 school 归一) */
+  code: string;
+  /** 联系人 */
+  contactName: string;
+  /** 联系电话(后端已脱敏) */
+  contactPhone: string;
+  /** 状态(1 启用 / 0 停用) */
+  status: number;
+}
+
+/** 机构列表查询参数(page/pageSize + 作用域 + 关键词) */
+export interface InstitutionFilter extends PageQuery {
+  /** 作用域:机构 id(必填,由 useScope().toQuery() 并入) */
+  institutionId: number;
+  /** 作用域:校区 id(0 = 机构级,缺省时后端不过滤) */
+  campusId?: number;
+  /** 关键词(机构名/编码模糊匹配,可选) */
+  keyword?: string;
+}
+
+/**
+ * 机构列表(GET /api/org/institutions,统一分页信封 PageR<InstitutionDTO>)。
+ *
+ * 入参:
+ *   - params InstitutionFilter:page/pageSize + 作用域 + 关键词。
+ *
+ * 出参:
+ *   - Promise<PageR<InstitutionDTO>>:code=200 时 data 为 PageData;useListPage 统一解 list/total。
+ * 异常场景:
+ *   - 作用域越界(institutionId 不匹配 token 作用域):后端 3xxx 段,data 省略 → useListPage 兜底空 + error。
+ */
+export function listInstitutions(params: InstitutionFilter): Promise<PageR<InstitutionDTO>> {
+  return http.get<PageR<InstitutionDTO>>('/api/org/institutions', { params }).then((r) => r.data);
+}

+ 75 - 0
apps/teacher/src/main.ts

@@ -0,0 +1,75 @@
+/**
+ * main.ts —— 老师端业务装配入口(README §技术栈:取 UMD 全局创建应用,框架运行时不 import)。
+ *
+ * 装配顺序(与 index.html 的 vendor 加载顺序对应,8 步):
+ *   1. 取 Vue UMD 全局(裸标识符经 vite define 注入为 Vue.*;createApp 显式走 Vue.createApp);
+ *   2. Pinia:createPinia(裸标识符注入 Pinia.createPinia)→ app.use;
+ *   3. 路由:buildRouter + setupGuards(守卫链:登录态 → 静默刷新 → 公开白名单 → 作用域就绪);
+ *   4. 注入路由实例给 http 拦截器(401 跳登录走 SPA push,免全页刷新)+ 注入业务错误 toast 渲染器(Vant Message);
+ *   5. Vant:import 自 npm(官方无 UMD,编译进 bundle)→ app.use(Vant, { locale: zhCn });
+ *      Vant CSS 经 import 'vant/lib/index.css' 引入(编译进 bundle,移动端不做 UMD 自托管);
+ *   6. 权限指令:v-perm(权限码 `域.资源.动作` 三段式);
+ *   7. 登录态 hydrate(页面刷新后从 token-store 恢复 UI 状态,守卫链第①步即可用);
+ *   8. mount('#app')。
+ *
+ * 异常场景:
+ *   - UMD 全局缺失(index.html 漏引 / vendor 404):typeof Vue === 'undefined' 时 fail-fast 白屏提示,
+ *     便于区分"框架没加载"与"业务代码 bug"。
+ *
+ * 注:Vant 经 npm import 是**装配点例外**(非业务值 import 框架运行时)——main.ts 是"应用工厂",
+ *   其职责就是装配 UI 库;Vant 不在 vite.config 的 define 清单(它不是 UMD 全局,无法 define 注入),
+ *   也不在 eslint 禁值 import 清单(仅拦 UMD 全局框架 vue/vue-router/pinia/axios)。
+ */
+import Vant from 'vant';
+import 'vant/lib/index.css';
+import { showFailToast } from 'vant';
+// 全局设计 token(CSS 自定义属性;须在 App 挂载前引入,组件 var() 引用才生效)
+import '@/styles/tokens.css';
+
+import { buildRouter, setupGuards } from '@/router';
+import { useLogin } from '@unified/shared/store/login';
+import { perm } from '@unified/shared/directives/perm';
+import { setBusinessErrorToaster, setRouterForRedirect } from '@unified/shared/api/http';
+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:#ee0a24;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');
+
+// 1. 应用实例(裸标识符 createApp 由 vite define 注入为 Vue.createApp)
+const app = createApp(App);
+
+// 2. Pinia(状态:login + scope,shared 层)
+app.use(createPinia());
+
+// 3. 路由 + 守卫链
+const router = buildRouter();
+setupGuards(router);
+app.use(router);
+
+// 4. 注入路由实例 + 业务错误 toast 渲染器(http 拦截器零 UI 依赖,经此解耦)
+setRouterForRedirect(router);
+setBusinessErrorToaster((m) => showFailToast(m));
+
+// 5. Vant(npm 编译;Vant 4 默认简体中文 locale,无需显式传 locale;CSS 已在顶部引入)
+app.use(Vant);
+
+// 6. 权限指令(按钮级 v-perm,权限码 `域.资源.动作`)
+app.directive('perm', perm);
+
+// 7. 登录态 hydrate(必须在首次路由解析前:守卫链第①步要读 isLoggedIn)
+useLogin().hydrate();
+
+// 8. 挂载
+app.mount('#app');

+ 138 - 0
apps/teacher/src/router/index.ts

@@ -0,0 +1,138 @@
+/**
+ * index.ts —— 老师端路由表 + 守卫链(编码规范:路由与鉴权 / 守卫链顺序)。
+ *
+ * 守卫链顺序(全局 beforeEach,按序短路,与 PC 端同源):
+ *   ① 白名单:登录页/公开路由直接放行(放在登录判断前);
+ *   ② 登录态:login.isLoggedIn(有 access 且未过期,提前 30s 判过期);
+ *   ③ 刷新:access 过期但有 refresh → 静默 refresh(refreshAccessToken 单例)→ 成功放行 / 失败跳登录;
+ *   ④ 作用域:路由 meta.scopeRequired(默认 true)且 scope 未就绪 → 跳登录。
+ *
+ * 与 PC 端差异(移动端作用域模型):
+ *   - PC 端有独立 /org/scope 作用域选择页(登录后全屏过渡页);
+ *   - 移动端**无**独立作用域选择页——登录响应直接下发 institutionId/campusId(见 LoginResult),
+ *     登录成功时由登录页调 scope.set 初始化作用域,随后进 layout 首页。
+ *     故守卫④"作用域未就绪"时无选择页可去,直接跳登录(未就绪 = 登录响应异常,需重登)。
+ */
+import { useLogin } from '@unified/shared/store/login';
+import { useScope } from '@unified/shared/store/scope';
+import { refreshAccessToken } from '@unified/shared/api/http';
+import type { RouteRecordRaw } from 'vue-router';
+
+/** 路由 meta 扩展(业务路由可声明的守卫参数) */
+declare module 'vue-router' {
+  interface RouteMeta {
+    /** 是否需要登录(默认 true;登录页/公开路由设 false) */
+    public?: boolean;
+    /** 是否要求作用域就绪(默认 true;登录页设 false) */
+    scopeRequired?: boolean;
+    /** 路由标题(document.title 同步用,中文) */
+    title?: string;
+  }
+}
+
+/**
+ * 路由表(createRouter 的 routes 入参;vite define 注入为 VueRouter.createRouter)。
+ *
+ * 套壳模型(嵌套路由):
+ *   - 顶层路由**不套壳**:/login(登录,公开);
+ *   - **layout 父路由**(path '/',component = views/layout):van-nav-bar + 内容区 <router-view/>
+ *     + van-tabbar;业务域页面全部作为其 children(相对 path),统一带底部 Tabbar 壳;
+ *   - 访问 '/' 自动 redirect 到首个业务页 /org/institutions。
+ *
+ * 出参:
+ *   - RouteRecordRaw[]:/login / layout(children: org/institutions, academic/checkin) / 兜底 404。
+ */
+export const routes: RouteRecordRaw[] = [
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/login/index.vue'),
+    meta: { public: true, scopeRequired: false, title: '登录' },
+  },
+  {
+    // layout 父路由:套壳(顶部 nav-bar + 底部 tabbar + 内容区 router-view)。业务域路由嵌套其下。
+    path: '/',
+    component: () => import('@/views/layout/index.vue'),
+    redirect: '/org/institutions',
+    children: [
+      {
+        path: 'org/institutions',
+        name: 'InstitutionList',
+        component: () => import('@/views/org/institution/list.vue'),
+        meta: { title: '机构管理' },
+      },
+      {
+        // 教务域示范页(打卡)——跨域示范:teacher 端覆盖教务 + 教学两域
+        path: 'academic/checkin',
+        name: 'CheckinList',
+        component: () => import('@/views/academic/checkin/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,
+  });
+}
+
+/**
+ * 全局前置守卫(挂载后生效;编码规范:守卫链)。
+ *
+ * 出参:
+ *   - 省略返回值 = 放行;返回 false / 跳转 = 拦截。
+ *
+ * 异常场景:
+ *   - 守卫内 pinia 未激活(极端早启动):useLogin 抛错——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: 'Login' };
+    }
+
+    return true;
+  });
+
+  // 路由标题同步(document.title 中文,默认简体中文)
+  router.afterEach((to) => {
+    document.title = to.meta.title ? `${to.meta.title} · 统一管理平台 · 老师端` : '统一管理平台 · 老师端';
+  });
+}

+ 34 - 0
apps/teacher/src/styles/tokens.css

@@ -0,0 +1,34 @@
+/**
+ * tokens.css —— 老师端全局设计 token(CSS 自定义属性,:root 注入)。
+ *
+ * 约定(编码规范:业务组件 <style> 禁硬编码色值/尺寸,必须引 token):
+ *   - 色板:页面/卡片背景、文字主次、品牌色;
+ *   - 间距:xs/sm/md/lg 四级;
+ *   - 字号:sm/md/lg;
+ *   - 布局量:导航栏/Tabbar 高度(layout 壳留位用)。
+ * 由 main.ts 在 App 挂载前 import 引入(Vite 注入 :root 变量,所有组件 var() 引用才生效)。
+ * 采用 plain CSS(非 SCSS):CSS 自定义属性无需预处理器,省去 sass 构建依赖。
+ */
+:root {
+  /* 色板 */
+  --app-bg-page: #f7f8fa;
+  --app-bg-card: #ffffff;
+  --app-text-primary: #1f2329;
+  --app-text-secondary: #969799;
+  --app-brand: #1989fa;
+
+  /* 间距 */
+  --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: 18px;
+
+  /* 布局量(layout 壳留位用) */
+  --app-nav-bar-height: 46px;
+  --app-tabbar-height: 50px;
+}

+ 134 - 0
apps/teacher/src/views/academic/checkin/list.vue

@@ -0,0 +1,134 @@
+<script setup lang="ts">
+/**
+ * list.vue —— 考勤打卡列表页(academic 域示范页;老师端核心场景,跨域示范 org + academic)。
+ *
+ * 收口:分页五元组 + 统一分页解构 + 下拉刷新/上拉加载,经 shared 的 useListPage + list-page;
+ * 作用域 institutionId/campusId 由 useScope().toQuery() 并入 fetcher(页面禁自拼)。
+ * 日期默认今日(原生 Date 取 yyyy-MM-dd,不引 dayjs——纯取当日字符串,无需日期库)。
+ * 打卡状态 1/2/3/4 → 正常/迟到/缺勤/请假(van-tag 色板,纯数据映射,零硬编码色值)。
+ */
+import { useListPage } from '@unified/shared/composables/use-list-page';
+import { useScope } from '@unified/shared/store/scope';
+import ListPage from '@unified/shared/components/list-page.vue';
+import { listCheckinRecords, type CheckinRecordDTO, type CheckinFilter } from '@/api/academic/checkin';
+
+const scope = useScope();
+
+/** 今日日期(yyyy-MM-dd;原生 Date 取,本地时区) */
+function today(): string {
+  const d = new Date();
+  const m = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  return `${d.getFullYear()}-${m}-${day}`;
+}
+
+/** 日期筛选(默认今日) */
+const filters = reactive<{ date: string }>({ date: today() });
+
+// 列表状态解构到顶层(useListPage 返回 Ref 集合,顶层 ref 才会被模板自动解包;
+// fetcher 内 (page, pageSize) 是拉取入参,与顶层 page ref 同名遮蔽属预期)
+const { rows, total, loading, error, page, fetchList } = useListPage<CheckinRecordDTO>({
+  fetcher: (page, pageSize) => {
+    const q: CheckinFilter = { ...scope.toQuery(), date: filters.date, page, pageSize };
+    return listCheckinRecords(q);
+  },
+});
+
+/** 打卡状态 → { 文案, van-tag type }(纯数据映射,编码规范:Badge 色板零硬编码) */
+const STATUS_MAP: Record<number, { label: string; type: 'success' | 'warning' | 'danger' | 'primary' }> = {
+  1: { label: '正常', type: 'success' },
+  2: { label: '迟到', type: 'warning' },
+  3: { label: '缺勤', type: 'danger' },
+  4: { label: '请假', type: 'primary' },
+};
+
+function statusMeta(status: number): { label: string; type: 'success' | 'warning' | 'danger' | 'primary' } {
+  return STATUS_MAP[status] ?? { label: '未知', type: 'primary' };
+}
+
+/** 上拉加载下一页(page++ 触发 useListPage 的 watch → 自动 fetchList) */
+function onLoad(): void {
+  page.value += 1;
+}
+
+/** 下拉刷新(重置第一页) */
+function onRefresh(): void {
+  page.value = 1;
+}
+
+/** 切换日期后重查(重置第一页,显式 fetchList 立即拉取,不等 watch 防抖) */
+function onDateChange(): void {
+  page.value = 1;
+  void fetchList();
+}
+
+onMounted(() => {
+  fetchList();
+});
+</script>
+
+<template>
+  <div class="checkin-list">
+    <!-- 日期选择(原生 date input;变更后重置第一页重查) -->
+    <div class="checkin-list__bar">
+      <input
+        v-model="filters.date"
+        type="date"
+        class="checkin-list__date"
+        @change="onDateChange"
+      />
+    </div>
+
+    <list-page
+      :rows="rows"
+      :total="total"
+      :loading="loading"
+      :error="error"
+      @load="onLoad"
+      @refresh="onRefresh"
+    >
+      <template #default="{ row }">
+        <van-cell :title="row.studentName" :label="row.className">
+          <template #value>
+            <van-tag :type="statusMeta(row.status).type">{{ statusMeta(row.status).label }}</van-tag>
+          </template>
+        </van-cell>
+        <div class="checkin-list__time">
+          <span v-if="row.checkinTime">{{ row.checkinTime }}</span>
+          <span v-else class="checkin-list__none">未打卡</span>
+          <span v-if="row.remark" class="checkin-list__remark">· {{ row.remark }}</span>
+        </div>
+      </template>
+    </list-page>
+  </div>
+</template>
+
+<style scoped>
+.checkin-list {
+  padding-bottom: var(--app-spacing-md);
+}
+.checkin-list__bar {
+  padding: var(--app-spacing-sm) var(--app-spacing-md);
+}
+.checkin-list__date {
+  width: 100%;
+  box-sizing: border-box;
+  padding: var(--app-spacing-sm) var(--app-spacing-md);
+  border: 1px solid var(--app-bg-page);
+  border-radius: 8px;
+  font-size: var(--app-font-md);
+  color: var(--app-text-primary);
+  background: var(--app-bg-card);
+}
+.checkin-list__time {
+  padding: 0 var(--app-spacing-md) var(--app-spacing-sm);
+  font-size: var(--app-font-sm);
+  color: var(--app-text-secondary);
+}
+.checkin-list__none {
+  color: var(--app-text-secondary);
+}
+.checkin-list__remark {
+  margin-left: var(--app-spacing-xs);
+}
+</style>

+ 93 - 0
apps/teacher/src/views/layout/index.vue

@@ -0,0 +1,93 @@
+<script setup lang="ts">
+/**
+ * layout/index.vue —— 老师端页面骨架(移动端壳:顶部 van-nav-bar + 内容区 + 底部 van-tabbar)。
+ *
+ * 职责边界:
+ *   - 提供**统一骨架**:van-nav-bar(标题/退出)+ 内容区 <router-view/>(渲染嵌套路由)+
+ *     底部 <tabbar-menu/>(递归渲染 login.tabTree,权限码下发);
+ *   - **不持有业务状态**:菜单树读 useLogin().tabTree、用户读 useLogin().realName、
+ *     机构读 useScope().current——全部从 store 读,本组件零数据逻辑;
+ *   - 作为**父路由 component** 使用(router/index.ts 嵌套路由):App.vue 只放一个 <router-view/>,
+ *     套壳与否由路由树决定(登录页是顶层路由不套壳;业务域路由是 layout 的子路由,套壳)。
+ *
+ * 与 PC 端 layout 的差异:PC 用 el-aside 侧边菜单(左)+ el-header(上);移动端用
+ *   van-nav-bar(上)+ van-tabbar(下),内容区在中。底部 tabbar fixed 需给内容区 padding-bottom 留位。
+ *
+ * 退出:doLogout(调后端吊销 + 清本地态)→ router.push('/login')(SPA,免全页刷新)。
+ *   showConfirmDialog 二次确认(编码规范:敏感操作)。
+ */
+import { useLogin } from '@unified/shared/store/login';
+import { useScope } from '@unified/shared/store/scope';
+import TabbarMenu from '@unified/shared/components/tabbar-menu.vue';
+import { showConfirmDialog } from 'vant';
+
+const login = useLogin();
+const scope = useScope();
+const router = useRouter();
+
+/** 退出二次确认(敏感操作)→ 登出 → 跳登录页 */
+async function handleLogout(): Promise<void> {
+  try {
+    await showConfirmDialog({ title: '退出登录', message: '确认退出当前账号?' });
+  } catch {
+    return; // 用户点取消:不退出
+  }
+  await login.doLogout();
+  await router.push('/login');
+}
+</script>
+
+<template>
+  <div class="app-layout">
+    <!-- 顶栏:标题 + 机构作用域 + 当前用户 + 退出 -->
+    <van-nav-bar title="统一管理平台 · 老师端" fixed placeholder>
+      <template #right>
+        <div class="app-layout__user">
+          <span class="app-layout__scope" v-if="scope.ready">{{ scope.current.institutionId }}</span>
+          <span class="app-layout__name">{{ login.realName }}</span>
+          <van-icon name="wap-home-o" @click="handleLogout" class="app-layout__logout" />
+        </div>
+      </template>
+    </van-nav-bar>
+
+    <!-- 内容区:嵌套路由出口(业务域页面渲染于此);padding-bottom 给底部 tabbar 留位 -->
+    <div class="app-layout__main">
+      <router-view />
+    </div>
+
+    <!-- 底部导航:权限码下发(tabTree 由 login store 下发,tabbar-menu 原样渲染) -->
+    <tabbar-menu :tab-tree="login.tabTree" />
+  </div>
+</template>
+
+<style scoped>
+.app-layout {
+  min-height: 100vh;
+  background: var(--app-bg-page);
+  box-sizing: border-box;
+}
+.app-layout__user {
+  display: flex;
+  align-items: center;
+  gap: var(--app-spacing-xs);
+  font-size: var(--app-font-sm);
+}
+.app-layout__scope {
+  color: var(--app-text-secondary);
+}
+.app-layout__name {
+  color: var(--app-text-primary);
+  max-width: 96px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.app-layout__logout {
+  color: var(--app-text-secondary);
+  padding: 4px;
+}
+.app-layout__main {
+  padding-bottom: var(--app-tabbar-height, 50px); /* 给底部 fixed tabbar 留位 */
+  min-height: calc(100vh - var(--app-nav-bar-height, 46px) - var(--app-tabbar-height, 50px));
+}
+</style>

+ 266 - 0
apps/teacher/src/views/login/index.vue

@@ -0,0 +1,266 @@
+<script setup lang="ts">
+/**
+ * 登录页(views/login/index.vue;公开路由,守卫 meta.public)。
+ *
+ * 职责:两种登录方式 tab 切换——① 账号密码(POST /api/auth/login,真)② 微信 per-campus 授权
+ * (占位接口,后端端点未就绪,UI 骨架完整可联调);成功则写 token-store + 初始化作用域
+ * (移动端作用域来自登录响应 institutionId/campusId,无独立选择页)+ router.push 进首页;
+ * 失败展示后端 message(直显,不包装)。
+ * SFC 骨架顺序(编码规范):imports → props/emits(无)→ store 注入 → 状态 → 生命周期 → 方法。
+ *
+ * 注:模板 van-* 组件经 main.ts 的 app.use(Vant) 全局注册,本页**不 import Vant 组件**
+ * (仅 import Vant Message/Toast 命令式 API 作按需增强;与 PC 端"组件走全局、命令式按需引"一致)。
+ */
+import { login } from '@unified/shared/api/auth/login';
+import { authorizeWechat, pollWechatStatus } from '@unified/shared/api/auth/wechat';
+import { useLogin } from '@unified/shared/store/login';
+import { useScope } from '@unified/shared/store/scope';
+import { showToast } from 'vant';
+import type { FieldRule } from 'vant';
+
+const loginStore = useLogin();
+const scope = useScope();
+/** 路由实例(SPA 跳转,免全页刷新;裸标识符经 vite define 注入为 VueRouter.useRouter) */
+const router = useRouter();
+
+/** 登录方式 tab('password' 账号密码 / 'wechat' 微信授权;用 string 兼容 Vant v-model:active 的 string|number 双向绑定) */
+const activeTab = ref<string>('password');
+
+/** 表单数据(单一数据源;rules 校验只拦格式,业务错误由后端 code 裁决) */
+const form = reactive({
+  username: '',
+  password: '',
+});
+
+/** 提交中(防重复提交;密码 tab 用) */
+const submitting = ref(false);
+/** 登录失败文案(后端 message 直显;空串 = 无错误;两 tab 共用) */
+const errorMsg = ref('');
+
+/**
+ * 微信授权态(占位,后端端点未就绪——接口路径/字段见 shared/api/auth/wechat.ts 占位注释)。
+ * 入参:无。
+ * 出参:
+ *   - authUrl string:授权 URL('' = 未取到,扫码/授权区展示"暂未开放"占位);
+ *   - polling boolean:是否正在轮询(驱动"授权中"文案)。
+ * 异常场景:
+ *   - 后端未就绪(authorizeWechat reject):authUrl 保持 '',授权区展示占位文案。
+ */
+const authUrl = ref('');
+const polling = ref(false);
+/** 轮询定时器句柄(组件卸载时必须清,防泄漏;占位接口下逻辑完整仍保留) */
+let pollTimer: ReturnType<typeof setInterval> | undefined;
+
+/** 表单校验规则(显式 rules 对象,编码规范;trigger 为 Vant FieldValidateTrigger:onBlur/onChange/onSubmit) */
+const rules: Record<'username' | 'password', FieldRule[]> = {
+  username: [{ required: true, message: '请输入用户名', trigger: 'onBlur' }],
+  password: [{ required: true, message: '请输入密码', trigger: 'onBlur' }],
+};
+
+/** 登录成功后统一落盘:applyLogin(写 token + 视图)+ 初始化作用域 + 跳首页 */
+async function onLoginSuccess(res: {
+  institutionId?: number;
+  campusId?: number;
+}): Promise<void> {
+  if (res.institutionId) {
+    // 移动端作用域来自登录响应(无独立选择页);institutionId 缺失则由守卫④拦回登录
+    scope.set({ institutionId: res.institutionId, campusId: res.campusId ?? 0 });
+  }
+  await router.push('/');
+}
+
+/**
+ * 提交账密登录(handle 前缀:用户触发的提交类方法,编码规范命名规则)。
+ *
+ * 出参:
+ *   - Promise<void>:恒 resolve;成功进首页,失败写 errorMsg。
+ * 异常场景:
+ *   - 表单校验失败:不发请求(van-form 标红);
+ *   - 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);
+      await onLoginSuccess(res.data);
+      return;
+    }
+    errorMsg.value = res.message || '登录失败';
+  } catch {
+    errorMsg.value = '网络连接异常';
+  } finally {
+    submitting.value = false;
+  }
+}
+
+/** 停止微信授权轮询(切 tab / 组件卸载 / 终态时调用;幂等) */
+function stopPolling(): void {
+  if (pollTimer !== undefined) {
+    clearInterval(pollTimer);
+    pollTimer = undefined;
+  }
+  polling.value = false;
+}
+
+/**
+ * 切换登录方式 tab(切到微信时发起授权 + 启动轮询;切走时停轮询)。
+ *
+ * 入参:tab 'password' | 'wechat'(van-tabs @change 回调值)。
+ * 出参:无(异步启动授权流程)。
+ * 异常场景:
+ *   - 后端未就绪:authorizeWechat reject → 停轮询,授权区展示占位文案。
+ */
+async function handleTabChange(tab: string | number): Promise<void> {
+  activeTab.value = tab === 'wechat' ? 'wechat' : 'password';
+  stopPolling();
+  errorMsg.value = '';
+  if (activeTab.value === 'wechat') {
+    authUrl.value = '';
+    // 取当前作用域校区(未登录时 institutionId/campusId 为 0,后端取默认 appid)
+    const campusId = scope.current.campusId ?? 0;
+    try {
+      const auth = await authorizeWechat(campusId);
+      if (auth.code === 200 && auth.data) {
+        authUrl.value = auth.data.url;
+        // 轮询授权状态(1.5s 间隔;占位节奏,后端就绪后可调)
+        const state = auth.data.state;
+        polling.value = true;
+        pollTimer = setInterval(async () => {
+          try {
+            const s = await pollWechatStatus(state);
+            if (s.code !== 200 || !s.data) return; // 单次轮询业务错误:静默,下轮重试
+            const { status, token } = s.data;
+            if (status === 'success' && token) {
+              stopPolling();
+              loginStore.applyLogin(token);
+              await onLoginSuccess(token);
+            } else if (status === 'cancel' || status === 'expire') {
+              stopPolling();
+              authUrl.value = ''; // 终态:清空授权态,提示重试
+            }
+          } catch {
+            // 单次轮询网络故障:静默(不中断轮询,下轮重试)
+          }
+        }, 1500);
+      }
+    } catch {
+      // 后端未就绪 / 取授权失败:占位(保持 authUrl 空,授权区显"暂未开放")
+      stopPolling();
+      showToast('微信授权暂未开放');
+    }
+  }
+}
+
+// 组件卸载清轮询定时器(防内存泄漏;登录页被守卫跳转销毁时必清)
+onUnmounted(() => {
+  stopPolling();
+});
+
+/** 打开微信授权页(新窗口;占位——真实 per-campus 授权多为整页跳转,此处新窗口便于联调观察) */
+function openWechatAuth(): void {
+  if (authUrl.value) window.open(authUrl.value, '_blank');
+}
+</script>
+
+<template>
+  <div class="login-page">
+    <van-cell-group inset class="login-card">
+      <van-cell title="统一管理平台 · 老师端" class="login-title" />
+    </van-cell-group>
+
+    <van-notice-bar v-if="errorMsg" color="#ee0a24" background="#fff1f0" :text="errorMsg" class="login-error" />
+
+    <!-- 登录方式切换(账号密码 / 微信授权):@change 切到微信时发起授权 + 启动轮询 -->
+    <van-tabs v-model:active="activeTab" @change="handleTabChange" class="login-tabs">
+      <!-- 账号密码 -->
+      <van-tab title="账号密码" name="password">
+        <van-form @submit="handleSubmit">
+          <van-cell-group inset>
+            <van-field
+              v-model="form.username"
+              name="username"
+              label="用户名"
+              placeholder="请输入用户名"
+              autocomplete="username"
+              :rules="rules.username"
+            />
+            <van-field
+              v-model="form.password"
+              name="password"
+              type="password"
+              label="密码"
+              placeholder="请输入密码"
+              autocomplete="current-password"
+              :rules="rules.password"
+            />
+          </van-cell-group>
+          <div class="login-actions">
+            <van-button round block type="primary" native-type="submit" :loading="submitting">
+              登 录
+            </van-button>
+          </div>
+        </van-form>
+      </van-tab>
+
+      <!-- 微信授权(占位:后端端点未就绪,UI 骨架完整;授权 URL 取到前显占位框) -->
+      <van-tab title="微信授权" name="wechat">
+        <div class="login-wechat">
+          <van-icon v-if="authUrl" name="wechat" size="64" color="#07c160" />
+          <van-icon v-else name="cross" size="64" color="var(--app-text-secondary)" />
+          <p class="login-wechat__tip">
+            {{ polling ? '请使用微信完成授权…' : '切换至此自动发起授权' }}
+          </p>
+          <van-button v-if="authUrl" type="primary" round @click="openWechatAuth">
+            打开微信授权
+          </van-button>
+          <van-button v-else type="default" round plain disabled>
+            微信授权暂未开放
+          </van-button>
+        </div>
+      </van-tab>
+    </van-tabs>
+  </div>
+</template>
+
+<style scoped>
+.login-page {
+  min-height: 100vh;
+  padding: var(--app-spacing-md);
+  background: var(--app-bg-page);
+  box-sizing: border-box;
+}
+.login-card {
+  margin-top: var(--app-spacing-lg);
+}
+.login-title .van-cell__title {
+  font-size: var(--app-font-lg);
+  font-weight: 600;
+  text-align: center;
+}
+.login-error {
+  margin: var(--app-spacing-sm) 16px;
+}
+.login-tabs {
+  margin-top: var(--app-spacing-sm);
+}
+.login-actions {
+  padding: var(--app-spacing-md);
+}
+.login-wechat {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: var(--app-spacing-lg) 0;
+  gap: var(--app-spacing-md);
+}
+.login-wechat__tip {
+  margin: 0;
+  font-size: var(--app-font-sm);
+  color: var(--app-text-secondary);
+}
+</style>

+ 83 - 0
apps/teacher/src/views/org/institution/list.vue

@@ -0,0 +1,83 @@
+<script setup lang="ts">
+/**
+ * list.vue —— 机构管理列表页(org 域示范页;编码规范:列表页一律 useListPage 收口)。
+ *
+ * 收口:分页五元组 + 统一分页解构 + 下拉刷新/上拉加载,全部经 shared 的 useListPage + list-page;
+ * 作用域 institutionId/campusId 由 useScope().toQuery() 并入 fetcher(页面禁自拼,对齐后端强制注入)。
+ * 模板 van-* 组件经 main.ts app.use(Vant) 全局注册,本页不 import 组件。
+ */
+import { useListPage } from '@unified/shared/composables/use-list-page';
+import { useScope } from '@unified/shared/store/scope';
+import ListPage from '@unified/shared/components/list-page.vue';
+import { listInstitutions, type InstitutionDTO, type InstitutionFilter } from '@/api/org/institution';
+
+const scope = useScope();
+
+/** 关键词筛选(单一数据源) */
+const filters = reactive<{ keyword: string }>({ keyword: '' });
+
+// 列表状态解构到顶层(useListPage 返回 Ref 集合,顶层 ref 才会被模板自动解包;
+// fetcher 内 (page, pageSize) 是拉取入参,与顶层 page ref 同名遮蔽属预期)
+const { rows, total, loading, error, page, fetchList } = useListPage<InstitutionDTO>({
+  fetcher: (page, pageSize) => {
+    const q: InstitutionFilter = { ...scope.toQuery(), keyword: filters.keyword || undefined, page, pageSize };
+    return listInstitutions(q);
+  },
+});
+
+/** 上拉加载下一页(page++ 触发 useListPage 的 watch → 自动 fetchList) */
+function onLoad(): void {
+  page.value += 1;
+}
+
+/** 下拉刷新(重置第一页) */
+function onRefresh(): void {
+  page.value = 1;
+}
+
+/** 搜索(关键词变更后重置第一页重查) */
+function onSearch(): void {
+  page.value = 1;
+}
+
+onMounted(() => {
+  fetchList();
+});
+</script>
+
+<template>
+  <div class="org-list">
+    <!-- 搜索条 -->
+    <van-search
+      v-model="filters.keyword"
+      placeholder="搜索机构名/编码"
+      @search="onSearch"
+      @clear="onSearch"
+    />
+
+    <list-page
+      :rows="rows"
+      :total="total"
+      :loading="loading"
+      :error="error"
+      @load="onLoad"
+      @refresh="onRefresh"
+    >
+      <template #default="{ row }">
+        <van-cell :title="row.name" :label="`编码 ${row.code}`">
+          <template #value>
+            <van-tag :type="row.status === 1 ? 'success' : 'danger'">
+              {{ row.status === 1 ? '启用' : '停用' }}
+            </van-tag>
+          </template>
+        </van-cell>
+      </template>
+    </list-page>
+  </div>
+</template>
+
+<style scoped>
+.org-list {
+  padding-bottom: var(--app-spacing-md);
+}
+</style>

+ 22 - 0
apps/teacher/tsconfig.json

@@ -0,0 +1,22 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@": ["./src"],
+      "@/*": ["./src/*"],
+      "@unified/shared": ["../../packages/shared/src"],
+      "@unified/shared/*": ["../../packages/shared/src/*"]
+    }
+  },
+  "include": [
+    "../../global.d.ts",
+    "./src/**/*.ts",
+    "./src/**/*.vue",
+    "./src/**/*.d.ts",
+    "../../packages/shared/src/**/*.ts",
+    "../../packages/shared/src/**/*.vue",
+    "../../shims/**/*.ts"
+  ],
+  "exclude": ["node_modules", "dist", "../../node_modules", "../../vendor"]
+}

+ 83 - 0
apps/teacher/vite.config.ts

@@ -0,0 +1,83 @@
+import { fileURLToPath, URL } from 'node:url';
+
+import vue from '@vitejs/plugin-vue';
+import { defineConfig } from 'vite';
+
+/**
+ * 框架运行时混合方案(移动端,README §技术栈):
+ *
+ * ① UMD 全局(Vue 系 + axios):index.html 按序引入 vendor/ 自托管 UMD(dev 经 publicDir 服务,
+ *    生产经 nginx/OSS),裸标识符(ref、computed、defineStore…)经下方 define 编译期替换为
+ *    Vue.*、VueRouter.*、Pinia.* 全局访问;SFC 编译器产物的模块名 import 经 alias 重定向到
+ *    仓库根 shims 目录(运行时取 UMD 全局转发,框架本体不进 dist)。
+ *
+ * ② npm 编译(Vant):Vant 4 官方无 UMD 构建,只能 import 编译进 bundle——
+ *    故 Vant **不进** define/alias 清单(`app.use(Vant)` 在 main.ts 走 npm import),
+ *    模板里 van-* 组件由 Vue 全局组件解析(经 alias 的 vue 转发),无需 vant shim。
+ *    dist 因此含 Vant 运行时(与 PC 端"dist 不含 ElementPlus"的差异点,由 Vant 无 UMD 决定)。
+ */
+
+/** 可裸写的 Vue 组合式 API 标识符(编译期注入为 Vue.*)——与 PC 端同源清单 */
+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(serve):把仓库根 vendor/ 当 public 根,index.html 的 /vendor/<库>@<版本>/<文件> 直接命中,
+   *   无需 OSS 即可联调;build:不拷贝任何框架文件进 dist(框架由 nginx/OSS 在运行时提供)。
+   *   注:publicDir 相对 root(本 app 目录)解析,故 ../../vendor = 仓库根 vendor。
+   */
+  publicDir: command === 'serve' ? '../../vendor' : false,
+  resolve: {
+    alias: {
+      // UMD 全局框架模块名 → 仓库根 shims 转发层(唯一落点;业务代码禁写这些 import)。
+      // 路径基:import.meta.url 在 apps/teacher/vite.config.ts,仓库根为上两级(../../)。
+      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)),
+      // 共享层:@unified/shared 直指源码(monorepo 源码消费,免构建)
+      '@unified/shared': fileURLToPath(new URL('../../packages/shared/src', import.meta.url)),
+      // 本端业务层路径别名(@ → 本 app 的 src)
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
+    },
+  },
+  define: defineMap,
+  server: {
+    port: 5174,
+    proxy: {
+      // dev 联调:/api 前缀请求代理到后端 dev 地址(可用环境变量覆盖)
+      '/api': {
+        target: process.env.VITE_API_PROXY_TARGET ?? 'http://127.0.0.1:8080',
+        changeOrigin: true,
+      },
+    },
+  },
+  build: {
+    // 质量门禁:主 chunk 上限(README §质量门禁);UMD 框架误打包时体积会显著超限
+    //(注意:Vant 经 npm 编译进 bundle 属预期,体积判据需结合 Vant 基础体量,非"零框架"判据)
+    chunkSizeWarningLimit: 1024,
+  },
+}));

+ 83 - 0
eslint.config.js

@@ -0,0 +1,83 @@
+// ESLint 9 flat config(monorepo 根,遍历 packages/ + apps/):vue3 + TS strict 配套。
+//
+// 与 PC 端(单项目)差异:
+//   ① ignores 补 monorepo 路径(apps/*/dist、packages/shared/dist、各 node_modules、vendor/);
+//   ② 禁值 import 清单**只含 UMD 全局框架**(vue/vue-router/pinia/axios)——
+//      Vant 4 走 npm 编译(官方无 UMD 构建,混合方案),`import Vant from 'vant'` 仅允许
+//      出现在 apps/*/src/main.ts 的 app.use(Vant) 装配点,不在本清单限内。
+import js from '@eslint/js';
+import tseslint from 'typescript-eslint';
+import pluginVue from 'eslint-plugin-vue';
+import prettier from 'eslint-config-prettier';
+
+/**
+ * 框架运行时模块名(UMD 全局框架;业务代码禁"值 import",见 README 编码规范)。
+ *
+ * 精确边界(同 PC 端):
+ *   - `import type { Ref } from 'vue'` 属**类型导入**(编译期擦除、零运行时足迹)——放行;
+ *   - `import { ref } from 'vue'` 属**值导入**——会把框架运行时打包进 dist,必须拦。
+ *
+ * 实现用 `no-restricted-syntax`(importKind='value' 只匹配值导入,放行 type 导入)。
+ */
+const FRAMEWORK_MODULE_SELECTOR =
+  ":is([source.value='vue'],[source.value='vue-router'],[source.value='pinia'],[source.value='axios'])";
+
+export default [
+  js.configs.recommended,
+  ...tseslint.configs.recommended,
+  ...pluginVue.configs['flat/essential'],
+  prettier,
+  {
+    // shims/ 由 scripts/gen-shims.cjs 生成(含逐行 eslint-disable 注释);
+    // scripts/ 是 Node CJS 工具脚本;vendor/ 是 UMD 源文件——均不 lint。
+    ignores: [
+      'dist/**',
+      'vendor/**',
+      'node_modules/**',
+      'shims/**',
+      'scripts/**',
+      'packages/*/dist/**',
+      'apps/*/dist/**',
+      '**/node_modules/**',
+      '**/*.cjs',
+    ],
+  },
+  {
+    // .vue:SFC 内 <script lang="ts"> 交给 @typescript-eslint/parser,
+    // 否则 TS 语法(类型注解 / interface / 泛型)在 SFC 内触发 Parsing error。
+    files: ['**/*.vue'],
+    languageOptions: {
+      parserOptions: {
+        parser: tseslint.parser,
+        sourceType: 'module',
+      },
+    },
+  },
+  {
+    // TS 项目里 no-undef 由 TS 编译器负责(global.d.ts 声明的裸标识符对 JS 规则不可见,会误报)。
+    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 UMD 全局框架"(值导入会把框架运行时打包进 dist)。
+      // 放行 import type(编译期擦除)。
+      'no-restricted-syntax': [
+        'error',
+        {
+          selector: `ImportDeclaration[importKind='value']${FRAMEWORK_MODULE_SELECTOR}`,
+          message:
+            'UMD 全局框架经 index.html 加载(vendor/ 自托管),业务代码禁止值 import;仅允许 `import type`(编译期擦除,零运行时足迹)。',
+        },
+      ],
+    },
+  },
+];

+ 93 - 0
global.d.ts

@@ -0,0 +1,93 @@
+/**
+ * global.d.ts —— UMD 全局变量 + vite define 裸标识符的 TypeScript 声明(移动端混合机制类型侧配套)。
+ *
+ * 背景(移动端混合方案,README §1.x):
+ *   - **Vue 系走 UMD 全局**:Vue / VueDemi / VueRouter / Pinia / axios 由 index.html 按序引入
+ *     (vendor/ 自托管,同 PC 端纪律),业务代码不 import 这些框架模块;
+ *   - **Vant 走 npm 编译**:Vant 4 官方无 UMD 构建(仅 es/lib),只能 `import Vant from 'vant'`
+ *     编译进 bundle,由 vant 包自带 TS 类型覆盖——故本文件**不声明 Vant 全局**(与 PC 端 ElementPlus
+ *     走 UMD 全局 + 此文件声明的差异点)。
+ *
+ * 落点:本文件放**仓库根**(monorepo 唯一类型检查入口 tsconfig.json 的 include 覆盖它 + shims/ +
+ *   packages/shared/src + apps/{端}/src)。
+ * 做两件事:
+ *   ① 声明 5 个 UMD 全局(Vue/VueDemi/VueRouter/Pinia/axios,typeof 引自同版本 dev 依赖);
+ *   ② 声明 vite define 注入的组合式 API 裸标识符(ref / computed / onMounted …),
+ *      使业务代码"裸写 ref()"既过类型检查、又运行时命中 Vue 全局。
+ *
+ * 版本纪律:dev 依赖版本必须与 index.html 头部"vendor 版本表"一致(升级走框架升级流程)。
+ */
+import type { AxiosStatic } from 'axios';
+import type * as PiniaNS from 'pinia';
+import type * as VueNS from 'vue';
+import type * as VueRouterNS from 'vue-router';
+
+declare global {
+  // —— UMD 全局常量(index.html 按序注入;vendor/ 自托管)——
+  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;
+  /** 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 {};

+ 32 - 0
package.json

@@ -0,0 +1,32 @@
+{
+  "name": "unified-mobile-frontend",
+  "version": "0.1.0",
+  "private": true,
+  "description": "统一 Go 后端移动端(老师/学生/家长)——pnpm monorepo 按角色分包,框架层 UMD 自托管 OSS,业务层本地编译",
+  "type": "module",
+  "scripts": {
+    "dev:teacher": "pnpm --filter unified-teacher dev",
+    "build": "pnpm -r build",
+    "typecheck": "vue-tsc --noEmit -p tsconfig.json && vue-tsc --noEmit -p apps/teacher/tsconfig.json",
+    "lint": "eslint ."
+  },
+  "packageManager": "pnpm@10.12.1+sha1.4bd977ce548dc9fcd1ca0c62838c55646cea6847",
+  "devDependencies": {
+    "@eslint/js": "^9.39.4",
+    "axios": "1.21.0",
+    "dayjs": "1.11.23",
+    "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",
+    "vant": "4.10.2",
+    "vite": "^6.4.5",
+    "vue": "3.5.45",
+    "vue-demi": "0.14.10",
+    "vue-router": "4.6.4",
+    "vue-tsc": "^3.3.14"
+  }
+}

+ 17 - 0
packages/shared/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@unified/shared",
+  "version": "0.1.0",
+  "private": true,
+  "description": "移动端三端(teacher/student/parent)共享层:SSO/登录态、接口封装、通用组件、契约类型、可组合逻辑(源码消费,免构建)",
+  "type": "module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "devDependencies": {
+    "axios": "1.21.0",
+    "dayjs": "1.11.23",
+    "pinia": "2.3.1",
+    "vant": "4.10.2",
+    "vue": "3.5.45",
+    "vue-router": "4.6.4"
+  }
+}

+ 87 - 0
packages/shared/src/api/auth/login.ts

@@ -0,0 +1,87 @@
+/**
+ * login.ts —— 登录/登出接口封装(auth 域;对应后端 identity 模块 ADR-014 SSO 最小可行)。
+ *
+ * 一域一目录、一资源一文件纪律:本文件只导出登录相关请求函数 + 对应 DTO。
+ * 三端共用一份(对齐"底层用户统一"——同一 canonical_user_id 登录,角色差异由 permissions/tabTree 区分)。
+ *
+ * 典型使用(shared/store/login.ts 内):
+ *
+ *   import { login } from '@unified/shared/api/auth/login';
+ *   const res = await login({ username, password });
+ *   if (res.code !== 200) return; // 错误文案 res.message 已由 http 层 toast
+ *   loginStore.applyLogin(res.data);
+ */
+import { http } from '../http';
+import type { MenuNode, 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[];
+  /**
+   * 可见底部导航树(后端按角色/权限过滤后下发;tabbar-menu 组件渲染,与 PC 端 menuTree 同构契约)。
+   * 移动端作 tabTree 消费(van-tabbar 各 tab),字段含义见 types/api.ts MenuNode。
+   */
+  tabTree: MenuNode[];
+}
+
+/**
+ * 登录(POST /api/auth/login)。
+ *
+ * 入参:
+ *   - req LoginReq:username/password 必填;字段缺失由前端表单 rules 拦截,
+ *     后端兜底 2001(参数缺失)。
+ *
+ * 出参:
+ *   - Promise<R<LoginResult>>:
+ *     - code=200:data 为凭证;调用方写入 login store;
+ *     - 账号密码错误:1005 段(后端定义),展示 message 即可;
+ *     - 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);
+}

+ 80 - 0
packages/shared/src/api/auth/wechat.ts

@@ -0,0 +1,80 @@
+/**
+ * wechat.ts —— 微信 per-campus 授权接口封装(auth 域;家长端主入口,README §鉴权与微信授权)。
+ *
+ * ⚠️ 占位说明(2026-11-06):后端微信授权端点**尚未就绪**。
+ *   本文件的接口路径 / 字段为**占位**(按微信 per-campus 授权通用流程假设),联调前必须与后端核对:
+ *     - POST /api/auth/wechat/authorize  发起授权(按 campusId 取对应 appid 的微信授权 URL + 本地 state)
+ *     - GET  /api/auth/wechat/status      轮询/回调后的授权状态(pending/scanning/success/cancel/expire)
+ *   微信 per-campus:campus → appid 映射由后端下发(一校区一公众号/小程序),前端只传 campusId,
+ *   不接触 appid/secret(密钥侧全在后端)。
+ *   后端就绪后按真实契约修订本文件字段名与路径(保持 camelCase,与后端 DTO 逐字段对齐)。
+ *
+ * 典型使用(apps/{端}/src/views/login 微信 tab,或家长端入口自动授权):
+ *
+ *   // 家长端入口:按作用域 campus 发起授权
+ *   const auth = await authorizeWechat(campusId);
+ *   window.location.href = auth.url; // 跳转微信授权页(携带 state)
+ *   // 授权回跳后的落地页:轮询状态直到 success 拿凭证
+ *   const timer = setInterval(async () => {
+ *     const s = await pollWechatStatus(auth.state);
+ *     if (s.status === 'success' && s.token) { loginStore.applyLogin(s.token); clearInterval(timer); }
+ *     if (s.status === 'expire' || s.status === 'cancel') { clearInterval(timer); }
+ *   }, 1500);
+ */
+import { http } from '../http';
+import type { R } from '../../types/api';
+import type { LoginResult } from './login';
+
+/** 授权状态机(前端轮询得到的状态;与后端枚举对齐,后端未定前为占位) */
+export type WechatAuthStatus = 'pending' | 'scanning' | 'success' | 'cancel' | 'expire';
+
+/** 发起授权返回体(占位 DTO,后端就绪后对齐) */
+export interface WechatAuthorizeResult {
+  /** 微信授权 URL(含 appid + state;前端整页跳转过去,用户授权后微信回调后端) */
+  url: string;
+  /** 本地 state 标识(轮询时回传,后端据此关联授权会话,防 CSRF) */
+  state: string;
+  /** 授权会话有效期(秒;前端据此提示"已过期请重试") */
+  expireSeconds: number;
+}
+
+/** 轮询授权状态返回体(占位 DTO;success 时携带登录凭证,结构同账号密码的 LoginResult) */
+export interface WechatStatusResult {
+  /** 当前授权状态 */
+  status: WechatAuthStatus;
+  /** 登录凭证(仅 status === 'success' 时非空;其余状态省略) */
+  token?: LoginResult;
+}
+
+/**
+ * 发起微信 per-campus 授权(POST /api/auth/wechat/authorize)。
+ *
+ * 入参:
+ *   - campusId number:当前作用域校区 id(后端据此查 appid 映射;0 = 机构级,后端取默认 appid)。
+ * 出参:
+ *   - Promise<R<WechatAuthorizeResult>>:code=200 时 data 为授权 URL + state;失败展示 message。
+ * 异常场景:
+ *   - 后端未就绪(404/501):按占位语义,调用方 catch 提示"微信授权暂未开放";
+ *   - 该 campus 无 appid 映射(4xxx 段):展示后端 message(如"该校区未配置微信入口")。
+ */
+export function authorizeWechat(campusId: number): Promise<R<WechatAuthorizeResult>> {
+  // TODO(后端就绪后):核对路径、入参名与返回结构
+  return http
+    .post<R<WechatAuthorizeResult>>('/api/auth/wechat/authorize', { campusId })
+    .then((r) => r.data);
+}
+
+/**
+ * 轮询微信授权状态(GET /api/auth/wechat/status?state=xxx)。
+ *
+ * 入参:
+ *   - state string:authorizeWechat 返回的本地 state 标识。
+ * 出参:
+ *   - Promise<R<WechatStatusResult>>:code=200 时 data.status 为状态机值;success 时 data.token 非空。
+ * 异常场景:
+ *   - 网络故障:Promise reject,调用方 catch(轮询里静默跳过本次,下轮重试)。
+ */
+export function pollWechatStatus(state: string): Promise<R<WechatStatusResult>> {
+  // TODO(后端就绪后):核对路径、query 参数名与状态枚举
+  return http.get<R<WechatStatusResult>>('/api/auth/wechat/status', { params: { state } }).then((r) => r.data);
+}

+ 219 - 0
packages/shared/src/api/http.ts

@@ -0,0 +1,219 @@
+/**
+ * http.ts —— axios 实例 + 统一信封处理 + 401 refresh 单例重放(编码规范 §接口契约)。
+ *
+ * 契约纪律(与后端 response 包一一对应,三端共用一份,对齐"底层用户统一"):
+ *   - HTTP 恒 200,判成功只认 body `code === 200`;业务错误**不** reject(Promise 正常 resolve),
+ *     由调用方读 `res.code / res.message` 决策——杜绝"拦截器 reject 吞掉业务错误码"的旧系统坑;
+ *   - 仅**网络层**故障(断网/超时/CORS)才 reject,catch 里统一提示"网络连接异常";
+ *   - 401 场景(code 1001/1002):refresh 单例(并发多个 1001 只刷一次,Promise 复用),
+ *     刷新成功重放原请求;刷新失败 → 清登录态 → 跳登录页。
+ *
+ * 典型使用(shared/api/{域}/{资源}.ts 内):
+ *
+ *   import { http } from '@unified/shared/api/http';
+ *   export function listXxx(params: PageQuery) {
+ *     return http.get<R<PageData<XxxDTO>>>('/api/xxx', { params }).then((r) => r.data);
+ *   }
+ *
+ * 装配(apps/{端}/src/main.ts,须在首帧路由解析前):
+ *
+ *   import { setRouterForRedirect } from '@unified/shared/api/http';
+ *   const router = buildRouter();
+ *   setRouterForRedirect(router); // 注入路由实例,401 跳登录走 SPA push(免全页刷新)
+ */
+import { clearTokens, getAccessToken, getRefreshToken, setTokens } from '../identity/token-store';
+import type { R } from '../types/api';
+import type { Router } from 'vue-router';
+
+/** 登录态失效相关错误码(与后端 errors 包 1xxx 段对齐) */
+const UNAUTHORIZED_CODES: readonly number[] = [1001, 1002];
+
+/**
+ * 路由跳转收口(SPA router.push,避免 location.href 全页刷新)。
+ *
+ * 为何用注入而非 import:本文件被各端 router 的守卫链间接依赖(守卫调 refreshAccessToken),
+ * 若此处 `import router` 会形成 router → http → router 循环。故用模块级变量,由各端 main.ts
+ * 装配后调 setRouterForRedirect 注入;未注入时(极端早启动)降级 location.href。
+ */
+let routerRef: Router | undefined;
+
+/** 注入路由实例(main.ts 装配 router 后调用;幂等)。入参:路由实例。出参:无。 */
+export function setRouterForRedirect(router: Router): void {
+  routerRef = router;
+}
+
+/** SPA 跳登录页(router.push;未注入 router 时降级 location.href)。入参:路径。出参:无。 */
+function spaPush(path: string): void {
+  if (routerRef) {
+    routerRef.push(path).catch(() => {
+      /* push 重复导航等异常:静默(目标页已到位或将被守卫重定向) */
+    });
+  } else {
+    window.location.href = path;
+  }
+}
+
+/** 错误 toast 去重表:同一 message 1s 内不重复弹(防刷屏) */
+const toastDedup = new Map<string, number>();
+const TOAST_DEDUP_MS = 1000;
+
+/**
+ * 业务错误 toast 渲染器(由各端 main.ts 装配时注入,默认降级 console.warn)。
+ *
+ * 为何注入而非在 http 层 import Vant:http 是底层基础设施(被 store/router/守卫间接依赖),
+ * 应保持**零 UI 依赖**(Vant 命令式 Message 在 main.ts 已 `app.use(Vant)` 后可用,
+ * 由 main.ts 把"弹 toast"这个 UI 行为注入进来,方向是 UI → 基础,而非基础 → UI)。
+ * 与 PC 端 ElementPlus 直接可用(UMD 全局)不同——Vant 走 npm 编译,经注入解耦。
+ */
+let toastHandler: ((message: string) => void) | undefined;
+
+/**
+ * 注册业务错误 toast 渲染器(apps/main.ts 装配 Vant 后调用;幂等)。
+ *
+ * 入参:
+ *   - fn (message: string) => void:渲染函数(如 (m) => Vant.Message.error(m))。
+ * 出参:无。
+ * 异常场景:
+ *   - 重复注册:后注册的覆盖前者。
+ */
+export function setBusinessErrorToaster(fn: (message: string) => void): void {
+  toastHandler = fn;
+}
+
+/**
+ * 弹业务错误提示(1s 内同文案去重)。
+ *
+ * 入参:
+ *   - message string:后端 message(已脱敏);空文案时兜底"系统繁忙"。
+ *
+ * 出参:无。
+ * 异常场景:
+ *   - 未注入 toastHandler(极端早启动 / main.ts 未装配):降级 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);
+  if (toastHandler) toastHandler(text);
+  else console.warn('[biz-error]', text);
+}
+
+/** 并发 401 共享的刷新 Promise(单例核心;未发起时 undefined) */
+let refreshing: Promise<boolean> | undefined;
+
+/**
+ * 跳登录页并清空登录态(刷新失败 / 401 不可恢复时调用)。
+ *
+ * 入参:无。
+ *
+ * 出参:无。
+ * 异常场景:
+ *   - 页面正在跳转中的重复调用:location 赋值幂等。
+ */
+function redirectToLogin(): void {
+  clearTokens();
+  spaPush('/login');
+}
+
+/**
+ * 刷新 access token(单例:并发 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:错误文案直显后端 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);
+  },
+);

+ 95 - 0
packages/shared/src/components/list-page.vue

@@ -0,0 +1,95 @@
+<script setup lang="ts" generic="T extends object">
+/**
+ * list-page.vue —— 移动端通用列表页(Vue 3.5 泛型 SFC:Vant van-pull-refresh 下拉 + van-list 上拉加载)。
+ *
+ * 泛型(Vue 3.5 特性):T 为行数据类型(各域 DTO),由页面传入的 rows 自动推断,
+ * #default 插槽作用域的 row 强类型为 T——页面模板内 row.name / row.status 无需 any 断言。
+ *
+ * 职责边界:
+ *   - 只负责**渲染 + 分页交互透传**:接收 useListPage 的状态(rows/total/loading/error),
+ *     触底 emit('load')、下拉 emit('refresh')——**不持有业务数据、不发请求**(数据收口在 useListPage);
+ *   - 三态齐全:loading(van-list 骨架)/ 空态(van-empty)/ 错误态(后端 message 直显)。
+ *
+ * 与 PC 端 ListPage 的差异:PC 用 el-table + el-pagination(翻页器);移动端用
+ *   van-list 触底加载(无限滚动)+ van-pull-refresh 下拉刷新,无翻页器(移动端惯例)。
+ *
+ * 典型使用(apps/teacher/src/views/org/institution/list.vue):
+ *
+ *   const lp = useListPage<InstitutionDTO>({ fetcher: (...) => listInstitutions(...) });
+ *   <list-page :rows="lp.rows" :total="lp.total" :loading="lp.loading" :error="lp.error"
+ *              @load="onLoad" @refresh="onRefresh">
+ *     <template #default="{ row }">
+ *       <van-cell :title="row.name" :label="row.code" />  ← row 强类型 = InstitutionDTO
+ *     </template>
+ *   </list-page>
+ */
+interface Props {
+  /** 当前页数据(useListPage.rows;请求失败时兜底 [];类型 T 由调用方 rows 推断) */
+  rows: T[];
+  /** 总记录数(useListPage.total;"共 N 条") */
+  total: number;
+  /** 加载中(useListPage.loading;驱动 van-list 骨架) */
+  loading?: boolean;
+  /** 错误文案(useListPage.error;非空时展示错误态) */
+  error?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  loading: false,
+  error: '',
+});
+
+/** 触底 / 下拉事件(父级据此驱动 useListPage:load=下一页,refresh=重置第一页) */
+const emit = defineEmits<{
+  /** van-list 触底(无更多数据时不会触发,由 finished 拦截) */
+  load: [];
+  /** 下拉刷新(van-pull-refresh 释放时) */
+  refresh: [];
+}>();
+
+/** 是否已无更多数据(rows 已达 total;驱动 van-list finished,停止触底加载) */
+const finished = computed(() => props.rows.length >= props.total && props.total > 0);
+
+/** 下拉刷新本地状态(van-pull-refresh v-model;刷新完成或错误后收回) */
+const refreshing = ref(false);
+
+/** 监听外部 loading 变化:加载结束(true→false)时收回下拉态 */
+watch(
+  () => props.loading,
+  (v) => {
+    if (!v) refreshing.value = false;
+  },
+);
+
+/** 触底:透传给父级(父级 page++ 后 fetchList;若已 finished 则 van-list 不触发此回调) */
+function onLoad(): void {
+  emit('load');
+}
+
+/** 下拉刷新:置 refreshing + 透传(父级 page=1 后 fetchList;loading 结束由 watch 收回) */
+function onRefresh(): void {
+  refreshing.value = true;
+  emit('refresh');
+}
+</script>
+
+<template>
+  <van-pull-refresh v-model="refreshing" @refresh="onRefresh" class="list-page">
+    <!-- 错误态:后端 message 直显(三态铁律:不二次包装文案) -->
+    <van-notice-bar v-if="error" color="#ee0a24" background="#fff1f0" :text="`加载失败:${error},下拉重试`" />
+
+    <!-- 空态(无错误且无数据) -->
+    <van-empty v-else-if="!loading && rows.length === 0" description="暂无数据" />
+
+    <!-- 列表(van-list 触底加载;finished 时停止;#default 插槽 row 强类型 = T) -->
+    <van-list v-else :loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
+      <slot v-for="(row, index) in rows" :key="index" :row="row" :index="index" />
+    </van-list>
+  </van-pull-refresh>
+</template>
+
+<style scoped>
+.list-page {
+  min-height: 100%;
+}
+</style>

+ 60 - 0
packages/shared/src/components/tabbar-menu.vue

@@ -0,0 +1,60 @@
+<script setup lang="ts">
+/**
+ * tabbar-menu.vue —— 移动端底部导航(递归渲染 login store 下发的 tabTree,对齐 PC side-menu 纪律)。
+ *
+ * 职责边界:
+ *   - 只负责**渲染**底部 Tabbar(van-tabbar + van-tabbar-item),不持有导航数据
+ *     (数据源 = useLogin().tabTree,由父级 layout 壳传入);
+ *   - 顶层节点(有 path)渲染为 tab,点击经 router.push 跳转;高亮由当前路由 path 驱动
+ *     (:model-value 绑 route.path,对齐 PC 的 :default-active="route.path");
+ *   - 权限码联动:tabTree 由后端按角色/权限过滤后下发,前端原样渲染(不在前端二次过滤,
+ *     避免"权限码 → 导航"映射双份漂移);按钮级权限另由 v-perm 指令兜底。
+ *
+ * 与 PC side-menu 的差异:
+ *   - PC 用 el-sub-menu 递归多层侧边栏;移动端底部 Tabbar 惯例为**一层**(顶层 tab),
+ *     故本组件只渲染**顶层**节点为 tab(有 children 的纯分组节点不渲染为 tab——tab 必须可跳转,
+ *     这是 UI 形态约束而非权限过滤);子页由各 tab 对应的路由页面内部承载。
+ *   - 图标:Vant 图标随 Vant CSS 加载(van-icon),`MenuNode.icon` 提供即渲染,缺省纯文本。
+ *
+ * 典型使用(apps/teacher/src/views/layout/index.vue 的底部):
+ *
+ *   <tabbar-menu :tab-tree="login.tabTree" />
+ */
+import type { MenuNode } from '../types/api';
+
+defineProps<{
+  /** 可见底部导航树(后端按角色/权限过滤后下发;取顶层节点渲染为 tab) */
+  tabTree: MenuNode[];
+}>();
+
+const route = useRoute();
+const router = useRouter();
+
+/** 当前激活 tab = 路由 path(驱动 van-tabbar 高亮,对齐 PC 的 :default-active="route.path") */
+const active = computed(() => route.path);
+
+/** tab 点击:经 router.push 跳转(SPA,免全页刷新) */
+function onTabClick(name: string | number): void {
+  if (typeof name === 'string' && name) router.push(name);
+}
+</script>
+
+<template>
+  <van-tabbar :model-value="active" @change="onTabClick">
+    <van-tabbar-item
+      v-for="item in tabTree"
+      :key="item.code"
+      v-show="item.path"
+      :name="item.path"
+    >
+      <template #icon>
+        <van-icon v-if="item.icon" :name="item.icon" />
+      </template>
+      <template #title>{{ item.label }}</template>
+    </van-tabbar-item>
+  </van-tabbar>
+</template>
+
+<style scoped>
+/* tabbar 固定底部(van-tabbar 默认 fixed);内容区需为 tabbar 留 padding-bottom(见 layout 壳) */
+</style>

+ 123 - 0
packages/shared/src/composables/use-list-page.ts

@@ -0,0 +1,123 @@
+/**
+ * use-list-page.ts —— 列表页统一规范 composable(编码规范"列表页一律复用 useListPage")。
+ *
+ * 收口内容(页面不得自写以下状态/逻辑):
+ *   - 分页五元组(page / pageSize / total / rows / loading);
+ *   - **统一分页解构**:fetcher 返回 PageR<T>,本函数统一解 `res.data.{list,total,page,pageSize,pages}`
+ *     (统一分页契约——消灭旧系统"两套读法"的坑);
+ *   - 页码/每页条数变更后的自动重载(watch → fetchList)。
+ * 三端共用一份。
+ *
+ * 作用域纪律(编码规范):fetcher 内必须把 `useScope().toQuery()` 并入请求参数
+ * (institutionId/campusId 只从 scope 取,页面禁自拼数值)——本 composable 不代劳,
+ * 因为各域接口的参数结构不同,由 fetcher 就近组装最清晰。
+ *
+ * 典型使用(apps/teacher/src/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());
+ */
+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 记"网络连接异常"(三态铁律)。
+ */
+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 取当前响应式值;作用域由 fetcher 并入)。
+   *
+   * 出参:
+   *   - 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) {
+        // 统一解构: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
packages/shared/src/directives/perm.ts

@@ -0,0 +1,63 @@
+/**
+ * perm.ts —— 按钮级权限指令 v-perm(编码规范:权限码三段式 + 禁 role==N)。
+ *
+ * 用法(模板):
+ *
+ *   <van-button v-perm="'academic.checkin.view'">查看</van-button>
+ *
+ * 行为:权限码缺失时**直接移除元素**(不是 disabled)——避免"看得见点不动"的误导;
+ * 未登录/权限集为空时全部移除(fail-closed)。三端共用一份。
+ *
+ * 典型使用(注册入口,见 apps/{端}/src/main.ts):
+ *
+ *   import { perm } from '@unified/shared/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
packages/shared/src/identity/token-store.ts

@@ -0,0 +1,110 @@
+/**
+ * token-store.ts —— token 的**唯一物理存取点**(编码规范"唯一 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') 之类的散写;
+ *   - 登出/刷新失败必须调用 clearTokens(),不得只清一半。
+ *
+ * 典型使用(apps/{端}/src/api 或 shared/api/http.ts 请求拦截器):
+ *
+ *   import { setTokens, getAccessToken, clearTokens } from '@unified/shared/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);
+}

+ 19 - 0
packages/shared/src/index.ts

@@ -0,0 +1,19 @@
+/**
+ * index.ts —— @unified/shared 包入口(三端共用的命名空间桶导出)。
+ *
+ * 消费方式:apps 经 `@unified/shared/...` 子路径按需导入(如 `@unified/shared/store/login`),
+ * 也可经本桶文件整包导入(`@unified/shared`)。本文件只 re-export,不做任何运行时逻辑
+ * (避免桶文件引入副作用或循环依赖)。
+ *
+ * 注:包 main 指向本文件(源码消费,免构建);tsconfig paths 把 `@unified/shared` 直指 src,
+ * dev 免构建、类型直达。
+ */
+export * from './types/api';
+export * from './identity/token-store';
+export * from './api/http';
+export * from './api/auth/login';
+export * from './api/auth/wechat';
+export * from './store/login';
+export * from './store/scope';
+export * from './composables/use-list-page';
+export * from './directives/perm';

+ 162 - 0
packages/shared/src/store/login.ts

@@ -0,0 +1,162 @@
+/**
+ * login.ts —— 登录态 store(Pinia setup 风格;编码规范"状态最小化")。
+ *
+ * 职责边界:
+ *   - 持有**响应式视图**(token/角色/权限码/tabTree),供组件 `useLogin()` 读取渲染 tab/按钮;
+ *   - token 的**物理存取**全部经 identity/token-store(本文件与 http.ts 都不直接碰 localStorage);
+ *   - 登录/登出动作封装在此(调 api/auth/login.ts → 写 token-store → 同步视图)。
+ * 三端共用一份(对齐"底层用户统一")。
+ *
+ * 典型使用(组件内):
+ *
+ *   const login = useLogin();
+ *   v-perm="'academic.checkin.view'" 指令读 login.hasPerm;
+ *   <template>{{ login.realName }}</template>
+ *   <tabbar-menu :tab-tree="login.tabTree" />
+ */
+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';
+import type { MenuNode } from '../types/api';
+
+/**
+ * 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 权限码集合(`域.资源.动作`;菜单/按钮显隐唯一依据) */
+  permissions: Ref<Set<string>>;
+  /** 可见底部导航树(登录响应下发;tabbar-menu 渲染。刷新后未恢复前为 [],tab 降级全隐藏) */
+  tabTree: Ref<MenuNode[]>;
+  /** 是否已登录(有 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>());
+  // 可见底部导航树(仅内存态,不落 localStorage——tabTree 是响应式视图非凭证,
+  // 刷新后随 roles/permissions 一样降级为空,完整恢复依赖后续 /api/auth/me 端点(规划项))
+  const tabTree = ref<MenuNode[]>([]);
+
+  /** 已登录判定:有 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.checkin.view`)。
+   *
+   * 出参:
+   *   - 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 ?? []);
+    tabTree.value = data.tabTree ?? [];
+  }
+
+  /**
+   * 执行登出:调后端吊销 → 清本地态(无论后端是否成功,本地态必清——登出不允许卡住)。
+   *
+   * 入参:无。
+   *
+   * 出参:
+   *   - 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();
+    tabTree.value = [];
+  }
+
+  /**
+   * 从 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();
+      tabTree.value = [];
+    }
+  }
+
+  return {
+    accessToken,
+    expiresAt,
+    realName,
+    roles,
+    permissions,
+    tabTree,
+    isLoggedIn,
+    hasPerm,
+    applyLogin,
+    doLogout,
+    hydrate,
+  };
+});

+ 90 - 0
packages/shared/src/store/scope.ts

@@ -0,0 +1,90 @@
+/**
+ * scope.ts —— 多租户作用域 store(institution/campus;编码规范结构铁律 1)。
+ *
+ * 纪律(对齐后端 scope 包"强制作用域注入"):
+ *   - 所有业务页面查询参数里的 institutionId/campusId **只从 useScope() 取**(use-list-page 的 fetcher 自动注入),
+ *     页面禁止各自拼参(消灭旧系统控制器隐式 schoolid/campusid 坑);
+ *   - 作用域未就绪状态下守卫链拦截,页面不得裸查。
+ * 三端共用一份。
+ *
+ * 典型使用:
+ *
+ *   const scope = useScope();
+ *   scope.set({ institutionId: 12, campusId: 3 }); // 登录后按 token 下发初始化 / 作用域选择器提交
+ *   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,后端按作用域越界拒绝(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 };
+});

+ 88 - 0
packages/shared/src/types/api.ts

@@ -0,0 +1,88 @@
+/**
+ * api.ts —— 前后端统一契约类型(移动端 shared 层;与 PC 端同源、与后端 response 包一一对应)。
+ *
+ * 纪律(编码规范 §契约):
+ *   - 本文件是 `R<T>` / `PageData<T>` / `MenuNode` 的**唯一定义点**,三端只 import 不重定义;
+ *   - 各域 DTO 定义在 `shared/api/{域}/{资源}.ts` 内,与本文件字段**逐字段**对齐(camelCase)。
+ *
+ * 移动端差异说明:
+ *   - `MenuNode` 在 PC 端作 `login.menuTree`(侧边栏菜单树)消费;在移动端作 `login.tabTree`
+ *     (底部 Tabbar 树)消费——契约本身不变(code 权限码 + label + path + icon + 递归 children),
+ *     仅消费组件不同(PC side-menu.vue ↔ 移动 tabbar-menu.vue)。
+ */
+
+/**
+ * 统一响应信封(后端 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 统一解构对象)。
+ *
+ * 泛型参数:
+ *   - 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;
+}
+
+/**
+ * 导航菜单节点(登录响应下发;tabbar-menu 组件 + 权限联动消费的跨域契约,与 PC 端 MenuNode 同构)。
+ *
+ * 设计:
+ *   - 后端按当前用户角色/权限**过滤后**下发可见导航树,前端原样渲染(不在前端二次过滤,
+ *     避免维护"权限码 → 导航"映射的双份漂移);
+ *   - `code` 保留该节点对应的权限码(`域.资源.动作`),供按钮级 v-perm 与导航项联动复用;
+ *   - 移动端底部 Tabbar 通常为一层(叶子节点 = 各 tab),但契约保留 `children` 递归能力,
+ *     兼容"tab 内二级页"或未来改侧边导航的需求;
+ *   - 叶子节点必有 `path`(路由跳转目标);父节点 `path` 可省略(仅展开,不跳转)。
+ */
+export interface MenuNode {
+  /** 权限码(`域.资源.动作` 三段式;导航项显隐 + 按钮权限联动依据) */
+  code: string;
+  /** 导航中文名(直接渲染,不引入 i18n) */
+  label: string;
+  /** 路由 path(叶子节点必填;父节点可省略 = 仅展开) */
+  path?: string;
+  /** 图标标识(Vant 图标名或资源路径;缺省不显示图标) */
+  icon?: string;
+  /** 子菜单(递归;空数组/缺省 = 叶子节点) */
+  children?: MenuNode[];
+}

+ 2338 - 0
pnpm-lock.yaml

@@ -0,0 +1,2338 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    devDependencies:
+      '@eslint/js':
+        specifier: ^9.39.4
+        version: 9.39.5
+      axios:
+        specifier: 1.21.0
+        version: 1.21.0
+      dayjs:
+        specifier: 1.11.23
+        version: 1.11.23
+      eslint:
+        specifier: ^9.39.4
+        version: 9.39.5
+      eslint-config-prettier:
+        specifier: ^10.1.8
+        version: 10.1.8(eslint@9.39.5)
+      eslint-plugin-vue:
+        specifier: ^10.12.0
+        version: 10.12.0(@typescript-eslint/parser@8.72.1(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(vue-eslint-parser@10.4.1(eslint@9.39.5))
+      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.5)(typescript@5.8.3)
+      vant:
+        specifier: 4.10.2
+        version: 4.10.2(vue@3.5.45(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-demi:
+        specifier: 0.14.10
+        version: 0.14.10(vue@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)
+
+  apps/teacher:
+    dependencies:
+      '@unified/shared':
+        specifier: workspace:*
+        version: link:../../packages/shared
+    devDependencies:
+      '@vitejs/plugin-vue':
+        specifier: ^6.0.10
+        version: 6.0.10(vite@6.4.5)(vue@3.5.45(typescript@5.8.3))
+      vant:
+        specifier: 4.10.2
+        version: 4.10.2(vue@3.5.45(typescript@5.8.3))
+      vite:
+        specifier: ^6.4.5
+        version: 6.4.5
+
+  packages/shared:
+    devDependencies:
+      axios:
+        specifier: 1.21.0
+        version: 1.21.0
+      dayjs:
+        specifier: 1.11.23
+        version: 1.11.23
+      pinia:
+        specifier: 2.3.1
+        version: 2.3.1(typescript@5.8.3)(vue@3.5.45(typescript@5.8.3))
+      vant:
+        specifier: 4.10.2
+        version: 4.10.2(vue@3.5.45(typescript@5.8.3))
+      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))
+
+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'}
+
+  '@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.5':
+    resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==}
+    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}
+
+  '@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]
+
+  '@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==}
+
+  '@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}
+
+  '@vant/popperjs@1.3.0':
+    resolution: {integrity: sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==}
+
+  '@vant/use@1.6.0':
+    resolution: {integrity: sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==}
+    peerDependencies:
+      vue: ^3.0.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==}
+
+  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==}
+
+  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'}
+
+  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.5:
+    resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
+    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.merge@4.6.2:
+    resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+  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'}
+
+  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==}
+
+  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==}
+
+  vant@4.10.2:
+    resolution: {integrity: sha512-7lm1KtFub11ozleFqQ6rL8oa3Z8i33G3IUEuJGWBXlQwPTRfu8BYWGGZ/GhA5QdWhERp5mtBq6TjpYEmLcwwow==}
+    peerDependencies:
+      vue: ^3.0.0
+
+  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-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
+
+  '@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.5)':
+    dependencies:
+      eslint: 9.39.5
+      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.5': {}
+
+  '@eslint/object-schema@2.1.7': {}
+
+  '@eslint/plugin-kit@0.4.1':
+    dependencies:
+      '@eslint/core': 0.17.0
+      levn: 0.4.1
+
+  '@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
+
+  '@types/esrecurse@4.3.1': {}
+
+  '@types/estree@1.0.10': {}
+
+  '@types/json-schema@7.0.15': {}
+
+  '@typescript-eslint/eslint-plugin@8.72.1(@typescript-eslint/parser@8.72.1(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)':
+    dependencies:
+      '@eslint-community/regexpp': 4.12.2
+      '@typescript-eslint/parser': 8.72.1(eslint@9.39.5)(typescript@5.8.3)
+      '@typescript-eslint/scope-manager': 8.72.1
+      '@typescript-eslint/type-utils': 8.72.1(eslint@9.39.5)(typescript@5.8.3)
+      '@typescript-eslint/utils': 8.72.1(eslint@9.39.5)(typescript@5.8.3)
+      '@typescript-eslint/visitor-keys': 8.72.1
+      eslint: 9.39.5
+      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.5)(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.5
+      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.5)(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.5)(typescript@5.8.3)
+      debug: 4.4.3
+      eslint: 9.39.5
+      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.5)(typescript@5.8.3)':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+      '@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.5
+      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
+
+  '@vant/popperjs@1.3.0': {}
+
+  '@vant/use@1.6.0(vue@3.5.45(typescript@5.8.3))':
+    dependencies:
+      vue: 3.5.45(typescript@5.8.3)
+
+  '@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': {}
+
+  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: {}
+
+  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
+
+  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.5):
+    dependencies:
+      eslint: 9.39.5
+
+  eslint-plugin-vue@10.12.0(@typescript-eslint/parser@8.72.1(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(vue-eslint-parser@10.4.1(eslint@9.39.5)):
+    dependencies:
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+      eslint: 9.39.5
+      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.5)
+      xml-name-validator: 5.0.0
+    optionalDependencies:
+      '@typescript-eslint/parser': 8.72.1(eslint@9.39.5)(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.5:
+    dependencies:
+      '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+      '@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.5
+      '@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.merge@4.6.2: {}
+
+  magic-string@0.30.21:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.6.0
+
+  math-intrinsics@1.1.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: {}
+
+  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.5)(typescript@5.8.3):
+    dependencies:
+      '@typescript-eslint/eslint-plugin': 8.72.1(@typescript-eslint/parser@8.72.1(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)
+      '@typescript-eslint/parser': 8.72.1(eslint@9.39.5)(typescript@5.8.3)
+      '@typescript-eslint/typescript-estree': 8.72.1(typescript@5.8.3)
+      '@typescript-eslint/utils': 8.72.1(eslint@9.39.5)(typescript@5.8.3)
+      eslint: 9.39.5
+      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: {}
+
+  vant@4.10.2(vue@3.5.45(typescript@5.8.3)):
+    dependencies:
+      '@vant/popperjs': 1.3.0
+      '@vant/use': 1.6.0(vue@3.5.45(typescript@5.8.3))
+      '@vue/shared': 3.5.45
+      vue: 3.5.45(typescript@5.8.3)
+
+  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-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.5):
+    dependencies:
+      debug: 4.4.3
+      eslint: 9.39.5
+      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: {}

+ 5 - 0
pnpm-workspace.yaml

@@ -0,0 +1,5 @@
+# pnpm 工作区声明(README §2:packages/* + apps/*)。
+# 三端(teacher/student/parent)+ 共享层(@unified/shared)同仓,按角色分包。
+packages:
+  - 'packages/*'
+  - 'apps/*'

+ 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;

+ 23 - 0
tsconfig.base.json

@@ -0,0 +1,23 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "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
+  }
+}

+ 17 - 0
tsconfig.json

@@ -0,0 +1,17 @@
+{
+  "extends": "./tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@unified/shared": ["packages/shared/src"],
+      "@unified/shared/*": ["packages/shared/src/*"]
+    }
+  },
+  "include": [
+    "global.d.ts",
+    "packages/shared/src/**/*.ts",
+    "packages/shared/src/**/*.vue",
+    "shims/**/*.ts"
+  ],
+  "exclude": ["dist", "vendor", "node_modules", "apps/*/node_modules", "apps/*/dist"]
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1 - 0
vendor/axios@1.21.0/axios.min.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 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)
+);

Dosya farkı çok büyük olduğundan ihmal edildi
+ 5 - 0
vendor/vue-router@4.6.4/vue-router.global.prod.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 4 - 0
vendor/vue@3.5.45/vue.global.prod.js


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor