13 KiB
13 KiB
AI 开发规范(media-science-frontend)
本文档基于当前仓库的真实配置与现有代码风格整理,目标是让 AI/新同学在不“自作主张改风格/改架构”的前提下,稳定产出可合并的代码。
技术栈与关键约束
- 框架:Vue 3 + TypeScript + Vite(
type: module - 路由:
unplugin-vue-router文件路由(vue-router/auto),Hash 模式(createWebHashHistory()) - 状态:Pinia(setup store 风格)
- 图表:图表:echarts
- UI:Ant Design Vue
- 样式:UnoCSS(
virtual:uno.css),允许在模板里大量使用原子类(复杂样式例如动画,渐变等使用class声明,其余使用原子样式) - 国际化:
@intlify/unplugin-vue-i18n(src/locales/*.json) - 格式化门禁:Husky + lint-staged + Prettier(提交前会对
*.{vue,ts,tsx,js,jsx,md,css,less,json}自动prettier --write) - TypeScript 严格性:
strict: true,并开启noUnusedLocals/noUnusedParameters - 自动导入:
unplugin-auto-import(vue、pinia、@vueuse/core、router auto imports),因此代码里可能未显式 importref/computed/watch/...
项目架构(目录分层)
以 src/ 为根:
- 入口:
src/main.ts(创建 app、挂载 router/pinia/Antd、全局指令) - 根组件:
src/App.vue(全局初始化逻辑、主题/locale、监听生命周期等) - 页面:
src/pages/**(文件路由:一个目录通常对应一个页面模块;页面内可用definePage({ alias: [...] })) - 通用组件:
src/components/**(跨页面复用的 UI/布局组件,如Layout.vue) - 状态管理:
src/store/**(Pinia stores) - 接口层:
src/api/**src/api/generated/**:Orval 自动生成(接口类型与路径的唯一真源,禁止手改)src/api/client.ts:项目侧统一入口/别名层(导出客户端实例与类型工具)src/api/*.ts:业务适配层(可选:解包 Result、分页扁平化、兼容旧页面结构等)
- 工具:
src/utils/**(如useRouteUtil.ts、useLocalStorage.ts、useWebSocket.ts等) - 类型声明:
src/**/*.d.ts、src/global.d.ts、src/vite-env.d.ts
仓库根目录速览(常用)
.
├─ src/ # 应用源码
├─ public/ # 静态资源(按 Vite 约定)
├─ vite/ # Vite 插件/构建辅助(如 LocaleType)
├─ vite.config.ts # 构建与插件配置(含 proxy、自动路由、自动导入等)
├─ uno.config.ts # UnoCSS 主题/shortcuts
├─ tsconfig*.json # TS 严格配置(strict + noUnused*)
├─ .prettierrc # 代码格式标准(提交前会强制执行)
├─ .husky/pre-commit # 提交钩子:lint-staged
├─ .env* # 环境变量(通过 import.meta.env 读取)
└─ AI_DEV_GUIDE.md # 本规范
编码风格(必须遵守)
代码格式(Prettier 为准)
来自 .prettierrc:
- 单引号:
'...' - 缩进:2 空格,禁用 tab
- 行宽:100
任何“手动对齐/自定义格式化习惯”都会被提交前的 Prettier 重写,AI 不要和格式化对抗。
Vue SFC 约定
- 优先使用:
<script lang="ts" setup>(当前仓库主流写法) - 模板类名:允许 UnoCSS 原子类;尽量复用
uno.config.ts里的 shortcuts(如flex-center) - 样式块:
- 页面级样式:优先
scoped - 全局样式:放在明确的全局入口(项目当前也存在在
App.vue内写全局样式的情况;新增时优先放到统一样式文件,除非确实需要跟随根组件)
- 页面级样式:优先
TypeScript/类型策略
- 避免:
any(除非是三方库/遗留代码无法避免,且要把any限制在最小范围) - 参数与返回值:对外导出的工具函数、store action、请求函数调用处尽量补齐类型
- 未使用变量/参数:因为启用了
noUnusedLocals/noUnusedParameters,新增代码必须确保无未使用项
路由规范(文件路由 + definePage)
- 页面文件位置:放在
src/pages/...下,按业务模块分目录 - 页面元信息:在页面组件的
setup中使用:
import { definePage } from 'vue-router/auto';
definePage({
alias: ['/xxx', '/yyy'],
});
- 路由工具:推荐使用
src/utils/useRouteUtil.ts获取route/router/params<T>()
状态管理规范(Pinia setup store)
- 位置:
src/store/*.ts - 风格:
defineStore('name', () => { ... }),对外返回ref/computed等 - 与请求层鉴权同步:涉及登录态/鉴权时,确保“状态层的 token”与“API 客户端实际使用的鉴权头/拦截器/存储”一致,避免出现“界面认为已登录但请求未携带 token”或相反的情况
API 开发规范(Orval 生成代码)
本规范以 src/api/generated/ 为接口类型与路径的唯一真源,通过 Orval 从后端 OpenAPI 自动生成 TypeScript 类型 + 客户端方法。
1. 目录与职责边界
src/api/generated/:Orval 自动生成目录,禁止手改。api.ts:getReadingPlatformAPI()工厂函数,返回包含全部接口方法的对象。model/:OpenAPI 生成的 DTO/VO/Result/PageResult/Params 类型。
src/api/client.ts:项目侧的“统一入口/别名层”,导出readingApi(完整客户端实例)以及常用的类型工具(解包、分页别名等)。src/api/*.ts:业务侧“适配层”(可选),用于:- 兼容既有页面期望的“扁平结构/字段名/返回形态”
- 补齐 OpenAPI 暂未覆盖的历史接口(短期过渡)
- 汇聚跨接口的业务逻辑(例如组合请求、额外校验)
2. 基本原则(必须遵守)
- 生成代码只读:不得在
src/api/generated/**内做任何手工修改(包括修复类型、改路径、加字段)。 - 以生成类型为准:参数/返回类型优先使用
src/api/generated/model导出的类型,避免手写interface漂移。 - 对外只暴露稳定的业务接口:页面/组件尽量通过
src/api/*.ts(适配层)或src/api/client.ts(直接调用)访问,避免散落调用方式导致难以迁移。
3. 推荐调用方式
3.1 直接使用 Orval 客户端
- 统一从
src/api/client.ts引入:readingApi:getReadingPlatformAPI()的实例ApiResultOf/UnwrapResult/PageDataOf等类型工具
示例(以 Result<T> 为包裹结构):
import { readingApi } from '@/api/client';
import type { ResultTenant } from '@/api/generated/model';
async function loadTenant(id: number) {
const res = (await readingApi.getTenant(id)) as ResultTenant;
return res.data; // T(可能为 undefined,取决于后端返回与类型定义)
}
3.2 使用“适配层”稳定返回结构
当页面已经依赖历史返回结构(例如直接要 items/total/page/pageSize),在 src/api/*.ts 内做一次性适配,页面只消费适配后的结构。
分页适配建议统一输出:
items: T[]total: numberpage: numberpageSize: number
4. Result / PageResult 约定与解包
后端统一响应通常为:
- 普通接口:
Result<T>,字段一般为code/message/data - 分页接口:
Result<PageResult<T>>,字段一般为items/total/page/pageSize
在生成代码中常见类型形态:
ResultXXX(如ResultTenant、ResultUserInfoResponse)ResultPageResultXXX(如ResultPageResultTenant)PageResultXXX(如PageResultTenant)
建议做法:
- 组件/页面层尽量不要直接处理
ResultXXX,而是由适配层解包并做兜底(空数组、默认分页参数等)。 - 严禁在页面散落
as any;确需兼容时,集中在src/api/*.ts适配层进行,并在适配层内把“最终对页面返回的类型”定义清楚。
5. 命名与重复接口(getXxx/getXxx1/getXxx2)
由于不同角色端点(teacher/school/parent/admin)可能存在同名资源,Orval 在生成时会用 1/2/3 后缀消歧,例如:
getTask(teacher) vsgetTask1(school) vsgetTask2(parent)
规范建议:
- 业务层不要直接暴露带数字后缀的方法名;
- 在
src/api/*.ts中封装为语义化名称,例如:teacherGetTask/schoolGetTask/parentGetTask- 或按模块拆分到
src/api/teacher/task.ts等(如后续重构允许)
6. 何时需要更新生成代码
当后端 Controller 或 DTO/VO 发生变更:
- 后端更新 OpenAPI(Knife4j/SpringDoc)
- 前端更新规范并重新生成(项目已有脚本):
npm run api:update
- 提交生成物(通常包含
api-spec.*与src/api/generated/**)
注意:如果某接口在后端已存在但 OpenAPI 未导出(例如缺少注解/返回类型不规范),应优先修后端文档,而不是在前端“硬编码路径”长期绕过。
7. 禁止事项(高频踩坑)
- 禁止:手改
src/api/generated/**(下次生成会被覆盖,且会引入不可追踪差异)。 - 禁止:页面里手写 axios 调用去访问
/api/v1/...(除非 OpenAPI 暂缺且已在适配层集中兜底)。 - 禁止:在业务代码中扩散
any来“快速通过类型检查”。
8. 迁移策略(从旧 http 到 Orval)
若已有模块使用 src/api/index.ts 的 http.get/post/...:
- 短期:保留旧实现,但新增/变更接口优先走
readingApi - 中期:逐模块把旧
http调用替换为readingApi,并在适配层维持页面不改 - 长期:页面全面只依赖适配层/生成客户端,减少重复封装
本地存储规范
- Key 统一:只在
src/utils/useLocalStorage.ts的LocalStorageKey中声明与复用 - 登出清理:使用
outLoginClearStorage()(如需扩展清理范围,优先扩展LocalStorageKey)
组件与文件命名
- Vue 组件:
- 通用组件:
src/components/PascalCase.vue(现有仓库既有 PascalCase 也有小写文件名;新增时优先 PascalCase,避免混乱扩大) - 页面组件:
src/pages/<module>/index.vue(仓库已有此习惯) - 页面子组件:
src/pages/<module>/components/*.vue
- 通用组件:
- TS 工具:
src/utils/camelCase.ts或useXxx.ts(hook/组合式函数) - Store:
src/store/useXxx.ts或src/store/xxx.ts(与现有文件保持一致,避免引入第三种命名体系)
变更边界(AI 必须遵守)
- 不做无关重构:只改与需求相关的文件/代码;不要“顺便”换写法、换目录结构、统一命名
- 不引入新依赖:除非需求明确且必要;新增依赖要同步更新
package.json并说明原因 - 不改公共行为:如
request、token 同步、路由生成规则、构建配置(除非需求明确)
环境变量与配置(不要绕开)
- 读取方式:统一用
src/utils/env.ts的getAppEnvConfig(),不要在业务代码里到处直接读import.meta.env.xxx - 常用字段:
VITE_PREFIX_API:请求前缀(当前request使用的是 (${VITE_PREFIX_API}${url}))VITE_BASE_API:存在但当前请求实现未拼接(历史上有注释;如需启用必须全局评估影响)
- 注意:
vite.config.ts已配置 proxy(如/scienceApi、/localhostApi);开发环境应优先通过前缀 + proxy 工作,而不是写死域名
提交与协作
- 提交前:确保
pnpm dev能启动、关键页面可访问(至少覆盖改动路径) - 格式化:提交时会自动跑 Prettier;如果出现大量无关格式 diff,说明你改动触发了更大范围格式化,需收敛修改范围
- 变更说明:PR/提交信息优先解释“为什么”而不是“改了什么”
AI 变更自检清单(提交前必过)
- 范围:是否只改了需求相关文件?是否避免“顺手重构/改命名/大面积格式化”?
- 类型:
tsconfig已启用strict与noUnused*,新增代码是否无未使用变量/参数? - 路由:新增页面是否放在
src/pages/并使用definePage(如需 alias)? - 请求:是否遵守 Orval 规范(生成代码只读、调用集中在
src/api/client.ts/适配层,避免页面散落 axios/fetch)? - 鉴权:涉及登录态/鉴权时,是否确保 store 中的 token 与 API 客户端实际使用的鉴权头/拦截器/存储一致?
- 存储:localStorage key 是否只使用
LocalStorageKey? - 格式:是否符合
.prettierrc(单引号、2 空格、行宽 100),并接受提交前会被 Prettier 重写?
常见开发模板(给 AI 直接套用)
新增页面(文件路由)
- 新建
src/pages/<biz>/index.vue - 在
script setup中写definePage({ alias: [...] }) - 使用
RouterView/组件拼装页面
新增接口并调用
- 后端更新 OpenAPI(确保该接口可被导出)
- 前端执行
npm run api:update重新生成(产物在src/api/generated/**) - 在
src/api/client.ts或src/api/*.ts(适配层)封装稳定接口 - 页面/组件只从
src/api/client.ts或适配层引入调用,避免散落直连请求