Tech Stack
Vue 3 (Composition API) · TypeScript · Vite · Pinia · Vue Router · UnoCSS · SCSS · Element Plus
Directory Structure
src/
├── api/ # API 请求层(index.ts + types.ts)
├── components/ # 全局复用组件
├── composables/ # 组合式函数(usePageTable 等)
├── constants/ # 常量(StorageKeys 等)
├── directives/ # 自定义指令(v-hasPerm 等)
├── lang/ # 国际化(index.ts + package/)
├── layouts/ # 布局组件
├── router/ # 路由 + 守卫
├── stores/ # Pinia(扁平结构,无 modules/)
├── styles/ # 全局样式
├── utils/ # 工具函数(request.ts, auth.ts)
├── views/ # 页面(与路由对应)
│ └── system/role/
│ └── index.vue # 单文件:搜索+表格+弹窗
└── settings.ts # 项目配置
Naming Conventions
| Types | Style | Example |
|---|---|---|
| Variables | camelCase | userName |
| Constants | UPPER_SNAKE_CASE | MAX_COUNT |
| Functions | camelCase, starting with a verb | fetchUserList |
| Classes/Interfaces/Types | PascalCase | UserInfo |
| Enums | PascalCase | StatusEnum |
| Enum values | UPPER_SNAKE_CASE | StatusEnum.ACTIVE |
| Boolean values | is/has/can/should prefix | isLoading |
| Vue component files | PascalCase | UserForm.vue |
| Page files | index.vue | system/user/index.vue |
| TS/JS modules | kebab-case | format-date.ts |
| Composables | use + camelCase | usePageTable |
| Store | use + module name + Store | useUserStore |
Do not use a frontend type with the VO/DTO suffix. Semantic naming:UserItem(list item),UserForm(form),UserQueryParams(query parameters),UserDetail(details).
Method naming and handle rules
| Scenario | Naming |
|---|---|
| Query/Load | fetchList / loadOptions |
| Open/Close dialog | openDialog / closeDialog |
| Submit/Save | submitForm / saveX |
| Add/Edit/Delete | createX / updateX / deleteX |
| Reset | resetForm / resetQuery |
| Event entry point (flow orchestration) | handleSubmit / handleEditClick |
handle Criterion: if the function does only one thing → do not use handle (reusable); if it combines multiple actions + flow control → use handle (event entry point).
// 单一动作 → 不用 handle
function openDialog() { dialogState.visible = true; }
// 流程编排 → 使用 handle
async function handleSubmit() {
const valid = await validateForm();
if (!valid) return;
await submitForm(formData);
closeDialog();
fetchList();
}
fetch/load already implies async semantics; do not add the Async suffix.
CSS / UnoCSS / SCSS boundaries
UnoCSS only handles non-semantic fine-tuning (spacing, alignment); structural styles belong to BEM + SCSS.
| Scenario | Solution |
|---|---|
| Global page skeleton | Global classes (such as page-*) |
| Elements with structural semantics | BEM + SCSS |
| Non-semantic layout fine-tuning | UnoCSS |
| Deep selectors/animations/media queries | SCSS |
Rules:
- No more than 3 atomic classes on the same element; extract BEM if exceeded
- Use CSS variables for colors; hardcoding is prohibited
- BEM format:
block__element--modifier, kebab-case, with page prefix - Use
is-*for state, and BEM Modifier for variants:layout--top
Component Standards
- SFC block order:
template→script setup→style scoped - Order inside script: imports → Props/Emits → state → computed properties → watchers → lifecycle → methods → defineExpose
- Props prefer TypeScript type declarations +
withDefaults - Components ≤ 300 lines
Types and API conventions
Public types
interface ApiResult<T = unknown> { code: string; data: T; msg: string; }
interface BaseQueryParams { pageNum: number; pageSize: number; sortBy?: string; order?: string; }
interface PageResult<T> { list: T[]; total: number; }
API definitions
const USER_BASE_URL = "/api/v1/users";
const UserAPI = {
/** 获取用户分页数据 */
getPage(q: UserQueryParams) { return request<unknown, PageResult<UserItem>>({ url: USER_BASE_URL, method: "get", params: q }); },
/** 获取用户表单数据 */
getFormData(id: string) { return request<unknown, UserForm>({ url: `${USER_BASE_URL}/${id}/form`, method: "get" }); },
/** 新增用户 */
create(data: UserForm) { return request({ url: USER_BASE_URL, method: "post", data }); },
/** 更新用户 */
update(id: string, data: UserForm) { return request({ url: `${USER_BASE_URL}/${id}`, method: "put", data }); },
/** 批量删除用户 */
deleteByIds(ids: string) { return request({ url: `${USER_BASE_URL}/${ids}`, method: "delete" }); },
};
export default UserAPI;
export * from "./types";
Response interceptor automatically unwraps:code === "00000" returns response.data.data. When the token expires (A0230), refreshTokenOnce() single-flight refresh.
Page development patterns
By default, use the Composables pattern to develop pages. Only when it is explicitly specified to use a CURD page, use the Config-driven pattern:
| Pattern | Applicable scenarios | Reference |
|---|---|---|
| Composables pattern (default) | List pages, form pages, pages with custom interactions | references/new-page-guide.md |
| Simple pages | Dashboards, settings pages, detail pages | references/new-page-guide.md |
| Config-driven CURD (must be specified) | Standard create, read, update, delete with no special interactions | references/curd-development.md |
Use a unified page root node class="page-container",Internal sections:page-search(search area),page-content(content area),page-toolbar(toolbar).
Store
Setup Store syntax, flat directory. Use outside components useXxxStoreHook() to avoid Pinia not being initialized.
export const useUserStore = defineStore("user", () => {
const userInfo = ref<UserInfo>({} as UserInfo);
return { userInfo };
});
export function useUserStoreHook() {
return useUserStore(store);
}
Composables
use prefix + camelCase. Parameters use an options object, returning reactive refs and methods.
Comments
Comments write "why" and "pitfalls", not what the code already says. Functions use /** */, not //。
format:
- Functions/methods →
/** */JSDoc - Types/interfaces/properties → single-line
/** */ - Inside functions "why" →
//inline - Configuration grouping →
//brief
Function comments depend on the situation:
/** 打开角色表单弹窗 */
function openDialog() { ... }
/**
* 打开编辑角色弹窗
*
* @param roleId 角色 ID
*/
async function handleEditClick(roleId: string) { ... }
/**
* 校验并提交角色表单
*
* 非自定义数据权限时丢弃部门 ID
*/
async function handleSubmit() { ... }
A simple function is fine with one line. If it has parameters, add @param. If there are pitfalls, add one more line in multiple lines—only add "why", do not restate what the function name already makes clear.
Common mistakes:
// ❌ 复述代码:函数名已经说了"打开弹窗",注释是废话
/** 打开弹窗 */
function openDialog() { ... }
// ❌ 描述"做什么"而非"为什么"
/** 遍历列表并过滤状态为启用的项 */
const enabledList = list.filter(item => item.status === 1);
// ✅ 写"为什么"
// status=1 是启用,数据库默认值是 0(禁用)
const enabledList = list.filter(item => item.status === 1);
// ❌ 每个函数都写注释,哪怕函数名一目了然
/** 获取用户信息 */
function getUserInfo() { ... }
// ✅ 函数名能说清的不写
function getUserInfo() { ... }
Anti-pattern quick reference
| Anti-pattern | Correct approach |
|---|---|
Types use VO/DTO suffix |
Semantic naming:UserItem、UserForm |
| Hardcoded colors | CSS variables |
| More than 3 atomic classes | Extract BEM + SCSS |
class UserAPI static method |
const UserAPI = {} object literal |
stores/modules/ subdirectory |
Flat stores/ structure |
Use directly outside components useXxxStore() |
Use useXxxStoreHook() |
Manually add to CURD buttons v-hasPerm |
Use permPrefix automatic concatenation |
meta.title Write Chinese directly |
Write language pack key |
Functions use // comments |
Use /** */ JSDoc format |
| Comments restate "what to do" | Write "why" and constraints |
Self-check list
- type has no
VO/DTOsuffix - boolean has
is/has/can/shouldprefix -
handleonly for process orchestration - API uses
const XXXAPI = {}object literal - BEM with page prefix, atomic classes ≤ 3
- colors use CSS variables
- SFC block order: template → script → style
- Store uses Setup Store +
useXxxStoreHook() - components ≤ 300 lines
-
meta.titleUse language pack key - permission identifiers follow
模块:资源:操作format - API modules follow
index.ts+types.tsstructure - Functions use
/** */comments, not// - comments write "why", not restate "what to do"
Reference documentation
The following documents are loaded on demand, covering detailed guides for specific development scenarios:
| Reference file | Applicable scenario |
|---|---|
| references/new-page-guide.md | Adding APIs and pages: create API modules, Composables pattern CURD, simple pages, pattern selection |
| references/curd-development.md | Config-driven CURD: configuration APIs, column templates, form items, usePage() |
| references/router-menu.md | Configuring routes and menus: static/dynamic routes, Meta fields, multi-level menus |
| references/permission.md | Permission control: v-hasPerm directive, hasPerm() function, permission identifier naming |
| references/page-caching.md | Page caching: keepAlive configuration, caching mechanism, refreshing the current page |
| references/i18n.md | Menu internationalization: translateRouteTitle, language pack configuration, language switching |
| references/project-config.md | Project configuration: settings.ts, environment variables, StorageKey management |
| references/components.md | Common components: Upload, Dict, TableSelect, IconSelect |