finance和marking v3 移动前端

Q 83a83f1aef auth: 移动端对齐两步登录契约 + roles string[] + org 出参 3 dni temu
apps 83a83f1aef auth: 移动端对齐两步登录契约 + roles string[] + org 出参 3 dni temu
packages 83a83f1aef auth: 移动端对齐两步登录契约 + roles string[] + org 出参 3 dni temu
scripts 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
shims 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
vendor 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
.env.example 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
.gitignore f30f6486a7 docs: 初始化移动端前端仓库(工程约定 README + .gitignore 占位) 4 dni temu
EXAMPLES.md 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
README.md 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
eslint.config.js 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
global.d.ts 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
package.json 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
pnpm-lock.yaml 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
pnpm-workspace.yaml 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
tsconfig.base.json 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu
tsconfig.json 1aeaab47e4 feat: pnpm monorepo 脚手架 + shared 全量 + teacher 端落地(4 门禁全绿) 4 dni temu

README.md

移动端前端(老师 / 学生 / 家长)

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 构建)。

定位

一个移动端仓服务三个角色端:

端 主要场景 主要域 当前状态
老师端 打卡、查看/批改练习、备课查看 教务(打卡)+ 教学(练习/备课) ✅ 落地(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 业务层工具链(本地编译)

关注点 选型 理由 / 依据
构建 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 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/                ← 【三端共享层】源码消费(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/
    ├── 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/登录态、接口封装、通用组件、契约类型、composable、指令 唯一 token 存取与 SSO 实现点;零端差异逻辑;改 shared 必须三端 build 通过
apps/{端}/src/views 该端页面 只经 shared/api 发起请求;不实现登录(登录页除外)
apps/{端}/src/router 该端独立路由 + 权限码守卫 角色差异用权限码,不写 role == N
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 为事实源:家长端进入时按绑定关系拉取孩子数据,不本地存绑定快照。
  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 接口与响应契约

  • 统一「HTTP 恒 200 + body code」信封 {code, data, message, success};判成功读 code === 200,错误文案读 message,不靠拦截器 reject、不靠 HTTP status。
  • 错误码分段(与后端/PC 端一致):1xxx 认证/权限 · 2xxx 参数 · 3xxx 数据 · 4xxx 依赖 · 5xxx 系统;1001/1002 走 identity 统一刷新/重登流程(并发只刷一次)。
  • 统一分页:请求 page/pageSize(默认 1/10,上限 200);响应 data 恒为 PageData 对象 {list, total, page, pageSize, pages},三端统一读 res.data.list / res.data.total(useListPage<T> 收口,不再区分分页/非分页两套读法)。
  • AI 类异步结果(批改/分析):派发成功 ≠ 完成,轮询任务终态或业务表,前端展示"处理中"态而非静默。
  • 命名:入参/出参 camelCase;组件 PascalCase;组合式函数 useXxx。

3.3 鉴权与微信授权

  • 家长端主入口 = 微信 per-campus 授权(campus → appid 映射由后端下发);老师/学生端账密 + 微信绑定。
  • token 刷新单例:并发过期只触发一次 refresh(Promise 复用),成功后重放原请求。
  • 授权失败/未绑定时引导到对应端登录页,不白屏。

3.4 可复用资产与惯例

  • 三端复用模式:shared/identity(SSO 接入、token 存取/刷新/吊销)+ shared/components(list-page + tabbar-menu)+ shared/composables(useListPage)三端共用一份。
  • locale:Vant 4 默认中文,不动;如混用 Element Plus 组件需命令式 + 声明式双注入。
  • 列表页规范: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 命名规则

对象 规则 示例
文件 组件 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.9 Git 与提交规范

  • 工程纪律:本地 commit 可执行;脚手架搭建与 git push 前必须经用户确认(逐次确认)。
  • 提交信息:conventional commits(feat: / fix: / chore: / docs: / refactor:);影响 shared 的提交在正文标注 affects: student,parent,teacher。
  • 提交前自检:各端 build 通过;敏感值未入库(.env 走 .gitignore,仅 .env.example 入库);vendor 变更必须与 index.html 版本表同步提交。

4. 构建与本地运行

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(家长端微信入口)。

框架层升级:执行 §1.4 五步流程。

5. 与文档目录的关系

  • 本仓库只放移动端代码;家长绑定、费用、练习契约见工作区文档库 academic/05-architecture/ 与 teaching/05-architecture/;学生端契约兼容基线见 unified/00-planning/cross-system-reconciliation.md §5。
  • 与 PC 端仓库共享编码惯例(统一信封/统一分页 PageData/脱敏/权限码三段式/commit 规范),差异:PC 端为 UMD 自托管(8 项含 Element Plus)+ 单项目 + SCSS;移动端为 UMD 混合(5 项 Vue 系 + Vant npm)+ pnpm monorepo + plain CSS token。