Front-end Development Specifications
Tech Stack
- Language: TypeScript
- Framework: Vue 3 Composition API ()
- CSS: Tailwind CSS (avoid using tags, except for scenarios that cannot be covered by Tailwind classes)
- State Management: Pinia
- UI Framework: Giime (enhanced based on Element Plus, → )
Naming Specifications
| 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
/
/
.
General Coding Rules
- 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 , unless overloading is required
- Asynchronous: Prefer , do not use Promise chain calls
- Modern ES: Prefer using , , , , , 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:
- Vue Tools: (, , , etc.)
- Data Processing: (methods not provided by ES such as , , , 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 Specifications
1. Reduce watch, prefer event binding
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. Use 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. Use useTemplateRef
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:
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. Prefer ref, avoid unnecessary reactive
loses reactivity when destructured, and you need to assign values field by field when replacing the whole object.
behaves more predictably, and you can directly replace the whole object via
:
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. Directly destructure Props
Vue 3.5+ supports maintaining reactivity and setting default values when destructuring
,
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 exposes 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:
ts
const dialogVisible = ref(false);
const openDialog = (data?: SomeType) => {
dialogVisible.value = true;
};
defineExpose({ openDialog });
7. Auto import, do not explicitly import Vue API
,
,
,
, etc. are already auto-imported via
. Explicit
import { ref } from 'vue'
will produce redundant code and is inconsistent with project configuration.
8. Use Tailwind CSS for styles
All styles are implemented via Tailwind classes. Only use
for scenarios that Tailwind cannot cover (such as deep selector
, complex animations).
9. Extract configuration data
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. Component code structure
Code Splitting Specifications
Every 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.
When to Trigger Splitting
| 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 (, , ), pre-split according to functional areas |
| 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
- Generally, files should not exceed 300 lines: Except for entry-level files (such as ), files of sub-components, dialogs, etc. should be controlled within 300 lines
- 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
- 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
- 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 # 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
Files under
are automatically generated by code generation tools, you can make simple modifications but
cannot create or delete files.
File Structure
Each interface generates two files:
- — Original axios method
- — useAxios reactive wrapper
The file name is generated by "request method + routing address":
post /open/v1/system/list
→
+
usePostOpenV1SystemList.ts
Selection Rules
| Scenario | Used Version | Reason |
|---|
| Default | wrapped version | Provides reactive data (, ) and automatic race condition cancellation |
| Loop/batch request | Original version (without prefix) | 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 Import Rules
All interfaces and types are uniformly imported from
or
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
- No need for try/catch: The 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
- When finally is needed: Use try/finally (do not write catch) to clean up side effects
Usage Process
- If the interface address is provided, first find the request method defined in
- 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
- Data sources such as drop-down boxes and radio buttons can be obtained from the interface document comments, and then extracted and reused in
For more usage examples, see api-conventions.md
Pinia State Management
Store File Organization
modules/xxx/
├── stores/
│ └── useXxxStore.ts # Module-specific store
└── index.vue
When to Use
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
// 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
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
| 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 |