本仓库
src/下 22 个文件全部按「三段式注释标准」编写(与后端EXAMPLES.md同源),既是可运行脚手架,也是编码范式参照。 新建任何.ts/.vue文件时,对照本文件的「注释模板」与「范式摘录」落笔。 接口契约与角色权限见工作区文档库unified/、academic/05-architecture/、teaching/05-architecture/。
每个结构体(interface/type/const 对象)与函数/方法必须包含三段:
| 段 | 必含要素 | 写法约定 |
|---|---|---|
| ① 功能说明 | 用途 + 核心逻辑 + 典型使用样例(调用代码) | 文件头块注释;函数用 JSDoc /** */ |
| ② 入参说明 | 逐项:名称 类型:含义(参数名用反引号) |
复杂入参分多行;简单单行 |
| ③ 出参说明 | 逐项:名称 类型:含义 + 异常场景(何时抛错/返回空/兜底) |
明确"恒 resolve / 不 reject / 抛错"之一 |
铁律:
ref/computed/PageData)。/**
* <文件名> —— <一句话定位(关联 README 章节号)>。
*
* 职责边界:
* - <这条文件做什么>;
* - <这条文件不做什么(防越界)>。
*
* 典型使用(<调用方文件>):
*
* <3~6 行真实调用代码,可复制到新文件直接跑>
*/
/**
* <函数名> —— <一句话功能>。
*
* 入参:
* - <参数名> <类型>:<含义>(边界条件:如"必须 > 0")
* - <参数名2> <类型>:<含义>(可选,缺省 <值>)
*
* 出参:
* - <返回类型>:<含义>
*
* 异常场景:
* - <场景1>:<行为(抛错/返回空/静默)>
* - <场景2>:<行为>
*/
每行标注「范式亮点」——读对应文件时重点看什么。
| 文件 | 职责 | 范式亮点 |
|---|---|---|
vite.config.ts |
define 注入 + alias→shims + dev 代理 + build 门禁 | 顶部块注释讲清"裸标识符替换"与"alias 重定向"两套机制的分工(§1.2) |
global.d.ts |
UMD 全局 + define 裸标识符 TS 声明 | declare global 内 var ElementPlus/var axios(允许属性式访问)vs const(纯引用)的差异注释 |
shims/vue.ts |
'vue' 模块名 → UMD 全局转发(161 导出) |
生成文件(scripts/gen-shims.cjs),文件头讲清"import type 取类型 + 运行时取 globalThis"双轨 |
index.html |
框架层挂载 + vendor 版本表 | 注释块含"加载顺序 + vue-demi 桥接原因 + OSS 布局"三件事 |
src/ 业务层| 文件 | 职责 | 范式亮点 |
|---|---|---|
main.ts |
业务装配入口 | 8 步装配顺序逐行注释(含 setRouterForRedirect 注入 + variables.scss 全局 token 引入)+ failMissingGlobal 失败提示函数(异常场景示范) |
App.vue |
根组件(locale 声明式注入) | const locale = ElementPlusLocaleZhCn 顶层解构(模板可引用) |
types/api.d.ts |
契约类型 R<T>/PageData<T>/PageQuery/MenuNode |
泛型 T 的默认值与边界注释(R<undefined> 省略 data);MenuNode 导航菜单树(code 权限码 + path + 递归 children,README §1.6) |
identity/token-store.ts |
token 物理存取收口 | 为何独立于 store(http 拦截器在 pinia 激活前可能触发)——职责边界注释范本 |
api/http.ts |
axios 实例 + 信封处理 + 401 refresh 单例 | 请求/响应拦截器分段注释;refreshAccessToken 单例 Promise 的并发安全注释;setRouterForRedirect 模块级注入(401 跳登录走 SPA push,解 router→http→router 循环依赖,README §1.7) |
api/auth/login.ts |
登录/登出接口 | 一资源一文件:导出 login/logout + LoginResult DTO 类型(含 menuTree 字段,登录响应下发菜单树) |
api/auth/wechat.ts |
微信扫码登录接口(占位,后端端点未就绪) | 取码 + 轮询两接口;WechatQrcodeStatus 状态机 + WechatQrcodeResult/WechatStatusResult 占位 DTO(token 复用 LoginResult,README §1.7);TODO 注释标明联调前需与后端核对路径/字段 |
api/org/institution.ts |
机构列表接口(示范) | fetcher 签名对齐 useListPage 的 PageR<T> 返回 |
store/login.ts |
登录态 store(setup 风格) | setup store 返回面类型(Ref/ComputedRef 字段)+ 消费时自动解包的注释;menuTree 仅内存态(不落 localStorage,刷新后随 roles/permissions 降级为空,README §1.6) |
store/scope.ts |
作用域 store | toQuery() 的"campus=0 省略键"边界注释 |
composables/use-list-page.ts |
列表页唯一规范入口 | 泛型 composable:入参 UseListPageOptions<T> + 出参 ListPageState<T> 双接口定义范式 |
components/ListPage.vue |
泛型表格+分页组件 | Vue 3.5 泛型 SFC(generic="T extends object")+ withDefaults 注释 |
components/list-column.ts |
列定义类型 ListColumn<T> |
纯类型文件:字段逐项注释(formatter 签名) |
components/side-menu.vue |
侧边菜单递归组件 | 自递归:父节点 el-sub-menu 内嵌 <side-menu> 递归子节点;叶子 el-menu-item index=path + el-menu router 模式点击跳转;MenuNode.icon 保留不渲染(图标包待独立决策,README §1.6) |
directives/perm.ts |
v-perm 权限指令 |
mounted/updated 双钩子 + fail-closed 行为注释 |
router/index.ts |
路由表(嵌套)+ 守卫链 | 守卫链 4 步顺序注释 + declare module 'vue-router' 扩展 RouteMeta;嵌套路由模型(layout 父路由 + 业务域子路由,README §1.6) |
views/layout/index.vue |
EP 标准布局壳(父路由 component) | EP 布局骨架 el-container>el-aside+el-header+el-main;el-menu :default-active="route.path" 自动高亮;退出 ElMessageBox.confirm 二次确认 + doLogout + router.push(README §1.6) |
views/login/index.vue |
登录页(公开路由) | SFC 三段式 + el-tabs 两 tab(账号密码 / 微信扫码);微信 tab 取码 + setInterval 轮询,onUnmounted 必清定时器防泄漏;单次轮询错误静默、终态停止(README §1.7) |
views/org/scope-select.vue |
作用域选择器 | 联动组件:从 store 读(例外于"零业务耦合"的注释说明) |
views/org/institution/list.vue |
机构列表(示范页) | setup 顶层解构 const { rows, total, page... } = useListPage(...) 范式 |
utils/format.ts |
纯函数工具集 | 零 Vue 依赖:每个函数独立 JSDoc(入参/出参/异常) |
styles/variables.scss |
全局样式 token | 色板/间距 token 命名约定注释 |
identity/token-store.ts/**
* token-store.ts —— token 物理存取收口(README §2 目录约定:唯一 localStorage 触点)。
*
* 职责边界:
* - 只存 access/refresh/expiresAt 三个键;提供 set/get/clear 原子操作;
* - **不**做登录态判定(那是 store/login 的事);**不**发请求(那是 api/http 的事)。
* 独立于 pinia 的原因:http 拦截器在 pinia 激活前就可能触发(401 refresh),
* 不能依赖 useLogin(),故 token 物理值走本模块。
*
* 典型使用(api/http.ts 请求拦截器):
*
* const t = getAccessToken();
* if (t) cfg.headers.Authorization = `Bearer ${t}`;
*/
composables/use-list-page.ts/**
* 执行一次分页拉取。
*
* 入参:无(page/pageSize 取当前响应式值;作用域自动并入)。
*
* 出参:
* - Promise<void>:恒 resolve(错误经 error 响应式暴露,不 reject——调用方不必 try/catch)。
* 异常场景:
* - 并发点击:loading 期间重入直接忽略(防重复请求)。
*/
async function fetchList(): Promise<void> {
if (loading.value) return;
loading.value = true;
error.value = '';
try {
const res = await options.fetcher(page.value, pageSize.value);
const body: PageData<T> | undefined = res.data;
if (res.code === 200 && body) {
rows.value = body.list ?? [];
total.value = body.total ?? 0;
} else {
rows.value = []; total.value = 0;
error.value = res.message || '查询失败';
}
} catch {
rows.value = []; total.value = 0;
error.value = '网络连接异常';
} finally {
loading.value = false;
}
}
store/login.ts/**
* setup store 的返回面类型(defineStore 的 setup 函数返回值;字段为 ref 本体)。
* 注:经 `useLogin()` 消费时 Pinia **自动解包**(组件里 login.accessToken 是 string | null),
* 本接口仅约束 setup 函数返回,消费方以 useLogin() 推断类型为准。
*/
export interface LoginStore {
/** access token(响应式视图;物理值以 token-store 为准) */
accessToken: Ref<string | null>;
/** 过期时间(Unix 毫秒;null = 无记录,按已过期处理) */
expiresAt: Ref<number | null>;
/** 角色码数组(7 角色字典;未登录为 []) */
roles: Ref<number[]>;
/** RBAC 权限码集合(`域.资源.动作`;菜单/按钮显隐唯一依据,README §3.1) */
permissions: Ref<Set<string>>;
/** 是否已登录(有 access token 且未过期;守卫链第一步) */
isLoggedIn: ComputedRef<boolean>;
/** 判断是否持有指定权限码(v-perm 指令用) */
hasPerm: (perm: string) => boolean;
}
main.ts/** UMD 全局缺失提示(开发期定位"vendor 没加载"用;生产 nginx 侧应监控 404) */
function failMissingGlobal(name: string): never {
document.getElementById('app')?.replaceWith(
Object.assign(document.createElement('pre'), {
style: 'padding:24px;color:#c0392b;font-size:14px',
textContent: `[框架未加载] 全局 ${name} 不存在——请检查 index.html 的 vendor 引用与 OSS/静态资源可达性。`,
}),
);
throw new Error(`missing UMD global: ${name}`);
}
api/http.ts + main.ts// http.ts:拦截器需要 router.push 跳登录,但 http 被 router 守卫间接依赖(守卫调 refreshAccessToken)。
// 若此处 `import { router } from '@/router'` 会形成 router → http → router 循环。
// 解法:模块级变量 + 由 main.ts 装配后注入,未注入时降级 location.href。
let routerRef: Router | undefined;
export function setRouterForRedirect(router: Router): void { routerRef = router; }
function spaPush(path: string): void {
if (routerRef) routerRef.push(path).catch(() => { /* 重复导航等异常静默 */ });
else window.location.href = path;
}
// main.ts:装配 router 后立即注入(须在首次路由解析前,但本注入不依赖 router 实例的解析能力)
const router = buildRouter();
setupGuards(router);
app.use(router);
setRouterForRedirect(router); // ← 注入路由实例给 http 拦截器
| 维度 | 后端(backend/EXAMPLES.md) |
前端(本文件) |
|---|---|---|
| 注释语言 | Go doc 风格(// FuncName 一句话) |
JSDoc(/** */) |
| 三段式 | 功能/入参/出参 + panic 场景 | 功能/入参/出参 + 异常场景 |
| 异常表述 | "panic" / "返回 error" | "抛错" / "恒 resolve" / "静默" |
| 契约对齐 | R 信封 / PageData 分页 |
R<T> / PageData<T>(同一契约的前端类型化) |
shims/ 由 scripts/gen-shims.cjs 生成,不手改注释(重新生成会覆盖)。