# PC 前端样例索引与注释规范 > 本仓库 `src/` 下 22 个文件全部按「三段式注释标准」编写(与后端 `EXAMPLES.md` 同源),既是**可运行脚手架**,也是**编码范式参照**。 > 新建任何 `.ts` / `.vue` 文件时,对照本文件的「注释模板」与「范式摘录」落笔。 > 接口契约与角色权限见工作区文档库 `unified/`、`academic/05-architecture/`、`teaching/05-architecture/`。 ## 1. 三段式注释标准(强制) 每个**结构体(interface/type/const 对象)**与**函数/方法**必须包含三段: | 段 | 必含要素 | 写法约定 | |---|---|---| | ① 功能说明 | 用途 + 核心逻辑 + **典型使用样例**(调用代码) | 文件头块注释;函数用 JSDoc `/** */` | | ② 入参说明 | 逐项:`名称 类型:含义`(参数名用反引号) | 复杂入参分多行;简单单行 | | ③ 出参说明 | 逐项:`名称 类型:含义` + **异常场景**(何时抛错/返回空/兜底) | 明确"恒 resolve / 不 reject / 抛错"之一 | **铁律**: - 注释必须与实现**逐字一致**(改签名必改注释,CI 不卡但评审必查)。 - **禁**空泛套话("这是一个工具函数");**禁**复制粘贴不改(每个文件的"典型使用"必须真实可运行)。 - 异常场景**必写**:网络失败、空数据、并发、权限不足、未登录——至少覆盖本函数可能触达的 1 类。 - 中文书写(与项目默认简体中文一致),术语保留英文(如 `ref`/`computed`/`PageData`)。 ### 1.1 文件头模板 ```ts /** * <文件名> —— <一句话定位(关联 README 章节号)>。 * * 职责边界: * - <这条文件做什么>; * - <这条文件不做什么(防越界)>。 * * 典型使用(<调用方文件>): * * <3~6 行真实调用代码,可复制到新文件直接跑> */ ``` ### 1.2 函数/方法模板 ```ts /** * <函数名> —— <一句话功能>。 * * 入参: * - <参数名> <类型>:<含义>(边界条件:如"必须 > 0") * - <参数名2> <类型>:<含义>(可选,缺省 <值>) * * 出参: * - <返回类型>:<含义> * * 异常场景: * - <场景1>:<行为(抛错/返回空/静默)> * - <场景2>:<行为> */ ``` ## 2. 样例文件索引(22 文件,按目录) > 每行标注「范式亮点」——读对应文件时重点看什么。 ### 仓库根(构建/类型侧) | 文件 | 职责 | 范式亮点 | |---|---|---| | `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`/`PageData`/`PageQuery`/`MenuNode` | 泛型 `T` 的默认值与边界注释(`R` 省略 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` 返回 | | `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` + 出参 `ListPageState` 双接口定义范式 | | `components/ListPage.vue` | 泛型表格+分页组件 | **Vue 3.5 泛型 SFC**(`generic="T extends object"`)+ `withDefaults` 注释 | | `components/list-column.ts` | 列定义类型 `ListColumn` | 纯类型文件:字段逐项注释(`formatter` 签名) | | `components/side-menu.vue` | 侧边菜单递归组件 | **自递归**:父节点 `el-sub-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 命名约定注释 | ## 3. 范式摘录(可直接套用的真实片段) ### 3.1 文件头(三要素齐全)—— 摘自 `identity/token-store.ts` ```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}`; */ ``` ### 3.2 函数三段式(入参/出参/异常)—— 摘自 `composables/use-list-page.ts` ```ts /** * 执行一次分页拉取。 * * 入参:无(page/pageSize 取当前响应式值;作用域自动并入)。 * * 出参: * - Promise:恒 resolve(错误经 error 响应式暴露,不 reject——调用方不必 try/catch)。 * 异常场景: * - 并发点击:loading 期间重入直接忽略(防重复请求)。 */ async function fetchList(): Promise { if (loading.value) return; loading.value = true; error.value = ''; try { const res = await options.fetcher(page.value, pageSize.value); const body: PageData | 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; } } ``` ### 3.3 结构体(interface 字段逐项)—— 摘自 `store/login.ts` ```ts /** * setup store 的返回面类型(defineStore 的 setup 函数返回值;字段为 ref 本体)。 * 注:经 `useLogin()` 消费时 Pinia **自动解包**(组件里 login.accessToken 是 string | null), * 本接口仅约束 setup 函数返回,消费方以 useLogin() 推断类型为准。 */ export interface LoginStore { /** access token(响应式视图;物理值以 token-store 为准) */ accessToken: Ref; /** 过期时间(Unix 毫秒;null = 无记录,按已过期处理) */ expiresAt: Ref; /** 角色码数组(7 角色字典;未登录为 []) */ roles: Ref; /** RBAC 权限码集合(`域.资源.动作`;菜单/按钮显隐唯一依据,README §3.1) */ permissions: Ref>; /** 是否已登录(有 access token 且未过期;守卫链第一步) */ isLoggedIn: ComputedRef; /** 判断是否持有指定权限码(v-perm 指令用) */ hasPerm: (perm: string) => boolean; } ``` ### 3.4 异常场景兜底 —— 摘自 `main.ts` ```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}`); } ``` ### 3.5 模块级注入解循环依赖 —— 摘自 `api/http.ts` + `main.ts` ```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 拦截器 ``` ## 4. 与后端的对照 | 维度 | 后端(`backend/EXAMPLES.md`) | 前端(本文件) | |---|---|---| | 注释语言 | Go doc 风格(`// FuncName 一句话`) | JSDoc(`/** */`) | | 三段式 | 功能/入参/出参 + panic 场景 | 功能/入参/出参 + 异常场景 | | 异常表述 | "panic" / "返回 error" | "抛错" / "恒 resolve" / "静默" | | 契约对齐 | `R` 信封 / `PageData` 分页 | `R` / `PageData`(同一契约的前端类型化) | ## 5. 质量门禁关联 - 注释**不**被 ESLint/vue-tsc 自动校验(注释无语法错误即通过); - 但**评审必查**:签名改动后注释未同步 = 打回(README §3.7 提交前自检)。 - `shims/` 由 `scripts/gen-shims.cjs` 生成,**不手改注释**(重新生成会覆盖)。