|
|
4 dni temu | |
|---|---|---|
| .gitignore | 4 dni temu | |
| README.md | 4 dni temu |
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。
| 层 | 内容 | 引入方式 | 版本管理 |
|---|---|---|---|
| 框架层 | Vue 3 / Vue Router 4 / Pinia / Element Plus(JS + CSS + zh-cn locale)/ axios,共 7 项 UMD 资源 | index.html 内 <link>/<script> 按固定顺序引入,源文件自托管到自有 OSS(自定义域名 + SSL,不用第三方 CDN) |
版本事实 = index.html 头部"vendor 版本表"(钉死次版本)+ 仓库 vendor/ 源文件(git 入库) |
| 业务层 | 本项目全部业务代码(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 配错)。index.html 版本表为准)| # | 资源 | OSS 路径模板 | 全局变量 | 用途 |
|---|---|---|---|---|
| 1 | Vue 3(prod UMD) | vue/{v}/vue.global.prod.js |
Vue |
框架本体 |
| 2 | Vue Router 4 | vue-router/{v}/vue-router.global.js |
VueRouter |
路由(按端组织 + 守卫链) |
| 3 | Pinia | pinia/{v}/pinia.iife.prod.js |
Pinia |
状态(login / scope 两 store) |
| 4 | Element Plus CSS | element-plus/{v}/index.css |
— | UI 样式(走自托管,不打包) |
| 5 | Element Plus(full UMD) | element-plus/{v}/index.full.min.js |
ElementPlus |
UI 组件(表格/表单/日期等全量) |
| 6 | Element Plus 中文 locale | element-plus/{v}/locale/zh-cn.js |
ElementPlusLocaleZhCn |
默认简体中文 |
| 7 | axios | axios/{v}/axios.min.js |
axios |
HTTP(统一封装于 src/api/) |
OSS 根路径固定:
https://<自有OSS域名>/static/vendor/<库名>/<版本号>/<文件名>/,只增不删(旧版本路径保留,回滚 = 改回 index.html 版本号)。
| 关注点 | 选型 | 理由 / 依据 |
|---|---|---|
| 构建 | Vite 6.x | 业务代码 HMR + 产物构建;框架 external 后主 chunk 只剩业务代码 |
| 语言 | TypeScript strict | 契约类型化:R<T> / PageData<T> / 各域 DTO;UMD 全局经 src/types/globals.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 强制 |
| 类型检查 | vue-tsc --noEmit |
质量门禁 0 错误 |
<html lang="zh-CN">;页面文案、placeholder、提示一律中文,默认且唯一语言。app.use(ElementPlus, { locale: ElementPlusLocaleZhCn })(命令式 ElMessageBox/Notification)+ <el-config-provider :locale="ElementPlusLocaleZhCn">(声明式),单用其一必有遗漏。vendor/ 对应目录并 commit → 3. 上传 OSS 对应路径 → 4. 改 index.html 版本表 → 5. 全量回归(vue-tsc + build + 端点联调)。
> 版本升级不再受 lockfile 保护,靠仓库 diff + 回归兜底;发布前 vendor/ 与 OSS 内容 md5 对账。.
├── index.html ← 【框架层挂载点】lang="zh-CN";7 项 vendor 资源按固定顺序引入;
│ 头部注释含"vendor 版本表"(库 → OSS 路径 → 版本,一处维护);
│ <div id="app"> + 业务入口 <script type="module" src="/src/main.ts">
├── vendor/ ← 【框架层自托管源文件】git 入库的版本事实(升级五步流程落点)
│ ├── vue@x.y/ vue.global.prod.js
│ ├── vue-router@x.y/ vue-router.global.js
│ ├── pinia@x.y/ pinia.iife.prod.js
│ ├── element-plus@x.y/ index.full.min.js + index.css + locale/zh-cn.js
│ └── axios@x.y/ axios.min.js
│ (上传 OSS 经 /static/vendor/<库名>/<版本号>/ 暴露;只增不删)
├── package.json ← 仅【业务层工具链】:vite / @vitejs/plugin-vue / vue-tsc / eslint / prettier / typescript
├── pnpm-lock.yaml ← 工具链版本锁定(入库)
├── vite.config.ts ← ① rollup external:vue/vue-router/pinia/axios → 全局变量映射(运行时只认 UMD 全局)
│ ② dev server:/api 代理 → 后端 dev 地址 ③ 产物 chunk 拆分(业务代码)
├── tsconfig.json ← strict 基线 + paths 别名(@ → src/)
├── .env.example ← 环境变量模板(VITE_API_BASE 等;实际 .env.* 走 .gitignore)
└── src/ ← 【业务层】全部业务代码(本地编译进 dist/)
├── main.ts ← 业务装配入口:取 UMD 全局 Vue.createApp + Element Plus locale 双注入
│ + Pinia + Router 挂载;不 import 框架运行时
├── types/
│ ├── globals.d.ts ← UMD 全局变量类型声明(declare global:Vue/VueRouter/Pinia/ElementPlus/axios)
│ └── api.d.ts ← 契约类型:R<T> 信封 / PageData<T>(list,total,page,pageSize,pages)/ 角色与权限码
├── views/ ← 页面层(只放视图与页面级状态;禁散写 axios)
│ ├── academic/ ← 教务域:排班/费用/打卡/充值/报课 ——【新写】(旧 jQuery 视图全部重写)
│ ├── teaching/ ← 教学 admin:备课/题库/组卷/练习分析 ——【按现有 Vue3 逐步迁入】
│ ├── org/ ← 机构/校区/班级/成员管理
│ └── system/ ← 系统:用户/角色/字典/任务监控/日志
├── components/ ← 跨页面复用组件:ListPage(表格+分页+筛选+批量收口)、作用域选择器
│ (institution·campus 联动)、GradeBadge/SubjectBadge(色板纯数据零 Vue 依赖)
├── api/ ← 接口封装(一域一目录、一资源一文件):axios 实例 + 统一信封处理 +
│ 401 refresh 单例重放 + 分页统一解构 res.data.{list,total,page,pageSize,pages}
├── router/ ← 路由表按端组织 + 守卫链:登录态 → 静默刷新 → 权限码 → 作用域就绪
├── store/ ← Pinia:login(token/角色/权限码)、scope(institution/campus)——唯一 token 存取点
├── composables/ ← 可组合逻辑:useListPage<T>(列表页唯一规范入口)等
├── utils/ ← 纯函数工具集(无 Vue 依赖):日期/金额格式化、脱敏回显、下载/导出辅助
└── styles/ ← 全局样式:variables.scss(色板/间距 token)+ reset + Element Plus 主题覆盖
目录用途约定:
| 目录 | 用途 | 约束 |
|---|---|---|
index.html |
框架层挂载 + 版本表 | 7 项 vendor 引用顺序固定;版本号只改这里,禁止散落 src |
vendor/ |
框架层源文件版本事实 | 与 OSS 内容一致(提交前 diff 核对);只增不删,禁止覆盖已发布旧版本路径 |
src/views/{域}/ |
页面层(四域分包:academic/teaching/org/system) | 只经 src/api/ 发请求;views 之间互引禁止(复用 → 上提 components/) |
src/components/ |
跨页面复用组件 | 零业务耦合(作用域感知组件除外,从 store 读) |
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 全局声明全覆盖(新增框架资源先补声明后写业务) |
| 对象 | 规则 | 示例 |
|---|---|---|
| 文件 | 组件 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 类旧系统无下划线混用命名 |
— |
组件 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/。src/ 内禁止 import Vue/VueRouter/Pinia/ElementPlus/axios;组合 API 标识符经 vite define 注入映射到 UMD 全局(或统一 const { ref } = Vue 解构,两种写法脚手架定稿时二选一),规范条文恒为"禁 import 框架"。interface 类型;可选项给默认值;禁内联 any。useListPage<T>(page/pageSize/total/loading 全在里面,组件只传 fetcher)。institutionId/campusId 只从 useScope() 取,禁止页面自拼(对齐后端"作用域强制注入",消灭旧系统隐式 schoolid 坑)。el-form + 显式 rules(提交前 validate());服务端 2001 错误 message 直接 toast,前端规则只拦格式。prop/label/width/formatter 数据驱动),新增列不改模板结构。v-perm="'域.资源.动作'" 自定义指令(读 login store 权限码集);禁止 role === N 硬编码。ElMessageBox.confirm 二次确认 + 操作后刷新 + 后端留痕。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 两个绑定(不再区分分页/非分页两套读法)。any 全面禁用(vue-tsc + ESLint no-explicit-any 双卡);未知结构用 unknown + 类型收窄。api/{域}/{资源}.ts 类型定义再写调用;禁止"先 any 后补类型"。R<T>/PageData<T> 泛型只在 types/api.d.ts 定义一次,全仓引用。| 门禁 | 命令 / 做法 | 基线 |
|---|---|---|
| 类型检查 | vue-tsc --noEmit |
0 错误 |
| 构建 | pnpm build(vite) |
0 错误;主 chunk < 1024KB;dist/ 产物 grep 不得含 vue/element-plus 运行时(防 external 配错回退成本地打包) |
| Lint | pnpm lint(eslint + prettier) |
0 错误 |
| vendor 一致性 | 发布前 vendor/ 与 OSS 路径内容 md5 对账 |
一致方可发布 |
| 端点联调 | 真实登录 token → 页面操作 → 断言 body code + 结构 | 结构不符先查后端路由重复注册 |
git push 前必须经用户确认(逐次确认)。feat: / fix: / chore: / docs: / refactor:),正文写清影响域。vue-tsc + pnpm build 通过;敏感值未入库(.env 走 .gitignore,仅 .env.example 入库);vendor 变更必须与 index.html 版本表同步提交。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 版本表 → 全量回归)。
academic/05-architecture/ 与 teaching/05-architecture/;前端策略(重写 vs 复用)见 unified/00-planning/cross-system-reconciliation.md §5。