EXAMPLES.md 9.9 KB

PC 前端样例索引与注释规范

本仓库 src/ 下 19 个文件全部按「三段式注释标准」编写(与后端 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 文件头模板

/**
 * <文件名> —— <一句话定位(关联 README 章节号)>。
 *
 * 职责边界:
 *   - <这条文件做什么>;
 *   - <这条文件不做什么(防越界)>。
 *
 * 典型使用(<调用方文件>):
 *
 *   <3~6 行真实调用代码,可复制到新文件直接跑>
 */

1.2 函数/方法模板

/**
 * <函数名> —— <一句话功能>。
 *
 * 入参:
 *   - <参数名> <类型>:<含义>(边界条件:如"必须 > 0")
 *   - <参数名2> <类型>:<含义>(可选,缺省 <值>)
 *
 * 出参:
 *   - <返回类型>:<含义>
 *
 * 异常场景:
 *   - <场景1>:<行为(抛错/返回空/静默)>
 *   - <场景2>:<行为>
 */

2. 样例文件索引(19 文件,按目录)

每行标注「范式亮点」——读对应文件时重点看什么。

仓库根(构建/类型侧)

文件 职责 范式亮点
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 业务装配入口 7 步装配顺序逐行注释 + failMissingGlobal 失败提示函数(异常场景示范)
App.vue 根组件(locale 声明式注入) const locale = ElementPlusLocaleZhCn 顶层解构(模板可引用)
types/api.d.ts 契约类型 R<T>/PageData<T>/PageQuery 泛型 T 的默认值与边界注释(R<undefined> 省略 data)
identity/token-store.ts token 物理存取收口 为何独立于 store(http 拦截器在 pinia 激活前可能触发)——职责边界注释范本
api/http.ts axios 实例 + 信封处理 + 401 refresh 单例 请求/响应拦截器分段注释;refreshAccessToken 单例 Promise 的并发安全注释
api/auth/login.ts 登录/登出接口 一资源一文件:导出 login/logout + LoginResult DTO 类型
api/org/institution.ts 机构列表接口(示范) fetcher 签名对齐 useListPage 的 PageR<T> 返回
store/login.ts 登录态 store(setup 风格) setup store 返回面类型(Ref/ComputedRef 字段)+ 消费时自动解包的注释
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 签名)
directives/perm.ts v-perm 权限指令 mounted/updated 双钩子 + fail-closed 行为注释
router/index.ts 路由表 + 守卫链 守卫链 4 步顺序注释 + declare module 'vue-router' 扩展 RouteMeta
views/login/index.vue 登录页(公开路由) SFC 三段式:<script setup> 内 defineProps + 响应式 + 生命周期
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

/**
 * 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

/**
 * 执行一次分页拉取。
 *
 * 入参:无(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;
  }
}

3.3 结构体(interface 字段逐项)—— 摘自 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;
}

3.4 异常场景兜底 —— 摘自 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}`);
}

4. 与后端的对照

维度 后端(backend/EXAMPLES.md) 前端(本文件)
注释语言 Go doc 风格(// FuncName 一句话) JSDoc(/** */)
三段式 功能/入参/出参 + panic 场景 功能/入参/出参 + 异常场景
异常表述 "panic" / "返回 error" "抛错" / "恒 resolve" / "静默"
契约对齐 R 信封 / PageData 分页 R<T> / PageData<T>(同一契约的前端类型化)

5. 质量门禁关联

  • 注释不被 ESLint/vue-tsc 自动校验(注释无语法错误即通过);
  • 但评审必查:签名改动后注释未同步 = 打回(README §3.7 提交前自检)。
  • shims/ 由 scripts/gen-shims.cjs 生成,不手改注释(重新生成会覆盖)。