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(避免使用
    <style>
    标签,Tailwind class 覆盖不了的场景除外)
  • 状态管理:Pinia
  • UI 框架:Giime(基于 Element Plus 增强,
    el-*
    gm-*
  • Language: TypeScript
  • Framework: Vue 3 Composition API (
    <script setup>
    )
  • CSS: Tailwind CSS (avoid using
    <style>
    tags, except for scenarios that cannot be covered by Tailwind classes)
  • State Management: Pinia
  • UI Framework: Giime (enhanced based on Element Plus,
    el-*
    gm-*
    )

命名规范

Naming Specifications

场景风格示例
类型/接口PascalCase
UserInfo
,
ApiResponse
变量/函数/文件夹camelCase
userName
,
handleSubmit()
环境常量UPPER_CASE
VITE_BASE_URL
组件文件PascalCase
UserProfile.vue
Composablesuse 前缀
useUserInfo
,
useFormValidation
布尔值辅助动词前缀
isLoading
,
hasError
,
canSubmit
事件处理handle 前缀
handleClick
,
handleSubmit
变量命名:避免
data
/
info
/
list
/
result
/
status
等通用名称,使用
formData
/
userInfo
/
taskList
等具体业务名称。
ScenarioStyleExample
Type/InterfacePascalCase
UserInfo
,
ApiResponse
Variable/Function/FoldercamelCase
userName
,
handleSubmit()
Environment ConstantUPPER_CASE
VITE_BASE_URL
Component FilePascalCase
UserProfile.vue
Composablesuse prefix
useUserInfo
,
useFormValidation
Boolean ValueAuxiliary verb prefix
isLoading
,
hasError
,
canSubmit
Event Handlinghandle prefix
handleClick
,
handleSubmit
Variable Naming: Avoid generic names such as
data
/
info
/
list
/
result
/
status
, use specific business names such as
formData
/
userInfo
/
taskList
.

通用编码规则

General Coding Rules

  • 注释:每个方法和变量添加 JSDoc 注释,使用中文;函数内部添加适量单行中文注释
  • 函数声明:优先使用
    const
    箭头函数而非
    function
    ,除非需要重载
  • 异步:优先
    async/await
    ,不用 Promise 链式调用
  • 现代 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
      uniqBy
      等 ES 未提供的方法)
  • 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
    const
    arrow functions over
    function
    , unless overloading is required
  • Asynchronous: Prefer
    async/await
    , do not use Promise chain calls
  • Modern ES: Prefer using
    ?.
    ,
    ??
    ,
    Promise.allSettled
    ,
    replaceAll
    ,
    Object.groupBy
    , etc.
  • 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
      ,
      useLocalStorage
      , etc.)
    • Data Processing:
      lodash-es
      (methods not provided by ES such as
      compact
      ,
      cloneDeep
      ,
      uniqBy
      , etc.)
  • Git Operations: Do not run git commands unless explicitly requested by the user
  • Formatting: Run
    npx eslint --fix <file path>
    after modification
  • 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+ 引入的
defineModel
将 props + emit + computed 三件套简化为一行,减少样板代码:
ts
// ❌ 三件套写法,样板代码多
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();

// ✅ defineModel 一行搞定
const value = defineModel<string>({ required: true });
defineModel
introduced in Vue 3.4+ simplifies the three-piece set of props + emit + computed into one line, reducing boilerplate code:
ts
// ❌ 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+ 推荐使用
useTemplateRef
获取模板引用,类型安全且避免 ref 变量名与模板 ref 属性名耦合:
ts
// ❌ 变量名必须与 template 中的 ref="inputRef" 完全一致
const inputRef = ref<FormInstance | null>(null);
ts
// ✅ 类型安全,解耦变量名和模板 ref
const inputRef = useTemplateRef('inputRef');
Vue 3.5+ recommends using
useTemplateRef
to get template references, which is type-safe and avoids coupling between ref variable names and template ref attribute names:
ts
// ❌ 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

reactive
在解构时会丢失响应性,整体替换时需要逐字段赋值。
ref
行为更可预测,通过
.value
可以直接整体替换:
ts
// ❌ reactive 解构丢失响应性,整体替换需 Object.assign
const profileData = reactive({ name: '', age: 0 });
ts
// ✅ ref 行为统一,profileData.value = newData 即可整体替换
const profileData = ref({ name: '', age: 0 });
reactive
loses reactivity when destructured, and you need to assign values field by field when replacing the whole object.
ref
behaves more predictably, and you can directly replace the whole object via
.value
:
ts
// ❌ 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+ 支持
defineProps
解构时保持响应性并设置默认值,
withDefaults
已不再需要:
ts
// ❌ 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
defineProps
,
withDefaults
is no longer needed:
ts
// ❌ 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

弹窗组件通过
defineExpose
暴露
openDialog
方法,父组件通过 ref 调用,避免通过 props 控制 visible 导致的状态同步问题:
ts
const dialogVisible = ref(false);

const openDialog = (data?: SomeType) => {
  dialogVisible.value = true;
};

defineExpose({ openDialog });
The dialog component exposes the
openDialog
method via
defineExpose
, and the parent component calls it via ref, avoiding state synchronization problems caused by controlling visible via props:
ts
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

ref
computed
watch
onMounted
等已通过
unplugin-auto-import
自动导入。显式
import { ref } from 'vue'
会产生冗余代码,且与项目配置不一致。
ref
,
computed
,
watch
,
onMounted
, etc. are already auto-imported via
unplugin-auto-import
. Explicit
import { ref } from 'vue'
will produce redundant code and is inconsistent with project configuration.

8. 样式用 Tailwind CSS

8. Use Tailwind CSS for styles

所有样式通过 Tailwind 的 class 实现。只有 Tailwind 无法覆盖的场景(如深度选择器
:deep()
、复杂动画)才使用
<style>
All styles are implemented via Tailwind classes. Only use
<style>
for scenarios that Tailwind cannot cover (such as deep selector
:deep()
, complex animations).

9. 配置数据抽离

9. Extract configuration data

选项列表、表单规则、tableId 等与页面逻辑无关的配置数据,在
modules/**/composables/useXxxOptions.ts
中抽离,保持组件专注于交互逻辑:
ts
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
modules/**/composables/useXxxOptions.ts
to keep components focused on interaction logic:
ts
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

统一使用
template
script
style
顺序。
Unified order of
template
script
style
.

代码拆分规范

Code Splitting Specifications

每次向已有
.vue
文件添加新功能、或创建新模块时,都应首先阅读 directory-structure.md 了解拆分原则。
Every time you add new features to an existing
.vue
file or create a new module, you should first read directory-structure.md to understand the splitting principles.

何时触发拆分

When to Trigger Splitting

触发场景操作
新建增删改查模块先阅读 crud.md 学习完整的 CRUD 代码模板和文件拆分方式,按模板生成所有文件
向已有页面添加功能(新增表单、弹窗等)先检查当前文件行数,超过 300 行应拆分;评估新增内容是否应作为独立子组件
新建模块/页面先规划目录结构(
components/
composables/
stores/
),按功能区域预拆分
修改迭代已有功能如果发现当前文件已臃肿(>300 行),在完成需求的同时顺带拆分,不要让文件继续膨胀
Trigger ScenarioOperation
New CRUD moduleFirst 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/pagePlan the directory structure first (
components/
,
composables/
,
stores/
), pre-split according to functional areas
Modify and iterate existing functionsIf the current file is already bloated (>300 lines), split it while completing the requirement, do not let the file continue to expand

核心原则

Core Principles

  • 一般
    .vue
    文件不超过 300 行
    :除入口级别文件(如
    index.vue
    )外,子组件、弹窗等文件应控制在 300 行以内
  • 入口文件可适当放宽
    index.vue
    作为模块入口承担编排职责,行数可适当超出,但也应尽量精简
  • 按功能区域拆分 UI 组件:每个独立的功能区域(搜索栏、表格、弹窗、表单区等)应作为独立子组件
  • 核心业务逻辑保留在
    index.vue
    :主页面负责数据获取、状态管理、子组件编排
  • UI 展示逻辑下沉到子组件:子组件只负责渲染和用户交互
  • 避免过度传参:当 props 层级超过 2 层时,使用 Pinia 替代深层 props 传递
  • Generally,
    .vue
    files should not exceed 300 lines
    : Except for entry-level files (such as
    index.vue
    ), files of sub-components, dialogs, etc. should be controlled within 300 lines
  • Entry files can be appropriately relaxed: As the module entry,
    index.vue
    undertakes the orchestration responsibility, the number of lines can be appropriately exceeded, but it should also be as streamlined as possible
  • 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
    index.vue
    : The main page is responsible for data acquisition, state management, and sub-component orchestration
  • 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.ts
For detailed splitting strategies, sample code and Props passing principles, see directory-structure.md

接口调用规范

API Calling Specifications

文件限制

File Restrictions

/src/api
下的文件由代码生成工具自动生成,可以简单修改但不能新建和删除
Files under
/src/api
are automatically generated by code generation tools, you can make simple modifications but cannot create or delete files.

文件结构

File Structure

每个接口生成两个文件:
  • postXxxYyyZzz.ts
    — 原始 axios 方法
  • usePostXxxYyyZzz.ts
    — useAxios 响应式封装
文件名通过 "请求方法+路由地址" 生成:
post /open/v1/system/list
postOpenV1SystemList.ts
+
usePostOpenV1SystemList.ts
Each interface generates two files:
  • postXxxYyyZzz.ts
    — Original axios method
  • usePostXxxYyyZzz.ts
    — useAxios reactive wrapper
The file name is generated by "request method + routing address":
post /open/v1/system/list
postOpenV1SystemList.ts
+
usePostOpenV1SystemList.ts

选择规则

Selection Rules

场景使用版本原因
默认
useXxx
封装版本
提供响应式数据(
data
isLoading
)和自动取消竞态
循环/批量请求原始版本(无
use
前缀)
useAxios
会取消前次请求,循环调用时只有最后一个生效
ts
// ✅ 默认:useAxios 封装
const { exec: getListExec } = usePostXxxV1ListPage();

await getListExec();
ts
// ✅ 批量:原始接口 + Promise.all
await Promise.all(ids.map(id => deleteXxxV1Item({ id })));
ScenarioUsed VersionReason
Default
useXxx
wrapped version
Provides reactive data (
data
,
isLoading
) and automatic race condition cancellation
Loop/batch requestOriginal version (without
use
prefix)
useAxios
will cancel the previous request, and only the last one will take effect when called in a loop
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.ts
interface/index.ts
导入,降低耦合度,方便后期重构:
ts
// ✅ 从 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
controller/index.ts
or
interface/index.ts
to reduce coupling and facilitate later refactoring:
ts
// ✅ 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
    createAxios
    拦截器自动弹出错误提示,业务代码不需要手动 catch
  • 无需判断 code:非正常响应码会被拦截器自动 reject,不需要
    if (code !== 200)
  • 需要 finally 时:使用 try/finally(不写 catch),用于清理副作用
  • No need for try/catch: The
    createAxios
    interceptor automatically pops up an error prompt, and business code does not need to catch manually
  • 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

  1. 如果提供了接口地址,第一步找到
    @/api/xxx/controller
    中定义的请求方法
  2. 仔细阅读接口文件中的类型定义,不要自己编参数
  3. 根据场景选择
    useXxx
    版本或原始版本
  4. 下拉框、单选框等数据源可从接口文档注释获取,获取后在
    useXxxOptions
    中抽离复用
更多用法示例见 api-conventions.md
  1. If the interface address is provided, first find the request method defined in
    @/api/xxx/controller
  2. Read the type definition in the interface file carefully, do not make up parameters yourself
  3. Select the
    useXxx
    version or the original version according to the scenario
  4. 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.vue
modules/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 passing
For 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
TopicDescriptionReference
CRUD Code TemplateMust read when creating new CRUD pages, complete file splitting and code templatescrud.md
Detailed Coding ConventionsComplete examples of guard clauses, function parameter design, tool library usage, etc.coding-conventions.md
API Calling GuideComplete description of useAxios usage, type definition, import rulesapi-conventions.md
Pinia Usage PatternsStore creation, reset, cross-component sharing and other modespinia-patterns.md
Component Splitting SpecificationsWhen to split, directory structure, Props passing principlesdirectory-structure.md
Dictionary Module SpecificationsuseDictionary, Store creation, naming specificationsdictionary.md
Environment Variable Configuration.env layering principle, domain name rules, multi-environment constructionenv.md