frontend-mandatory-standards
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese前端开发规范
Front-end Development Specifications
技术栈
Tech Stack
- 语言:TypeScript
- 框架:Vue 3 Composition API ()
<script setup> - CSS:Tailwind CSS(避免使用 标签,Tailwind class 覆盖不了的场景除外)
<style> - 状态管理:Pinia
- UI 框架:Giime(基于 Element Plus 增强,→
el-*)gm-*
- Language: TypeScript
- Framework: Vue 3 Composition API ()
<script setup> - CSS: Tailwind CSS (avoid using tags, except for scenarios that cannot be covered by Tailwind classes)
<style> - State Management: Pinia
- UI Framework: Giime (enhanced based on Element Plus, →
el-*)gm-*
命名规范
Naming Specifications
| 场景 | 风格 | 示例 |
|---|---|---|
| 类型/接口 | PascalCase | |
| 变量/函数/文件夹 | camelCase | |
| 环境常量 | UPPER_CASE | |
| 组件文件 | PascalCase | |
| Composables | use 前缀 | |
| 布尔值 | 辅助动词前缀 | |
| 事件处理 | handle 前缀 | |
变量命名:避免 //// 等通用名称,使用 // 等具体业务名称。
datainfolistresultstatusformDatauserInfotaskList| Scenario | Style | Example |
|---|---|---|
| Type/Interface | PascalCase | |
| Variable/Function/Folder | camelCase | |
| Environment Constant | UPPER_CASE | |
| Component File | PascalCase | |
| Composables | use prefix | |
| Boolean Value | Auxiliary verb prefix | |
| Event Handling | handle prefix | |
Variable Naming: Avoid generic names such as ////, use specific business names such as //.
datainfolistresultstatusformDatauserInfotaskList通用编码规则
General Coding Rules
- 注释:每个方法和变量添加 JSDoc 注释,使用中文;函数内部添加适量单行中文注释
- 函数声明:优先使用 箭头函数而非
const,除非需要重载function - 异步:优先 ,不用 Promise 链式调用
async/await - 现代 ES:优先使用 、
?.、??、Promise.allSettled、replaceAll等Object.groupBy - 守卫语句:条件提前退出,减少嵌套深度
- 函数参数:1-2 个主参数 + options 对象,避免参数超长
ts
// ✅ 函数参数设计示例
const urlToFile: (url: string, options?: FileConversionOptions) => Promise<File>;- 工具库优先:减少造轮子
- 日期:
dayjs - Vue 工具:(
vueuse、tryOnMounted、useEventListener等)useLocalStorage - 数据处理:(
lodash-es、compact、cloneDeep等 ES 未提供的方法)uniqBy
- 日期:
- Git 操作:不要操作 git 命令,除非用户明确要求
- 格式化:修改后执行
npx eslint --fix <文件路径> - 类型检查:执行
npx vue-tsc --noEmit -p tsconfig.app.json
更多示例见 coding-conventions.md
- Comments: Add JSDoc comments in Chinese for each method and variable; add appropriate single-line Chinese comments inside functions
- Function Declaration: Prefer arrow functions over
const, unless overloading is requiredfunction - Asynchronous: Prefer , do not use Promise chain calls
async/await - Modern ES: Prefer using ,
?.,??,Promise.allSettled,replaceAll, etc.Object.groupBy - Guard Clause: Exit early when conditions are met to reduce nesting depth
- Function Parameters: 1-2 main parameters + options object to avoid overly long parameters
ts
// ✅ Function parameter design example
const urlToFile: (url: string, options?: FileConversionOptions) => Promise<File>;- Prefer tool libraries: Reduce reinventing the wheel
- Date:
dayjs - Vue Tools: (
vueuse,tryOnMounted,useEventListener, etc.)useLocalStorage - Data Processing: (methods not provided by ES such as
lodash-es,compact,cloneDeep, etc.)uniqBy
- Date:
- Git Operations: Do not run git commands unless explicitly requested by the user
- Formatting: Run after modification
npx eslint --fix <file path> - Type Check: Run
npx vue-tsc --noEmit -p tsconfig.app.json
For more examples, see coding-conventions.md
Composition API 规范
Composition API Specifications
1. 减少 watch,优先事件绑定
1. Reduce watch, prefer event binding
watch 是隐式依赖,难以追踪变更来源。优先在事件回调中直接处理逻辑,数据流更清晰:
ts
// ❌ 隐式监听,难以定位谁触发了变更
watch(count, () => {
console.log('changed');
});
// ✅ 在事件中直接处理,数据流清晰
const handleCountChange = () => {
console.log('changed');
};
// <gm-input v-model="count" @change="handleCountChange" />watch is an implicit dependency, making it difficult to track the source of changes. Prioritize processing logic directly in event callbacks for clearer data flow:
ts
// ❌ Implicit monitoring, difficult to locate who triggered the change
watch(count, () => {
console.log('changed');
});
// ✅ Process directly in the event, clear data flow
const handleCountChange = () => {
console.log('changed');
};
// <gm-input v-model="count" @change="handleCountChange" />2. 使用 defineModel
2. Use defineModel
Vue 3.4+ 引入的 将 props + emit + computed 三件套简化为一行,减少样板代码:
defineModelts
// ❌ 三件套写法,样板代码多
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
// ✅ defineModel 一行搞定
const value = defineModel<string>({ required: true });defineModelts
// ❌ Three-piece writing, lots of boilerplate code
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
// ✅ defineModel done in one line
const value = defineModel<string>({ required: true });3. 使用 useTemplateRef
3. Use useTemplateRef
Vue 3.5+ 推荐使用 获取模板引用,类型安全且避免 ref 变量名与模板 ref 属性名耦合:
useTemplateRefts
// ❌ 变量名必须与 template 中的 ref="inputRef" 完全一致
const inputRef = ref<FormInstance | null>(null);ts
// ✅ 类型安全,解耦变量名和模板 ref
const inputRef = useTemplateRef('inputRef');Vue 3.5+ recommends using to get template references, which is type-safe and avoids coupling between ref variable names and template ref attribute names:
useTemplateRefts
// ❌ Variable name must be exactly the same as ref="inputRef" in template
const inputRef = ref<FormInstance | null>(null);ts
// ✅ Type safe, decouple variable name and template ref
const inputRef = useTemplateRef('inputRef');4. 优先 ref,避免无理由 reactive
4. Prefer ref, avoid unnecessary reactive
reactiveref.valuets
// ❌ reactive 解构丢失响应性,整体替换需 Object.assign
const profileData = reactive({ name: '', age: 0 });ts
// ✅ ref 行为统一,profileData.value = newData 即可整体替换
const profileData = ref({ name: '', age: 0 });reactiveref.valuets
// ❌ reactive loses reactivity when destructured, need Object.assign for whole replacement
const profileData = reactive({ name: '', age: 0 });ts
// ✅ ref behaves uniformly, profileData.value = newData can replace the whole object directly
const profileData = ref({ name: '', age: 0 });5. Props 直接解构
5. Directly destructure Props
Vue 3.5+ 支持 解构时保持响应性并设置默认值, 已不再需要:
definePropswithDefaultsts
// ❌ withDefaults 在 Vue 3.5+ 已不必要
const props = withDefaults(defineProps<{ size?: number }>(), { size: 10 });
// ✅ 直接解构,简洁且响应式
const { size = 10 } = defineProps<{ size?: number }>();Vue 3.5+ supports maintaining reactivity and setting default values when destructuring , is no longer needed:
definePropswithDefaultsts
// ❌ withDefaults is no longer necessary in Vue 3.5+
const props = withDefaults(defineProps<{ size?: number }>(), { size: 10 });
// ✅ Direct destructuring, concise and reactive
const { size = 10 } = defineProps<{ size?: number }>();6. Dialog 暴露 openDialog
6. Dialog exposes openDialog
弹窗组件通过 暴露 方法,父组件通过 ref 调用,避免通过 props 控制 visible 导致的状态同步问题:
defineExposeopenDialogts
const dialogVisible = ref(false);
const openDialog = (data?: SomeType) => {
dialogVisible.value = true;
};
defineExpose({ openDialog });The dialog component exposes the method via , and the parent component calls it via ref, avoiding state synchronization problems caused by controlling visible via props:
openDialogdefineExposets
const dialogVisible = ref(false);
const openDialog = (data?: SomeType) => {
dialogVisible.value = true;
};
defineExpose({ openDialog });7. 自动导入,不要显式 import Vue API
7. Auto import, do not explicitly import Vue API
refcomputedwatchonMountedunplugin-auto-importimport { ref } from 'vue'refcomputedwatchonMountedunplugin-auto-importimport { ref } from 'vue'8. 样式用 Tailwind CSS
8. Use Tailwind CSS for styles
所有样式通过 Tailwind 的 class 实现。只有 Tailwind 无法覆盖的场景(如深度选择器 、复杂动画)才使用 。
:deep()<style>All styles are implemented via Tailwind classes. Only use for scenarios that Tailwind cannot cover (such as deep selector , complex animations).
<style>:deep()9. 配置数据抽离
9. Extract configuration data
选项列表、表单规则、tableId 等与页面逻辑无关的配置数据,在 中抽离,保持组件专注于交互逻辑:
modules/**/composables/useXxxOptions.tsts
export const useXxxOptions = () => {
const tableId = 'xxx-xxx-xxx-xxx-xxx';
const xxxOptions = [{ label: '选项1', value: 1, tagType: 'primary' as const }];
const rules = {
xxx: [{ required: true, message: 'xxx不能为空', trigger: 'blur' }],
};
return { tableId, xxxOptions, rules };
};Configuration data unrelated to page logic such as option lists, form rules, tableId, etc. should be extracted in to keep components focused on interaction logic:
modules/**/composables/useXxxOptions.tsts
export const useXxxOptions = () => {
const tableId = 'xxx-xxx-xxx-xxx-xxx';
const xxxOptions = [{ label: 'Option 1', value: 1, tagType: 'primary' as const }];
const rules = {
xxx: [{ required: true, message: 'xxx cannot be empty', trigger: 'blur' }],
};
return { tableId, xxxOptions, rules };
};10. 组件代码结构
10. Component code structure
统一使用 → → 顺序。
templatescriptstyleUnified order of → → .
templatescriptstyle代码拆分规范
Code Splitting Specifications
每次向已有 文件添加新功能、或创建新模块时,都应首先阅读 directory-structure.md 了解拆分原则。
.vueEvery time you add new features to an existing file or create a new module, you should first read directory-structure.md to understand the splitting principles.
.vue何时触发拆分
When to Trigger Splitting
| 触发场景 | 操作 |
|---|---|
| 新建增删改查模块 | 先阅读 crud.md 学习完整的 CRUD 代码模板和文件拆分方式,按模板生成所有文件 |
| 向已有页面添加功能(新增表单、弹窗等) | 先检查当前文件行数,超过 300 行应拆分;评估新增内容是否应作为独立子组件 |
| 新建模块/页面 | 先规划目录结构( |
| 修改迭代已有功能 | 如果发现当前文件已臃肿(>300 行),在完成需求的同时顺带拆分,不要让文件继续膨胀 |
| Trigger Scenario | Operation |
|---|---|
| New CRUD module | First read crud.md to learn the complete CRUD code template and file splitting method, generate all files according to the template |
| Add functions to existing pages (new forms, dialogs, etc.) | First check the current file line count, split if it exceeds 300 lines; evaluate whether the new content should be an independent sub-component |
| New module/page | Plan the directory structure first ( |
| Modify and iterate existing functions | If the current file is already bloated (>300 lines), split it while completing the requirement, do not let the file continue to expand |
核心原则
Core Principles
- 一般 文件不超过 300 行:除入口级别文件(如
.vue)外,子组件、弹窗等文件应控制在 300 行以内index.vue - 入口文件可适当放宽:作为模块入口承担编排职责,行数可适当超出,但也应尽量精简
index.vue - 按功能区域拆分 UI 组件:每个独立的功能区域(搜索栏、表格、弹窗、表单区等)应作为独立子组件
- 核心业务逻辑保留在 :主页面负责数据获取、状态管理、子组件编排
index.vue - UI 展示逻辑下沉到子组件:子组件只负责渲染和用户交互
- 避免过度传参:当 props 层级超过 2 层时,使用 Pinia 替代深层 props 传递
- Generally, files should not exceed 300 lines: Except for entry-level files (such as
.vue), files of sub-components, dialogs, etc. should be controlled within 300 linesindex.vue - Entry files can be appropriately relaxed: As the module entry, undertakes the orchestration responsibility, the number of lines can be appropriately exceeded, but it should also be as streamlined as possible
index.vue - Split UI components by functional area: Each independent functional area (search bar, table, dialog, form area, etc.) should be an independent sub-component
- Core business logic remains in : The main page is responsible for data acquisition, state management, and sub-component orchestration
index.vue - UI display logic is sunk to sub-components: Sub-components are only responsible for rendering and user interaction
- Avoid excessive parameter passing: When the props level exceeds 2 layers, use Pinia instead of deep props passing
标准模块目录结构
Standard Module Directory Structure
modules/xxx/
├── index.vue # 主页面(编排子组件、管理数据)
├── components/ # UI 子组件
│ ├── Search.vue
│ ├── Table.vue
│ └── EditDialog.vue
├── composables/ # 逻辑复用
│ └── useXxxOptions.ts
└── stores/ # 状态管理(需要时)
└── useXxxStore.ts详细的拆分策略、示例代码和 Props 传递原则见 directory-structure.md
modules/xxx/
├── index.vue # Main page (orchestrate sub-components, manage data)
├── components/ # UI sub-components
│ ├── Search.vue
│ ├── Table.vue
│ └── EditDialog.vue
├── composables/ # Logic reuse
│ └── useXxxOptions.ts
└── stores/ # State management (when needed)
└── useXxxStore.tsFor detailed splitting strategies, sample code and Props passing principles, see directory-structure.md
接口调用规范
API Calling Specifications
文件限制
File Restrictions
/src/apiFiles under are automatically generated by code generation tools, you can make simple modifications but cannot create or delete files.
/src/api文件结构
File Structure
每个接口生成两个文件:
- — 原始 axios 方法
postXxxYyyZzz.ts - — useAxios 响应式封装
usePostXxxYyyZzz.ts
文件名通过 "请求方法+路由地址" 生成: → +
post /open/v1/system/listpostOpenV1SystemList.tsusePostOpenV1SystemList.tsEach interface generates two files:
- — Original axios method
postXxxYyyZzz.ts - — useAxios reactive wrapper
usePostXxxYyyZzz.ts
The file name is generated by "request method + routing address": → +
post /open/v1/system/listpostOpenV1SystemList.tsusePostOpenV1SystemList.ts选择规则
Selection Rules
| 场景 | 使用版本 | 原因 |
|---|---|---|
| 默认 | | 提供响应式数据( |
| 循环/批量请求 | 原始版本(无 | |
ts
// ✅ 默认:useAxios 封装
const { exec: getListExec } = usePostXxxV1ListPage();
await getListExec();ts
// ✅ 批量:原始接口 + Promise.all
await Promise.all(ids.map(id => deleteXxxV1Item({ id })));| Scenario | Used Version | Reason |
|---|---|---|
| Default | | Provides reactive data ( |
| Loop/batch request | Original version (without | |
ts
// ✅ Default: useAxios wrapper
const { exec: getListExec } = usePostXxxV1ListPage();
await getListExec();ts
// ✅ Batch: original interface + Promise.all
await Promise.all(ids.map(id => deleteXxxV1Item({ id })));API 导入规则
API Import Rules
所有接口和类型统一从 或 导入,降低耦合度,方便后期重构:
controller/index.tsinterface/index.tsts
// ✅ 从 controller 统一导入
import type { PostGmpV1CrowdListInput } from '@/api/gmp/controller';
import { postGmpV1CrowdList } from '@/api/gmp/controller';ts
// ✅ 公共类型从 interface 导入
import type { CrowdItemVo } from '@/api/gmp/interface';ts
// ❌ 不要从具体文件导入
import { postGmpV1CrowdDetail } from '@/api/gmp/controller/RenQunGuanLi/postGmpV1CrowdList';All interfaces and types are uniformly imported from or to reduce coupling and facilitate later refactoring:
controller/index.tsinterface/index.tsts
// ✅ Import uniformly from controller
import type { PostGmpV1CrowdListInput } from '@/api/gmp/controller';
import { postGmpV1CrowdList } from '@/api/gmp/controller';ts
// ✅ Import public types from interface
import type { CrowdItemVo } from '@/api/gmp/interface';ts
// ❌ Do not import from specific files
import { postGmpV1CrowdDetail } from '@/api/gmp/controller/RenQunGuanLi/postGmpV1CrowdList';错误处理
Error Handling
- 无需 try/catch:拦截器自动弹出错误提示,业务代码不需要手动 catch
createAxios - 无需判断 code:非正常响应码会被拦截器自动 reject,不需要
if (code !== 200) - 需要 finally 时:使用 try/finally(不写 catch),用于清理副作用
- No need for try/catch: The interceptor automatically pops up an error prompt, and business code does not need to catch manually
createAxios - No need to judge code: Abnormal response codes will be automatically rejected by the interceptor, no need for
if (code !== 200) - When finally is needed: Use try/finally (do not write catch) to clean up side effects
使用流程
Usage Process
- 如果提供了接口地址,第一步找到 中定义的请求方法
@/api/xxx/controller - 仔细阅读接口文件中的类型定义,不要自己编参数
- 根据场景选择 版本或原始版本
useXxx - 下拉框、单选框等数据源可从接口文档注释获取,获取后在 中抽离复用
useXxxOptions
更多用法示例见 api-conventions.md
- If the interface address is provided, first find the request method defined in
@/api/xxx/controller - Read the type definition in the interface file carefully, do not make up parameters yourself
- Select the version or the original version according to the scenario
useXxx - Data sources such as drop-down boxes and radio buttons can be obtained from the interface document comments, and then extracted and reused in
useXxxOptions
For more usage examples, see api-conventions.md
Pinia 状态管理
Pinia State Management
Store 文件组织
Store File Organization
modules/xxx/
├── stores/
│ └── useXxxStore.ts # 模块专用 store
└── index.vuemodules/xxx/
├── stores/
│ └── useXxxStore.ts # Module-specific store
└── index.vue何时使用
When to Use
Pinia 适合跨多层组件共享状态、异步任务轮询等场景。不要过度使用——只在父子组件间传递的数据用 props/emits,局部状态用 ref,简单表单数据用 v-model。
Pinia is suitable for scenarios such as sharing state across multi-layer components, asynchronous task polling, etc. Do not overuse it — use props/emits for data passed between parent and child components, ref for local state, v-model for simple form data.
使用规范
Usage Specifications
ts
// 命名体现业务含义,不要用 const store = useXxxStore
const xxxStore = useXxxStore();
xxxStore.resetStore(); // 进入页面时重置,确保干净状态ts
// The name reflects the business meaning, do not use const store = useXxxStore
const xxxStore = useXxxStore();
xxxStore.resetStore(); // Reset when entering the page to ensure a clean state跨组件共享状态
Cross-component State Sharing
当组件嵌套超过 2 层且需要共享状态时,用 Pinia 替代 props 层层传递:
index.vue → Result.vue → ResultSuccess.vue → ImageSection.vue
↓
各组件直接访问 store,不需要 props 逐层传递Store 标准写法、轮询任务模式等见 pinia-patterns.md
When components are nested more than 2 layers and need to share state, use Pinia instead of layer-by-layer props passing:
index.vue → Result.vue → ResultSuccess.vue → ImageSection.vue
↓
All components access the store directly, no need for layer-by-layer props passingFor standard Store writing, polling task modes, etc., see pinia-patterns.md
参考文档
Reference Documents
| 主题 | 说明 | 参考 |
|---|---|---|
| CRUD 代码模板 | 新建增删改查页面时必读,完整的文件拆分和代码模板 | crud.md |
| 编码约定详解 | 守卫语句、函数参数设计、工具库用法等完整示例 | coding-conventions.md |
| 接口调用指南 | useAxios 用法、类型定义、导入规则完整说明 | api-conventions.md |
| Pinia 使用模式 | Store 创建、重置、跨组件共享等模式 | pinia-patterns.md |
| 组件拆分规范 | 何时拆分、目录结构、Props 传递原则 | directory-structure.md |
| 字典模块规范 | useDictionary、Store 创建、命名规范 | dictionary.md |
| 环境变量配置 | .env 分层原则、域名规则、多环境构建 | env.md |
| Topic | Description | Reference |
|---|---|---|
| CRUD Code Template | Must read when creating new CRUD pages, complete file splitting and code templates | crud.md |
| Detailed Coding Conventions | Complete examples of guard clauses, function parameter design, tool library usage, etc. | coding-conventions.md |
| API Calling Guide | Complete description of useAxios usage, type definition, import rules | api-conventions.md |
| Pinia Usage Patterns | Store creation, reset, cross-component sharing and other modes | pinia-patterns.md |
| Component Splitting Specifications | When to split, directory structure, Props passing principles | directory-structure.md |
| Dictionary Module Specifications | useDictionary, Store creation, naming specifications | dictionary.md |
| Environment Variable Configuration | .env layering principle, domain name rules, multi-environment construction | env.md |