# 移动端前端(老师 / 学生 / 家长) > 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` / `PageData` / 各域 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`(顶部)+ 内容区 ``(中)+ ``(底部 van-tabbar)。作为**父路由 component**(嵌套路由),App.vue 保持裸 `` 不改——套壳与否由路由树决定(登录页是顶层路由不套壳;业务域是 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 / PageData / PageR / 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 版本表 +
+ /src/main.ts │ └── src/ │ ├── main.ts ← 8 步装配:取 UMD 全局 → Pinia → Router → 注入 http → Vant → v-perm → hydrate → mount │ ├── App.vue ← 裸 │ ├── 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` 收口,不再区分分页/非分页两套读法)。 - 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` + `` 泛型 SFC(van-pull-refresh + van-list + 三态);**useListPage 结果须解构到顶层**(`const { rows, total, loading, error, page, fetchList } = useListPage({...})`)——Vue 只自动解包顶层 setup 绑定,嵌套属性(`lp.rows`)不解包。 - **脱敏展示**:手机号/身份证按后端已脱敏值展示,前端不再二次处理(源头脱敏)。 ### 3.5 命名规则 | 对象 | 规则 | 示例 | |---|---|---| | 文件 | 组件 `PascalCase.vue`;其余 `kebab-case.ts` | `ListPage.vue`、`use-list-page.ts`、`list-page.vue` | | 组件 | 注册名 PascalCase,模板里 kebab-case | `` / `` | | 组合式函数 | `useXxx` | `useListPage()`、`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`。 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`/`PageData` 泛型只在 `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. 构建与本地运行 ```bash 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。