349 lines
6.8 KiB
Plaintext
349 lines
6.8 KiB
Plaintext
---
|
|
description: Vue 3 前端架构规范和组件开发
|
|
globs:
|
|
- "frontend/**/*.vue"
|
|
- "frontend/**/*.ts"
|
|
alwaysApply: false
|
|
---
|
|
|
|
# 前端架构规范
|
|
|
|
## 组件结构
|
|
|
|
### 目录组织
|
|
|
|
- 页面组件放在 `views/` 目录下,按模块组织
|
|
- 公共组件放在 `components/` 目录下
|
|
- 组件命名使用 PascalCase
|
|
|
|
### 组件语法
|
|
|
|
使用 Vue 3 Composition API 的 `<script setup lang="ts">` 语法:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from "vue";
|
|
import { message } from "ant-design-vue";
|
|
import { getUsers, type User } from "@/api/users";
|
|
|
|
const loading = ref(false);
|
|
const users = ref<User[]>([]);
|
|
|
|
const activeUsers = computed(() =>
|
|
users.value.filter((u) => u.validState === 1)
|
|
);
|
|
|
|
onMounted(async () => {
|
|
await fetchUsers();
|
|
});
|
|
|
|
const fetchUsers = async () => {
|
|
try {
|
|
loading.value = true;
|
|
users.value = await getUsers();
|
|
} catch (error) {
|
|
message.error("获取用户列表失败");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="p-4">
|
|
<a-spin :spinning="loading">
|
|
<a-table :dataSource="activeUsers" />
|
|
</a-spin>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## API 调用规范
|
|
|
|
### 目录结构
|
|
|
|
所有 API 调用放在 `api/` 目录下,按模块组织:
|
|
|
|
```
|
|
api/
|
|
├── users.ts
|
|
├── roles.ts
|
|
├── contests.ts
|
|
└── auth.ts
|
|
```
|
|
|
|
### API 函数命名
|
|
|
|
- `getXxx` - 获取数据
|
|
- `createXxx` - 创建数据
|
|
- `updateXxx` - 更新数据
|
|
- `deleteXxx` - 删除数据
|
|
|
|
### 示例
|
|
|
|
```typescript
|
|
// api/users.ts
|
|
import request from "@/utils/request";
|
|
|
|
export interface User {
|
|
id: number;
|
|
username: string;
|
|
nickname: string;
|
|
email?: string;
|
|
validState: number;
|
|
}
|
|
|
|
export interface CreateUserDto {
|
|
username: string;
|
|
password: string;
|
|
nickname: string;
|
|
email?: string;
|
|
roleIds?: number[];
|
|
}
|
|
|
|
export const getUsers = (params?: { skip?: number; take?: number }) => {
|
|
return request.get<User[]>("/users", { params });
|
|
};
|
|
|
|
export const createUser = (data: CreateUserDto) => {
|
|
return request.post<User>("/users", data);
|
|
};
|
|
|
|
export const updateUser = (id: number, data: Partial<CreateUserDto>) => {
|
|
return request.put<User>(`/users/${id}`, data);
|
|
};
|
|
|
|
export const deleteUser = (id: number) => {
|
|
return request.delete(`/users/${id}`);
|
|
};
|
|
```
|
|
|
|
## 状态管理 (Pinia)
|
|
|
|
### Store 规范
|
|
|
|
- Store 文件放在 `stores/` 目录下
|
|
- 使用 `defineStore()` 定义 store
|
|
- Store 命名使用 camelCase + Store 后缀
|
|
|
|
### 示例
|
|
|
|
```typescript
|
|
// stores/auth.ts
|
|
import { defineStore } from "pinia";
|
|
import { ref, computed } from "vue";
|
|
import { login, getUserInfo, type LoginDto, type User } from "@/api/auth";
|
|
|
|
export const useAuthStore = defineStore("auth", () => {
|
|
const token = ref<string | null>(localStorage.getItem("token"));
|
|
const user = ref<User | null>(null);
|
|
const menus = ref<any[]>([]);
|
|
|
|
const isAuthenticated = computed(() => !!token.value && !!user.value);
|
|
|
|
const loginAction = async (loginDto: LoginDto) => {
|
|
const {
|
|
accessToken,
|
|
user: userInfo,
|
|
menus: userMenus,
|
|
} = await login(loginDto);
|
|
token.value = accessToken;
|
|
user.value = userInfo;
|
|
menus.value = userMenus;
|
|
localStorage.setItem("token", accessToken);
|
|
};
|
|
|
|
const logout = () => {
|
|
token.value = null;
|
|
user.value = null;
|
|
menus.value = [];
|
|
localStorage.removeItem("token");
|
|
};
|
|
|
|
return {
|
|
token,
|
|
user,
|
|
menus,
|
|
isAuthenticated,
|
|
loginAction,
|
|
logout,
|
|
};
|
|
});
|
|
```
|
|
|
|
## 路由管理
|
|
|
|
### 路由规范
|
|
|
|
- 路由配置在 `router/index.ts`
|
|
- 支持动态路由(基于菜单权限)
|
|
- 路由路径包含租户编码:`/:tenantCode/xxx`
|
|
- 路由 meta 包含权限信息
|
|
|
|
### 示例
|
|
|
|
```typescript
|
|
{
|
|
path: '/:tenantCode/users',
|
|
name: 'Users',
|
|
component: () => import('@/views/users/Index.vue'),
|
|
meta: {
|
|
requiresAuth: true,
|
|
permissions: ['user:read'],
|
|
roles: ['admin'],
|
|
},
|
|
}
|
|
```
|
|
|
|
## 表单验证
|
|
|
|
### 使用 VeeValidate + Zod
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { useForm } from "vee-validate";
|
|
import { toTypedSchema } from "@vee-validate/zod";
|
|
import * as z from "zod";
|
|
|
|
const schema = z.object({
|
|
username: z.string().min(3, "用户名至少3个字符"),
|
|
password: z.string().min(6, "密码至少6个字符"),
|
|
email: z.string().email("邮箱格式不正确").optional(),
|
|
});
|
|
|
|
const { defineField, handleSubmit, errors } = useForm({
|
|
validationSchema: toTypedSchema(schema),
|
|
});
|
|
|
|
const [username] = defineField("username");
|
|
const [password] = defineField("password");
|
|
const [email] = defineField("email");
|
|
|
|
const onSubmit = handleSubmit(async (values) => {
|
|
try {
|
|
await createUser(values);
|
|
message.success("创建成功");
|
|
} catch (error) {
|
|
message.error("创建失败");
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<a-form @submit.prevent="onSubmit">
|
|
<a-form-item
|
|
:help="errors.username"
|
|
:validateStatus="errors.username ? 'error' : ''"
|
|
>
|
|
<a-input v-model:value="username" placeholder="用户名" />
|
|
</a-form-item>
|
|
<a-form-item
|
|
:help="errors.password"
|
|
:validateStatus="errors.password ? 'error' : ''"
|
|
>
|
|
<a-input-password v-model:value="password" placeholder="密码" />
|
|
</a-form-item>
|
|
<a-button type="primary" html-type="submit">提交</a-button>
|
|
</a-form>
|
|
</template>
|
|
```
|
|
|
|
## UI 组件规范
|
|
|
|
### Ant Design Vue
|
|
|
|
- 使用 Ant Design Vue 组件库
|
|
- 遵循 Ant Design 设计规范
|
|
|
|
### 样式
|
|
|
|
- 使用 Tailwind CSS 工具类
|
|
- 复杂样式使用 SCSS
|
|
- 响应式设计,移动端优先
|
|
|
|
### 状态管理
|
|
|
|
组件必须有 loading 和 error 状态:
|
|
|
|
```vue
|
|
<template>
|
|
<div class="p-4">
|
|
<a-spin :spinning="loading">
|
|
<a-alert
|
|
v-if="error"
|
|
type="error"
|
|
:message="error"
|
|
closable
|
|
@close="error = null"
|
|
/>
|
|
<div v-else>
|
|
<!-- 内容 -->
|
|
</div>
|
|
</a-spin>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## TypeScript 类型定义
|
|
|
|
### 类型文件组织
|
|
|
|
- TypeScript 类型定义放在 `types/` 目录下
|
|
- 接口类型使用 `interface`
|
|
- 数据模型使用 `type`
|
|
- 导出类型供其他模块使用
|
|
|
|
### 示例
|
|
|
|
```typescript
|
|
// types/user.ts
|
|
export interface User {
|
|
id: number;
|
|
username: string;
|
|
nickname: string;
|
|
email?: string;
|
|
tenantId: number;
|
|
validState: number;
|
|
createTime: string;
|
|
modifyTime: string;
|
|
}
|
|
|
|
export type CreateUserParams = Omit<
|
|
User,
|
|
"id" | "tenantId" | "createTime" | "modifyTime"
|
|
>;
|
|
|
|
export type UserRole = {
|
|
userId: number;
|
|
roleId: number;
|
|
role: Role;
|
|
};
|
|
```
|
|
|
|
## 性能优化
|
|
|
|
### 路由懒加载
|
|
|
|
```typescript
|
|
const routes = [
|
|
{
|
|
path: "/users",
|
|
component: () => import("@/views/users/Index.vue"),
|
|
},
|
|
];
|
|
```
|
|
|
|
### 组件按需加载
|
|
|
|
```typescript
|
|
import { defineAsyncComponent } from "vue";
|
|
|
|
const AsyncComponent = defineAsyncComponent(
|
|
() => import("@/components/HeavyComponent.vue")
|
|
);
|
|
```
|
|
|
|
### 避免不必要的重新渲染
|
|
|
|
使用 `computed`、`watchEffect` 和 `memo` 优化性能。
|