finance和marking v3 前端(PC)

Q fb84478673 feat(schedule): 批次 C 排班页操作面(课程域端点 46~56:排班规则/课表/生成/开下课/打卡/报名/欠费核销) 20 小時之前
scripts 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
shims 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
src fb84478673 feat(schedule): 批次 C 排班页操作面(课程域端点 46~56:排班规则/课表/生成/开下课/打卡/报名/欠费核销) 20 小時之前
vendor 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
.env.example 32ed17b544 feat(academic): 落地 PC 端 mock 联调(axios adapter 拦截,58 端点 + 8 异常分支) 1 天之前
.gitignore 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
.npmrc 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
EXAMPLES.md ece17bb25e feat: 底层框架落地——EP 布局壳 + 菜单权限码下发 + 账号密码/微信扫码登录 + router.push 收口 4 天之前
README.md ece17bb25e feat: 底层框架落地——EP 布局壳 + 菜单权限码下发 + 账号密码/微信扫码登录 + router.push 收口 4 天之前
eslint.config.js 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
global.d.ts 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
index.html 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
package.json b629606fb8 auth: PC 微信扫码对齐后端真实契约(qrcode 编码 + 复用两步②) 4 天之前
pnpm-lock.yaml b629606fb8 auth: PC 微信扫码对齐后端真实契约(qrcode 编码 + 复用两步②) 4 天之前
prettier.config.js 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
tsconfig.json 0681a3a046 feat: PC 前端脚手架全量落地(UMD 自托管 + define 注入 + shims 转发 + 三段式注释) 4 天之前
vite.config.ts e4148d43f2 fix(vite): 补 Vue 编译期特性开关 define(修课酬方案页 __VUE_PROD_DEVTOOLS__ 未定义致菜单点击无响应) 20 小時之前

README.md

PC 端前端(管理/运营)

Vue3 管理端,承载教务域重写 + 教学 admin 复用两块页面,统一走 nginx 入口 + 统一 Go 后端 SSO。 本文档覆盖四块:技术栈 / 目录结构与用途 / 编码规范 / 构建。接口契约与角色权限见工作区文档库 unified/、academic/05-architecture/、teaching/05-architecture/(本仓库不复制契约原文)。 框架层选型(2026-11-06 定稿):UMD 全局脚本 + 自托管 OSS + 业务层本地编译 + 默认简体中文。

定位

单一 Vue3 项目,面向登录角色中的 超管/教务/财务/老师(管理侧) 等 PC 端角色(市场角色登录入口暂禁用——不建市场端入口/路由,后续启用时再单独拍板)。学生/家长日常操作走移动端仓库 DEV-MERGE-LEGACY-MOBILE-FRONTEND-2026。

1. 技术栈

1.1 资源双层结构(核心选型)

层 内容 引入方式 版本管理
框架层 Vue 3 / vue-demi / Vue Router 4 / Pinia / Element Plus(JS + CSS + zh-cn locale)/ axios,共 8 项 UMD 资源 index.html 内 <link>/<script> 按固定顺序引入,源文件自托管到自有 OSS(自定义域名 + SSL,不用第三方 CDN) 版本事实 = index.html 头部"vendor 版本表"(钉死次版本)+ 仓库 vendor/ 源文件(git 入库)
业务层 本项目全部业务代码(TS 编写) 本地 pnpm 工具链编译(Vite/vue-tsc/ESLint/Prettier 为 dev 依赖),产物 dist/ 经 nginx 部署 pnpm-lock.yaml 入库(只锁工具链,不含框架)
  • 开发测试:pnpm dev 起 Vite 本地服务(HMR 只热更业务代码,框架层从自托管 OSS 加载);/api 代理到后端 dev 地址。
  • 构建:pnpm build 只打包业务代码 → dist/;产物禁止包含框架运行时(质量门禁 grep 抽查,防 external 配错)。

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 全局(官方构建写死 }({}, VueDemi));本文件读 globalThis.Vue 后把 Vue API 镜像到 VueDemi(Vue3 下等价 re-export Vue)。漏加载 → pinia 报 VueDemi is not defined,store 全崩。与 pinia 同升同降(2026-11-06 补入)
3 Vue Router 4 vue-router/{v}/vue-router.global.js VueRouter 路由(按端组织 + 守卫链)
4 Pinia pinia/{v}/pinia.iife.prod.js Pinia 状态(login / scope 两 store)
5 Element Plus CSS element-plus/{v}/index.css — UI 样式(走自托管,不打包)
6 Element Plus(full UMD) element-plus/{v}/index.full.min.js ElementPlus UI 组件(表格/表单/日期等全量)
7 Element Plus 中文 locale element-plus/{v}/locale/zh-cn.js ElementPlusLocaleZhCn 默认简体中文
8 axios axios/{v}/axios.min.js axios HTTP(统一封装于 src/api/)

OSS 根路径固定:https://<自有OSS域名>/static/vendor/<库名>/<版本号>/<文件名>/,只增不删(旧版本路径保留,回滚 = 改回 index.html 版本号)。

1.3 业务层工具链(本地编译)

关注点 选型 理由 / 依据
构建 Vite 6.x 业务代码 HMR + 产物构建;框架 external 后主 chunk 只剩业务代码
语言 TypeScript strict 契约类型化:R<T> / PageData<T> / 各域 DTO;UMD 全局经仓库根 global.d.ts 声明类型
UI 组件库 Element Plus 2.x(UMD 自托管) 教学 admin 现有栈,复用成本最低;zhCn locale 双注入
状态 Pinia login(token/角色/权限码)+ scope(institution/campus 作用域)
路由 Vue Router 4 按端组织 + 守卫链(登录态 → 静默刷新 → 权限码 → 作用域就绪)
HTTP axios 1.x(UMD 自托管,封装于 src/api/) 统一信封处理 + 401 refresh 单例重放;业务错误不 reject
鉴权 SSO token(access 30min + refresh 30 天,ADR-014) 统一 identity 签发;菜单/按钮按权限码显隐,禁 role == N 硬编码
包管理 pnpm 10.12.1(lockfile 入库) 本机已装;与移动端仓同栈
代码风格 ESLint 9 + Prettier 3 提交前 lint;no-explicit-any 强制
样式预处理 SCSS(sass devDep) 全局 token 集中在 src/styles/variables.scss(:root 变量);main.ts 全局 import 注入;组件 <style scoped> 引 var(--app-xxx) token,禁硬编码色值/尺寸
类型检查 vue-tsc --noEmit 质量门禁 0 错误

1.4 默认简体中文

  • <html lang="zh-CN">;页面文案、placeholder、提示一律中文,默认且唯一语言。
  • Element Plus locale 双注入(教务实战铁律):app.use(ElementPlus, { locale: ElementPlusLocaleZhCn })(命令式 ElMessageBox/Notification)+ <el-config-provider :locale="ElementPlusLocaleZhCn">(声明式),单用其一必有遗漏。
  • 不引入 vue-i18n(7 角色全中文、无多语言需求);将来真需多语言再上 i18n 框架,属另立决策。

1.5 框架升级规则(自托管五步,缺一不可)

  1. 下载新版本 UMD 文件 → 2. 覆盖仓库 vendor/ 对应目录并 commit → 3. 上传 OSS 对应路径 → 4. 改 index.html 版本表 → 5. 全量回归(vue-tsc + build + 端点联调)。 > 版本升级不再受 lockfile 保护,靠仓库 diff + 回归兜底;发布前 vendor/ 与 OSS 内容 md5 对账。

1.6 布局与导航模型(2026-11-06 定稿:EP 标准布局 + 菜单权限码下发)

  • 布局壳(src/views/layout/index.vue):EP 标准骨架 el-container > el-aside(侧边菜单) + el-container(el-header + el-main)。作为父路由 component(router/index.ts 嵌套路由),App.vue 保持裸 <router-view/> 不改——套壳与否由路由树决定(登录页/作用域选择页/404 是顶层路由不套壳;业务域是 layout 的子路由套壳)。
  • 侧边菜单(src/components/side-menu.vue,递归组件):父节点(children 非空)渲染 el-sub-menu 内嵌 <side-menu> 自递归;叶子节点 el-menu-item 的 index = path,配合 el-menu 的 router 模式点击即跳转。菜单高亮靠 :default-active="route.path"(el-menu 内部 watch route 自动同步)。
  • 菜单权限码下发:后端按当前用户角色/权限过滤后下发 menuTree(MenuNode[] 树,契约在 types/api.d.ts),前端原样渲染、不在前端二次过滤(避免维护"权限码 → 菜单"映射的双份漂移)。MenuNode.code 保留权限码(域.资源.动作),供按钮级 v-perm 与菜单项联动复用。
  • menuTree 不落 localStorage:menuTree 是响应式视图非凭证(与 roles/permissions 一致),刷新后降级空数组、菜单全隐藏,完整恢复依赖后续 /api/auth/me 端点(规划项)。
  • 图标:暂不引 @element-plus/icons-vue UMD(需新增第 9 项 vendor + 改 index.html,属独立决策);MenuNode.icon 字段保留但不渲染,菜单先纯文本呈现。

1.7 登录模型(2026-11-06 定稿:账号密码 + 微信扫码)

  • 账号密码(主通道,已就绪):POST /api/auth/login → 凭证落盘(applyLogin 写 token-store + 响应式视图)→ router.push('/org/scope') 跳机构选择页。
  • 微信扫码(副通道,占位):后端端点未就绪,src/api/auth/wechat.ts 为占位接口(路径/字段按通用扫码流程假设,联调前与后端核对):
    • POST /api/auth/wechat/qrcode 取二维码(返回 url + 本地 state + expireSeconds);
    • GET /api/auth/wechat/status?state=xxx 轮询状态(pending/scanning/success/cancel/expire);
    • success 时返回的 token 结构同 LoginResult,直接复用 applyLogin(不新增冗余登录方法)。
  • 登录页 UI(views/login/index.vue):el-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,免全页刷新)。

2. 目录结构与用途

.
├── index.html                 ← 【框架层挂载点】lang="zh-CN";8 项 vendor 资源按固定顺序引入;
│                                  头部注释含"vendor 版本表"(库 → OSS 路径 → 版本,一处维护);
│                                  <div id="app"> + 业务入口 <script type="module" src="/src/main.ts">
├── vendor/                    ← 【框架层自托管源文件】git 入库的版本事实(升级五步流程落点)
│   ├── vue@x.y/                 vue.global.prod.js
│   ├── vue-demi@x.y/            vue-demi.iife.js(pinia 桥接层,须在 vue 之后、pinia 之前加载)
│   ├── vue-router@x.y/          vue-router.global.js
│   ├── pinia@x.y/               pinia.iife.prod.js(依赖 VueDemi 全局)
│   ├── element-plus@x.y/        index.full.min.js + index.css + locale/zh-cn.js
│   └── axios@x.y/               axios.min.js
│                                  (上传 OSS 经 /static/vendor/<库名>/<版本号>/ 暴露;只增不删)
├── package.json               ← 仅【业务层工具链】:vite / @vitejs/plugin-vue / vue-tsc / eslint / prettier / typescript
├── pnpm-lock.yaml             ← 工具链版本锁定(入库)
├── vite.config.ts             ← ① define 注入:裸标识符 ref/computed/defineStore… → Vue.*/VueRouter.*/Pinia.* 全局
│                                  ② alias:'vue'/'vue-router'/'pinia' → shims/*.ts(SFC 编译器产物 import 重定向)
│                                  ③ dev server:/api 代理 → 后端 dev 地址;publicDir=vendor(dev 本地命中 vendor)
│                                  ④ build.publicDir=false(dist 不拷贝任何框架文件)+ chunkSizeWarningLimit=1024
├── shims/                     ← 【胶水层】'vue'/'vue-router'/'pinia' 模块名 → UMD 全局的唯一转发层(git 入库)
│   ├── vue.ts                 ←   由 scripts/gen-shims.cjs 生成:import type 取 ESM 命名空间类型 + 运行时值取 globalThis.Vue
│   ├── vue-router.ts          ←   同机制 → globalThis.VueRouter
│   └── pinia.ts               ←   同机制 → globalThis.Pinia(编译后只剩"取全局 + 转发",框架本体不进 dist)
├── scripts/                   ← 构建辅助脚本:gen-shims.cjs(重新生成 shims/ 转发清单,框架升级时运行)
├── tsconfig.json              ← strict 基线 + paths 别名(@ → src/);include 含仓库根 global.d.ts + src/** + shims/**
├── global.d.ts                ← UMD 全局 + define 注入裸标识符的 TS 声明(declare global;放仓库根以覆盖 shims/)
├── .env.example               ← 环境变量模板(VITE_API_BASE 等;实际 .env.* 走 .gitignore)
└── src/                       ← 【业务层】全部业务代码(本地编译进 dist/)
    ├── main.ts                ← 业务装配入口:取 UMD 全局 Vue.createApp + Element Plus locale 双注入
    │                              + Pinia + Router + v-perm 指令挂载;不 import 框架运行时
    ├── App.vue                ← 根组件:<el-config-provider :locale="ElementPlusLocaleZhCn"><router-view /></…>
    ├── identity/
    │   └── token-store.ts     ←   token 物理存取(localStorage 收口):setTokens/getAccessToken/getRefreshToken/
    │                               getExpiresAt/clearTokens;http 拦截器与 store 都经此,禁止散写 localStorage
    ├── directives/
    │   └── perm.ts            ←   v-perm 按钮级权限指令:读 login store 权限码集,缺失时 el.remove()(fail-closed)
    ├── types/
    │   └── api.d.ts           ←   契约类型:R<T> 信封 / PageData<T>(list,total,page,pageSize,pages)/ PageQuery
    │                                  / MenuNode(导航菜单树,登录响应下发,layout 壳 + 侧边菜单消费)
    │                              (UMD 全局声明在仓库根 global.d.ts,不在此目录)
    ├── views/                 ← 页面层(只放视图与页面级状态;禁散写 axios)
    │   ├── academic/          ←   教务域:排班/费用/打卡/充值/报课 ——【新写】(旧 jQuery 视图全部重写)
    │   ├── teaching/          ←   教学 admin:备课/题库/组卷/练习分析 ——【按现有 Vue3 逐步迁入】
    │   ├── org/               ←   机构/校区/班级/成员管理(示范页 institution/list.vue + scope-select.vue)
    │   ├── system/            ←   系统:用户/角色/字典/任务监控/日志
    │   ├── layout/            ←   【EP 标准布局壳】index.vue:el-aside 侧边菜单 + el-header + el-main(父路由
    │   │                          component,嵌套路由套壳;侧边菜单递归渲染 login.menuTree,README §1.6)
    │   └── login/             ←   登录页(公开路由,守卫白名单):el-tabs 两 tab 账号密码 + 微信扫码(占位)
    ├── components/            ← 跨页面复用组件:ListPage.vue(泛型表格+分页+工具条)、list-column.ts(列定义类型)、
    │                             side-menu.vue(侧边菜单递归组件,el-sub-menu 自递归 + el-menu router 模式)
    ├── api/
    │   ├── http.ts            ←   axios 实例 + 统一信封处理 + 401 refresh 单例重放 + 业务错误 toast 去重
    │   │                          + setRouterForRedirect 注入(401 跳登录走 SPA push,免全页刷新,README §1.7)
    │   ├── auth/login.ts      ←   登录/登出接口(LoginResult 含 menuTree 字段,一资源一文件,返回类型化 DTO)
    │   ├── auth/wechat.ts     ←   微信扫码登录接口(占位,后端端点未就绪:取码 + 轮询状态,README §1.7)
    │   └── org/institution.ts ←   机构列表接口(示范:fetcher 供 useListPage 用)
    ├── router/                ← 路由表嵌套组织(layout 父路由 + 业务域子路由)+ 守卫链:登录态 → 静默刷新 → 白名单 → 作用域就绪
    ├── store/                 ← Pinia:login(token/角色/权限码)、scope(institution/campus)——唯一 token 视图存取点
    ├── composables/
    │   └── use-list-page.ts   ←   useListPage<T>:列表页唯一规范入口(分页五元组 + 统一解构 res.data.{list,total})
    ├── utils/
    │   └── format.ts          ←   纯函数工具集(无 Vue 依赖):日期/金额格式化、脱敏回显、下载/导出辅助
    └── styles/
        └── variables.scss     ← 全局样式与 token:色板/间距 token + Element Plus 主题覆盖

目录用途约定:

目录 用途 约束
index.html 框架层挂载 + 版本表 8 项 vendor 引用顺序固定(vue → vue-demi → vue-router → pinia → …);版本号只改这里,禁止散落 src
vendor/ 框架层源文件版本事实 与 OSS 内容一致(提交前 diff 核对);只增不删,禁止覆盖已发布旧版本路径
shims/ 框架模块名 → UMD 全局转发胶水层 全仓唯一允许出现 vue/vue-router/pinia 模块名的目录;由 scripts/gen-shims.cjs 生成,禁手改;运行时值取自 globalThis,编译后框架本体不进 dist
scripts/ 构建辅助脚本 gen-shims.cjs(框架升级五步流程后重跑);Node CJS,不 lint、不随业务进 dist
global.d.ts UMD 全局 + define 裸标识符 TS 声明 放仓库根(覆盖 shims/ 与 src/);新增框架资源先补声明后写业务
src/views/{域}/ 页面层(四域分包:academic/teaching/org/system + layout 壳 + login) 只经 src/api/ 发请求;views 之间互引禁止(复用 → 上提 components/);layout/index.vue 是布局壳(父路由 component,非业务域,README §1.6)
src/components/ 跨页面复用组件 零业务耦合(作用域感知组件除外,从 store 读);side-menu.vue 递归渲染 login.menuTree(README §1.6)
src/api/{域}/ 接口封装(一域一目录,一资源一文件) 信封处理与分页解构只在此;返回类型化 DTO
src/router/ 路由与守卫 权限码驱动;不写 role == N 硬编码
src/store/ 全局状态(登录态/作用域) 唯一 token 存取点(禁 localStorage 散写);scope 未就绪时守卫拦截
src/composables/ 可组合逻辑 列表页一律复用 useListPage<T>
src/utils/ 纯函数工具集 文件内零组件、零 store 依赖;可被 composables/api/views 任意引用,反向禁止
src/styles/ 全局样式与 token 业务组件 <style> 禁硬编码色值/尺寸,必须引 variables token;页面级局部样式留组件内 <style scoped>
src/types/ 契约类型 R<T>/PageData<T> 只在此定义一次;UMD 全局声明全覆盖(新增框架资源先补声明后写业务)

3. 编码规范

3.1 命名规则

对象 规则 示例
文件 组件 PascalCase.vue;其余 kebab-case.ts ListPage.vue、use-list-page.ts、grade-badge.ts
组件 注册名 PascalCase,模板里 kebab-case <list-page> / ListPage
组合式函数 useXxx useListPage<T>()、useScope()
变量/函数 camelCase institutionId、handleSubmitRefund
常量 模块内 UPPER_SNAKE(仅真常量:错误码段、权限码枚举) PERM_CODE
接口/类型 名词 PascalCase,DTO 带域前缀 PageData<T>、AcademicFeeDTO、TeachingQuestionDTO
API 字段 一律 camelCase,与后端 DTO 严格对齐(institutionId 非 institution_id);禁止前端自造映射层 —
路由 path kebab-case,与后端端点一致(/{端}/{模块}/{资源}) /admin/academic/fees
路由 name PascalCase 页名 AcademicFeeList
权限码 域.资源.动作 三段式(点分) academic.fee.refund
事件/方法 内部回调 onXxx;用户触发的提交类 handleXxx onScopeChange、handleDelete
禁词 禁用 userid/schoolid/campusid 类旧系统无下划线混用命名 —

3.2 文件组织

组件 SFC 骨架(顺序固定):

<script setup lang="ts">   ← ① 类型 import(仅类型与本地 DTO,禁 import 框架运行时)
                            ← ② defineProps/defineEmits 显式类型(禁 any,unknown 收窄)
                            ← ③ store 注入(useLogin/useScope)
                            ← ④ 响应式状态(ref/computed,单一数据源)
                            ← ⑤ 生命周期(onMounted 拉数据)
                            ← ⑥ 方法(handleXxx 在前,onXxx 在后)
</script>
<template>                  ← ⑦ 结构;列表页一律 <list-page> 收口
</template>
<style scoped>              ← ⑧ 局部样式;色值/尺寸引 @/styles/variables token

域目录内约定:

  • views/{域}/{模块}/:list.vue(列表)+ detail.vue(详情)+ 页内私有子组件 {模块}-xxx.vue(私有组件不放 components/);
  • api/{域}/{资源}.ts:一资源一文件,只导出该资源的请求函数 + 对应 DTO 类型;
  • 跨域引用只允许经 api/ 与 components/。

3.3 组件编写

  1. 框架运行时全走 UMD 全局(2026-11-06 定稿:define 注入):src/ 内禁止值 import 框架(import { ref } from 'vue' 会把框架运行时打进 dist,ESLint no-restricted-syntax 拦截)。落地机制:组合式 API 裸标识符(ref/computed/defineStore…)经 vite define 编译期替换为 Vue.*/VueRouter.*/Pinia.* 全局访问(清单见 vite.config.ts 的 VUE_APIS/VUE_ROUTER_APIS/PINIA_APIS);仅允许 import type(编译期擦除、零运行时足迹,TS strict 下类型必须由此引入,如 import type { Ref } from 'vue')。SFC 模板编译器产物内部的模块名 import 经 vite.config.ts 的 alias 重定向到 shims/*.ts(运行时从 globalThis 取值转发,框架本体不进 dist)。纪律:shims/ 是全仓唯一允许出现框架模块名的目录,业务代码不写。
  2. Props:显式 interface 类型;可选项给默认值;禁内联 any。
  3. 状态最小化:能用 store 的(token/scope/权限码)不本地存;列表状态收口 useListPage<T>(page/pageSize/total/loading 全在里面,组件只传 fetcher)。
  4. 作用域纪律:业务查询参数里的 institutionId/campusId 只从 useScope() 取,禁止页面自拼(对齐后端"作用域强制注入",消灭旧系统隐式 schoolid 坑)。
  5. 表单:el-form + 显式 rules(提交前 validate());服务端 2001 错误 message 直接 toast,前端规则只拦格式。
  6. 表格:列定义数组化(prop/label/width/formatter 数据驱动),新增列不改模板结构。
  7. 权限:按钮级 v-perm="'域.资源.动作'" 自定义指令(读 login store 权限码集);禁止 role === N 硬编码。
  8. 三态:所有异步区域必须有 loading + 空态 + 错误态;错误态展示后端 message(已脱敏),不二次包装文案。
  9. 敏感操作(充值/退款/改密/删除):ElMessageBox.confirm 二次确认 + 操作后刷新 + 后端留痕。
  10. 脱敏:手机号/身份证直接展示后端已脱敏值,前端不存明文、不二次处理。

3.4 接口与响应契约(前后端统一,2026-11-05 拍板)

  • 统一「HTTP 恒 200 + body code」信封 {code, data, message, success};判成功读 code === 200,错误文案读 message,不靠拦截器 reject、不靠 HTTP status(源自教务 Go 重构在产验证)。
  • 错误码分段(与后端一致):1xxx 认证/权限 · 2xxx 参数 · 3xxx 数据 · 4xxx 依赖 · 5xxx 系统;1001/1002 统一走 refresh 单例(并发只刷一次,Promise 复用),成功后重放原请求;refresh 失败才清 login store 跳登录。错误 toast 去重:同一 message 1s 内不重复弹。
  • 统一分页:请求 page/pageSize(默认 1/10,上限 200);响应 data 恒为 PageData 对象 {list, total, page, pageSize, pages},useListPage<T> 统一解构 res.data.{list, total, page, pageSize, pages},组件层只见 rows/total 两个绑定(不再区分分页/非分页两套读法)。

3.5 类型纪律(TS strict)

  • any 全面禁用(vue-tsc + ESLint no-explicit-any 双卡);未知结构用 unknown + 类型收窄。
  • DTO 与后端字段逐字段对齐:新增接口先补 api/{域}/{资源}.ts 类型定义再写调用;禁止"先 any 后补类型"。
  • R<T>/PageData<T> 泛型只在 types/api.d.ts 定义一次,全仓引用。

3.6 质量门禁

门禁 命令 / 做法 基线
类型检查 vue-tsc --noEmit 0 错误
构建 pnpm build(vite) 0 错误;主 chunk < 1024KB;dist/ 产物 grep 不得含 Vue 运行时本体特征串(判据:"vnode"/"Teleport"/"Suspense"/createBaseVNode 等 runtime-core 内部标识——prod 压缩后仍保留、且不属于公开 API,shims 转发层不会引用。⚠️ 不可用公开 API 名如 openBlock/createVNode/Fragment 当判据:shims 层 export const openBlock = V.openBlock 压缩后必然保留属性名,属设计预期而非残留。辅助佐证:dist 主 chunk 体积(业务代码 ~10KB 量级)vs vendor Vue UMD(172KB)——框架本体物理装不进小 chunk)
Lint pnpm lint(eslint + prettier) 0 错误
vendor 一致性 发布前 vendor/ 与 OSS 路径内容 md5 对账 一致方可发布
端点联调 真实登录 token → 页面操作 → 断言 body code + 结构 结构不符先查后端路由重复注册

3.7 Git 与提交规范

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

4. 构建与本地运行

pnpm install          # 工具链依赖(pnpm 10.12.1;lockfile 入库)
pnpm dev              # 本地开发(vite dev,业务代码 HMR;框架层自 OSS 加载;/api 代理到后端 dev 地址)
pnpm build            # 产物 → dist/(仅业务代码;部署经 nginx 按 location 分发,ADR-010)

环境变量(.env.example 入库,实际 .env.* 不入库):VITE_API_BASE(后端 base,dev 走 vite proxy 时可省)。

框架层升级:执行 §1.5 五步流程(下载 → 覆盖 vendor/ 并 commit → 上传 OSS → 改 index.html 版本表 → 全量回归)。

5. 与文档目录的关系

  • 本仓库只放前端代码;接口契约、端点清单、错误码见工作区文档库 academic/05-architecture/ 与 teaching/05-architecture/;前端策略(重写 vs 复用)见 unified/00-planning/cross-system-reconciliation.md §5。
  • 与移动端仓库共享编码惯例(统一信封/统一分页 PageData/脱敏/权限码三段式/commit 规范),差异:PC 端为 UMD 自托管 + 单项目,移动端为 pnpm monorepo + Vant 4。