|
|
3 일 전 | |
|---|---|---|
| scripts | 3 일 전 | |
| shims | 3 일 전 | |
| src | 3 일 전 | |
| vendor | 3 일 전 | |
| .env.example | 3 일 전 | |
| .gitignore | 3 일 전 | |
| .npmrc | 3 일 전 | |
| EXAMPLES.md | 3 일 전 | |
| README.md | 3 일 전 | |
| eslint.config.js | 3 일 전 | |
| global.d.ts | 3 일 전 | |
| index.html | 3 일 전 | |
| package.json | 3 일 전 | |
| pnpm-lock.yaml | 3 일 전 | |
| prettier.config.js | 3 일 전 | |
| tsconfig.json | 3 일 전 | |
| vite.config.ts | 3 일 전 |
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-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 配错)。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 版本号)。
| 关注点 | 选型 | 理由 / 依据 |
|---|---|---|
| 构建 | 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 强制 |
| 类型检查 | 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";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
│ (UMD 全局声明在仓库根 global.d.ts,不在此目录)
├── views/ ← 页面层(只放视图与页面级状态;禁散写 axios)
│ ├── academic/ ← 教务域:排班/费用/打卡/充值/报课 ——【新写】(旧 jQuery 视图全部重写)
│ ├── teaching/ ← 教学 admin:备课/题库/组卷/练习分析 ——【按现有 Vue3 逐步迁入】
│ ├── org/ ← 机构/校区/班级/成员管理(示范页 institution/list.vue + scope-select.vue)
│ ├── system/ ← 系统:用户/角色/字典/任务监控/日志
│ └── login/ ← 登录页(公开路由,守卫白名单)
├── components/ ← 跨页面复用组件:ListPage.vue(泛型表格+分页+工具条)、list-column.ts(列定义类型)
├── api/
│ ├── http.ts ← axios 实例 + 统一信封处理 + 401 refresh 单例重放 + 业务错误 toast 去重
│ ├── auth/login.ts ← 登录/登出接口(一资源一文件,返回类型化 DTO)
│ └── org/institution.ts ← 机构列表接口(示范:fetcher 供 useListPage 用)
├── router/ ← 路由表按端组织 + 守卫链:登录态 → 静默刷新 → 白名单 → 作用域就绪
├── 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) | 只经 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 框架(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/ 是全仓唯一允许出现框架模块名的目录,业务代码不写。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 运行时本体特征串(判据:"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 + 结构 | 结构不符先查后端路由重复注册 |
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。