Files
telegram-management-system/frontend-vben/apps/web-antd/docs/DEVELOPMENT.md
你的用户名 237c7802e5
Some checks failed
Deploy / deploy (push) Has been cancelled
Initial commit: Telegram Management System
Full-stack web application for Telegram management
- Frontend: Vue 3 + Vben Admin
- Backend: NestJS
- Features: User management, group broadcast, statistics

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 15:37:50 +08:00

2867 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Telegram 管理系统开发规范文档
> **版本**: v2.0.0
> **更新时间**: 2024-01-20
> **维护者**: 开发团队
## 📋 文档目录
- [开发环境配置](#开发环境配置)
- [代码规范](#代码规范)
- [项目结构规范](#项目结构规范)
- [组件开发规范](#组件开发规范)
- [API开发规范](#api开发规范)
- [状态管理规范](#状态管理规范)
- [路由规范](#路由规范)
- [样式规范](#样式规范)
- [测试规范](#测试规范)
- [Git工作流](#git工作流)
- [代码审查](#代码审查)
- [性能优化规范](#性能优化规范)
---
## 🛠️ 开发环境配置
### 必备工具
#### 1. 基础开发工具
```bash
# Node.js版本管理
nvm install 18.19.0
nvm use 18.19.0
# 包管理器
npm install -g pnpm@8.15.0
# Git配置
git config --global user.name "Your Name"
git config --global user.email "your.email@company.com"
git config --global init.defaultBranch main
```
#### 2. VSCode插件配置
推荐安装以下插件:
```json
{
"recommendations": [
"vue.volar",
"vue.vscode-typescript-vue-plugin",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-next",
"formulahendry.auto-rename-tag",
"christian-kohler.path-intellisense",
"ms-vscode.vscode-json",
"ms-playwright.playwright"
]
}
```
#### 3. VSCode设置
```json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.suggest.autoImports": true,
"vue.inlayHints.missingProps": true,
"vue.inlayHints.inlineHandlerLeading": true,
"vue.inlayHints.optionsWrapper": true
}
```
### 项目环境配置
#### 1. 环境变量设置
```bash
# 开发环境配置
cp .env.development.example .env.development
# 本地环境配置
cp .env.local.example .env.local
```
#### 2. 依赖安装
```bash
# 安装项目依赖
pnpm install
# 安装Playwright浏览器
pnpm test:install
# 验证环境
pnpm typecheck
pnpm lint
pnpm test:unit
```
---
## 📝 代码规范
### TypeScript规范
#### 1. 类型定义
```typescript
// ✅ 推荐使用interface定义对象类型
interface UserInfo {
id: string;
username: string;
email: string;
profile?: UserProfile;
}
// ✅ 推荐使用type定义联合类型
type UserStatus = 'active' | 'inactive' | 'suspended';
// ✅ 推荐:使用泛型增强类型安全
interface ApiResponse<T> {
success: boolean;
data: T;
message: string;
}
// ❌ 避免使用any类型
const userData: any = {}; // 不推荐
// ✅ 推荐:使用具体类型
const userData: UserInfo = {
id: '123',
username: 'admin',
email: 'admin@example.com',
};
```
#### 2. 函数定义
```typescript
// ✅ 推荐:明确的参数和返回类型
async function fetchUserData(userId: string): Promise<UserInfo> {
const response = await api.get<ApiResponse<UserInfo>>(`/users/${userId}`);
return response.data.data;
}
// ✅ 推荐:使用类型守卫
function isValidUser(user: unknown): user is UserInfo {
return (
typeof user === 'object' &&
user !== null &&
typeof (user as UserInfo).id === 'string' &&
typeof (user as UserInfo).username === 'string'
);
}
// ✅ 推荐:解构参数使用类型注解
function updateUser({
id,
username,
email,
}: Pick<UserInfo, 'id' | 'username' | 'email'>): Promise<void> {
// 实现逻辑
}
```
#### 3. 组合式API类型定义
```typescript
// ✅ 推荐:为组合式函数定义返回类型
interface UseUserReturn {
userInfo: Ref<UserInfo | null>;
loading: Ref<boolean>;
error: Ref<string | null>;
fetchUser: (id: string) => Promise<void>;
updateUser: (data: Partial<UserInfo>) => Promise<void>;
}
function useUser(): UseUserReturn {
const userInfo = ref<UserInfo | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const fetchUser = async (id: string): Promise<void> => {
// 实现逻辑
};
const updateUser = async (data: Partial<UserInfo>): Promise<void> => {
// 实现逻辑
};
return {
userInfo,
loading,
error,
fetchUser,
updateUser,
};
}
```
### ESLint规范
#### 1. 基础规则
```javascript
// .eslintrc.js
module.exports = {
extends: [
'@vben/eslint-config',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier',
],
rules: {
// 变量命名
camelcase: ['error', { properties: 'never' }],
'no-var': 'error',
'prefer-const': 'error',
// 函数规范
'prefer-arrow-callback': 'error',
'arrow-spacing': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
// Vue特定规则
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
'vue/define-props-declaration': ['error', 'type-based'],
'vue/define-emits-declaration': ['error', 'type-based'],
'vue/no-v-html': 'warn',
// TypeScript特定规则
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
},
};
```
### Prettier规范
```javascript
// .prettierrc.js
module.exports = {
semi: true,
singleQuote: true,
trailingComma: 'es5',
tabWidth: 2,
useTabs: false,
printWidth: 100,
bracketSpacing: true,
arrowParens: 'avoid',
endOfLine: 'lf',
vueIndentScriptAndStyle: false,
htmlWhitespaceSensitivity: 'ignore',
};
```
### 命名规范
#### 1. 文件命名
```bash
# ✅ 推荐组件使用PascalCase
UserProfile.vue
AccountList.vue
MessageTemplate.vue
# ✅ 推荐工具函数使用kebab-case
user-utils.ts
api-client.ts
validation-rules.ts
# ✅ 推荐页面文件使用kebab-case
user-list.vue
account-management.vue
private-message.vue
# ✅ 推荐:类型定义文件
types/user.ts
types/api.ts
types/common.ts
```
#### 2. 变量和函数命名
```typescript
// ✅ 推荐变量使用camelCase
const userName = 'admin';
const isUserActive = true;
const userAccountList = [];
// ✅ 推荐常量使用SCREAMING_SNAKE_CASE
const API_BASE_URL = 'https://api.example.com';
const MAX_RETRY_COUNT = 3;
const DEFAULT_PAGE_SIZE = 20;
// ✅ 推荐函数使用动词开头的camelCase
function getUserInfo() {}
function validateUserInput() {}
function handleUserLogin() {}
// ✅ 推荐布尔值使用is/has/can/should开头
const isLoading = false;
const hasPermission = true;
const canEdit = false;
const shouldUpdate = true;
// ✅ 推荐事件处理函数使用handle/on开头
const handleClick = () => {};
const onUserSelect = () => {};
const handleFormSubmit = () => {};
```
#### 3. 组件命名
```typescript
// ✅ 推荐组件使用PascalCase
export default defineComponent({
name: 'UserProfileCard',
// ...
});
// ✅ 推荐组件props使用camelCase
interface Props {
userId: string;
showAvatar?: boolean;
onUserClick?: (user: UserInfo) => void;
}
// ✅ 推荐组件emits使用kebab-case
const emit = defineEmits<{
'user-select': [user: UserInfo];
'status-change': [status: UserStatus];
}>();
```
---
## 🏗️ 项目结构规范
### 目录结构
```
src/
├── api/ # API接口层
│ ├── modules/ # 按模块分组的API
│ │ ├── auth.ts
│ │ ├── user.ts
│ │ └── message.ts
│ ├── types/ # API相关类型定义
│ └── client.ts # API客户端配置
├── assets/ # 静态资源
│ ├── images/
│ ├── icons/
│ └── styles/
├── components/ # 全局组件
│ ├── UI/ # 基础UI组件
│ ├── Business/ # 业务组件
│ └── Layout/ # 布局组件
├── composables/ # 组合式函数
│ ├── useUser.ts
│ ├── usePermission.ts
│ └── useApi.ts
├── directives/ # 自定义指令
├── hooks/ # React风格的hooks向前兼容
├── layouts/ # 布局组件
├── locales/ # 国际化资源
│ ├── lang/
│ │ ├── zh-CN/
│ │ └── en-US/
│ └── index.ts
├── router/ # 路由配置
│ ├── modules/
│ ├── guards/
│ └── index.ts
├── stores/ # 状态管理
│ ├── modules/
│ │ ├── user.ts
│ │ ├── auth.ts
│ │ └── permission.ts
│ └── index.ts
├── types/ # 全局类型定义
│ ├── api.ts
│ ├── user.ts
│ └── global.ts
├── utils/ # 工具函数
│ ├── request.ts
│ ├── validation.ts
│ └── format.ts
├── views/ # 页面组件
│ ├── auth/
│ ├── dashboard/
│ └── user/
├── App.vue
└── main.ts
```
### 文件命名约定
#### 1. 组件文件
```bash
# 页面组件 - PascalCase
UserList.vue
AccountManagement.vue
PrivateMessageSend.vue
# 业务组件 - PascalCase
UserCard.vue
MessageTemplate.vue
StatisticsChart.vue
# 基础组件 - 带前缀的PascalCase
BaseButton.vue
BaseTable.vue
BaseForm.vue
```
#### 2. 工具文件
```bash
# 工具函数 - kebab-case
api-client.ts
user-utils.ts
validation-rules.ts
date-format.ts
# 类型定义 - kebab-case
user-types.ts
api-types.ts
common-types.ts
# 常量文件 - kebab-case
api-constants.ts
app-constants.ts
```
#### 3. 存储和组合式函数
```bash
# Store文件 - camelCase
userStore.ts
authStore.ts
permissionStore.ts
# 组合式函数 - camelCase with use前缀
useUser.ts
useAuth.ts
usePermission.ts
useApi.ts
```
---
## 🧩 组件开发规范
### Vue 3 组合式API规范
#### 1. 组件结构
```vue
<template>
<div class="user-profile-card">
<!-- 使用语义化的HTML标签 -->
<header class="user-profile-card__header">
<h3 class="user-profile-card__title">{{ userInfo?.username }}</h3>
</header>
<main class="user-profile-card__body">
<!-- 内容区域 -->
</main>
<footer class="user-profile-card__footer" v-if="showActions">
<!-- 操作区域 -->
</footer>
</div>
</template>
<script setup lang="ts">
// 1. 类型导入
import type { UserInfo } from '@/types/user';
// 2. 组件Props定义
interface Props {
userInfo: UserInfo | null;
showActions?: boolean;
readonly?: boolean;
}
// 3. 组件Emits定义
interface Emits {
(e: 'user-click', user: UserInfo): void;
(e: 'action-click', action: string, user: UserInfo): void;
}
// 4. Props和Emits声明
const props = withDefaults(defineProps<Props>(), {
showActions: true,
readonly: false,
});
const emit = defineEmits<Emits>();
// 5. 组合式函数调用
const { t } = useI18n();
const { hasPermission } = usePermission();
// 6. 响应式数据
const loading = ref(false);
const error = ref<string | null>(null);
// 7. 计算属性
const canEdit = computed(() => !props.readonly && hasPermission('user:update'));
const displayName = computed(
() =>
props.userInfo?.profile?.displayName ||
props.userInfo?.username ||
'Unknown',
);
// 8. 方法定义
const handleUserClick = () => {
if (props.userInfo) {
emit('user-click', props.userInfo);
}
};
const handleActionClick = (action: string) => {
if (props.userInfo) {
emit('action-click', action, props.userInfo);
}
};
// 9. 生命周期钩子
onMounted(() => {
console.log('UserProfileCard mounted');
});
// 10. 对外暴露的方法(如果需要)
defineExpose({
refresh: () => {
// 刷新逻辑
},
});
</script>
<style lang="scss" scoped>
.user-profile-card {
@apply rounded-lg border border-gray-200 bg-white shadow-sm;
&__header {
@apply border-b border-gray-100 p-4;
}
&__title {
@apply text-lg font-semibold text-gray-900;
}
&__body {
@apply p-4;
}
&__footer {
@apply flex justify-end space-x-2 border-t border-gray-100 p-4;
}
}
</style>
```
#### 2. 组合式函数规范
```typescript
// composables/useUser.ts
import { ref, computed, type Ref } from 'vue';
import type { UserInfo, CreateUserData, UpdateUserData } from '@/types/user';
import { userApi } from '@/api/modules/user';
export interface UseUserOptions {
immediate?: boolean;
}
export interface UseUserReturn {
// 状态
userList: Ref<UserInfo[]>;
currentUser: Ref<UserInfo | null>;
loading: Ref<boolean>;
error: Ref<string | null>;
// 计算属性
activeUsers: ComputedRef<UserInfo[]>;
userCount: ComputedRef<number>;
// 方法
fetchUsers: (params?: UserListParams) => Promise<void>;
fetchUser: (id: string) => Promise<UserInfo | null>;
createUser: (data: CreateUserData) => Promise<UserInfo>;
updateUser: (id: string, data: UpdateUserData) => Promise<UserInfo>;
deleteUser: (id: string) => Promise<void>;
refresh: () => Promise<void>;
}
export function useUser(options: UseUserOptions = {}): UseUserReturn {
// 响应式状态
const userList = ref<UserInfo[]>([]);
const currentUser = ref<UserInfo | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
// 计算属性
const activeUsers = computed(() =>
userList.value.filter((user) => user.status === 'active'),
);
const userCount = computed(() => userList.value.length);
// 私有方法
const setError = (message: string) => {
error.value = message;
console.error('[useUser]', message);
};
const clearError = () => {
error.value = null;
};
// 公共方法
const fetchUsers = async (params?: UserListParams): Promise<void> => {
try {
loading.value = true;
clearError();
const response = await userApi.getList(params);
userList.value = response.data.items;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch users');
} finally {
loading.value = false;
}
};
const fetchUser = async (id: string): Promise<UserInfo | null> => {
try {
loading.value = true;
clearError();
const response = await userApi.getById(id);
currentUser.value = response.data;
return response.data;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch user');
return null;
} finally {
loading.value = false;
}
};
const createUser = async (data: CreateUserData): Promise<UserInfo> => {
try {
loading.value = true;
clearError();
const response = await userApi.create(data);
userList.value.push(response.data);
return response.data;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create user');
throw err;
} finally {
loading.value = false;
}
};
const updateUser = async (
id: string,
data: UpdateUserData,
): Promise<UserInfo> => {
try {
loading.value = true;
clearError();
const response = await userApi.update(id, data);
const index = userList.value.findIndex((user) => user.id === id);
if (index !== -1) {
userList.value[index] = response.data;
}
return response.data;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update user');
throw err;
} finally {
loading.value = false;
}
};
const deleteUser = async (id: string): Promise<void> => {
try {
loading.value = true;
clearError();
await userApi.delete(id);
const index = userList.value.findIndex((user) => user.id === id);
if (index !== -1) {
userList.value.splice(index, 1);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete user');
throw err;
} finally {
loading.value = false;
}
};
const refresh = async (): Promise<void> => {
await fetchUsers();
};
// 初始化
if (options.immediate) {
fetchUsers();
}
return {
// 状态
userList,
currentUser,
loading,
error,
// 计算属性
activeUsers,
userCount,
// 方法
fetchUsers,
fetchUser,
createUser,
updateUser,
deleteUser,
refresh,
};
}
```
#### 3. 组件通信规范
```typescript
// 父子组件通信 - Props和Emits
// 父组件
<template>
<UserList
:users="users"
:loading="loading"
@user-select="handleUserSelect"
@user-delete="handleUserDelete"
/>
</template>
// 子组件
interface Props {
users: UserInfo[];
loading?: boolean;
}
interface Emits {
(e: 'user-select', user: UserInfo): void;
(e: 'user-delete', id: string): void;
}
// 兄弟组件通信 - 使用Store
const userStore = useUserStore();
const selectedUser = computed(() => userStore.selectedUser);
// 跨层级通信 - Provide/Inject
// 祖先组件
provide('user-context', {
currentUser,
permissions,
updateUser
});
// 后代组件
const userContext = inject<UserContext>('user-context');
```
---
## 🌐 API开发规范
### API客户端配置
```typescript
// api/client.ts
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
import { useAuthStore } from '@/stores/modules/auth';
import { message } from 'ant-design-vue';
class ApiClient {
private instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
const authStore = useAuthStore();
const token = authStore.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 添加请求ID用于追踪
config.headers['X-Request-ID'] = this.generateRequestId();
return config;
},
(error) => {
console.error('[API Request Error]', error);
return Promise.reject(error);
},
);
// 响应拦截器
this.instance.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
return this.handleError(error);
},
);
}
private handleError(error: any) {
const { response } = error;
if (!response) {
message.error('网络连接失败,请检查网络设置');
return Promise.reject(new Error('Network Error'));
}
const { status, data } = response;
switch (status) {
case 401:
this.handleUnauthorized();
break;
case 403:
message.error('权限不足,无法访问该资源');
break;
case 404:
message.error('请求的资源不存在');
break;
case 422:
this.handleValidationError(data);
break;
case 500:
message.error('服务器内部错误,请稍后重试');
break;
default:
message.error(data?.message || '请求失败,请稍后重试');
}
return Promise.reject(error);
}
private handleUnauthorized() {
const authStore = useAuthStore();
authStore.logout();
// 重定向到登录页
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
}
private handleValidationError(data: any) {
if (data?.errors && Array.isArray(data.errors)) {
data.errors.forEach((error: any) => {
message.error(`${error.field}: ${error.message}`);
});
} else {
message.error(data?.message || '数据验证失败');
}
}
private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// HTTP方法封装
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.get(url, config);
}
post<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Promise<T> {
return this.instance.post(url, data, config);
}
put<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Promise<T> {
return this.instance.put(url, data, config);
}
patch<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Promise<T> {
return this.instance.patch(url, data, config);
}
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.delete(url, config);
}
}
export const apiClient = new ApiClient();
```
### API模块定义
```typescript
// api/modules/user.ts
import { apiClient } from '../client';
import type {
UserInfo,
UserListParams,
UserListResponse,
CreateUserData,
UpdateUserData,
} from '@/types/user';
export const userApi = {
/**
* 获取用户列表
*/
getList(params?: UserListParams): Promise<UserListResponse> {
return apiClient.get('/users', { params });
},
/**
* 获取用户详情
*/
getById(id: string): Promise<{ data: UserInfo }> {
return apiClient.get(`/users/${id}`);
},
/**
* 创建用户
*/
create(data: CreateUserData): Promise<{ data: UserInfo }> {
return apiClient.post('/users', data);
},
/**
* 更新用户
*/
update(id: string, data: UpdateUserData): Promise<{ data: UserInfo }> {
return apiClient.put(`/users/${id}`, data);
},
/**
* 删除用户
*/
delete(id: string): Promise<void> {
return apiClient.delete(`/users/${id}`);
},
/**
* 批量操作用户
*/
batchAction(
action: string,
userIds: string[],
data?: any,
): Promise<{ data: { success: number; failed: number } }> {
return apiClient.post('/users/batch', {
action,
userIds,
data,
});
},
/**
* 上传用户头像
*/
uploadAvatar(id: string, file: File): Promise<{ data: { url: string } }> {
const formData = new FormData();
formData.append('avatar', file);
return apiClient.post(`/users/${id}/avatar`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
},
};
```
### API类型定义
```typescript
// types/user.ts
export interface UserInfo {
id: string;
username: string;
email: string;
profile: UserProfile;
roles: string[];
status: UserStatus;
createdAt: string;
updatedAt: string;
}
export interface UserProfile {
firstName: string;
lastName: string;
avatar?: string;
phone?: string;
bio?: string;
}
export type UserStatus = 'active' | 'inactive' | 'suspended';
export interface UserListParams {
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
filters?: {
status?: UserStatus;
roles?: string[];
username?: string;
email?: string;
createdAt?: {
start?: string;
end?: string;
};
};
}
export interface UserListResponse {
success: boolean;
data: {
items: UserInfo[];
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
};
}
export interface CreateUserData {
username: string;
email: string;
password: string;
profile: Omit<UserProfile, 'avatar'>;
roles?: string[];
}
export interface UpdateUserData {
username?: string;
email?: string;
profile?: Partial<UserProfile>;
roles?: string[];
status?: UserStatus;
}
```
---
## 📦 状态管理规范
### Pinia Store规范
```typescript
// stores/modules/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { UserInfo, UserListParams } from '@/types/user';
import { userApi } from '@/api/modules/user';
export const useUserStore = defineStore('user', () => {
// ==================== 状态定义 ====================
const userList = ref<UserInfo[]>([]);
const currentUser = ref<UserInfo | null>(null);
const selectedUsers = ref<UserInfo[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
// 分页信息
const pagination = ref({
page: 1,
pageSize: 20,
total: 0,
totalPages: 0,
});
// 筛选条件
const filters = ref<UserListParams['filters']>({});
// ==================== 计算属性 ====================
const activeUsers = computed(() =>
userList.value.filter((user) => user.status === 'active'),
);
const userCount = computed(() => userList.value.length);
const hasSelectedUsers = computed(() => selectedUsers.value.length > 0);
const selectedUserIds = computed(() =>
selectedUsers.value.map((user) => user.id),
);
// ==================== 私有方法 ====================
const setError = (message: string) => {
error.value = message;
console.error('[UserStore]', message);
};
const clearError = () => {
error.value = null;
};
// ==================== Actions ====================
/**
* 获取用户列表
*/
const fetchUsers = async (params?: UserListParams) => {
try {
loading.value = true;
clearError();
const mergedParams = {
...params,
filters: { ...filters.value, ...params?.filters },
};
const response = await userApi.getList(mergedParams);
userList.value = response.data.items;
pagination.value = response.data.pagination;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch users');
throw err;
} finally {
loading.value = false;
}
};
/**
* 获取用户详情
*/
const fetchUser = async (id: string): Promise<UserInfo | null> => {
try {
loading.value = true;
clearError();
const response = await userApi.getById(id);
currentUser.value = response.data;
return response.data;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch user');
return null;
} finally {
loading.value = false;
}
};
/**
* 创建用户
*/
const createUser = async (data: CreateUserData): Promise<UserInfo> => {
try {
loading.value = true;
clearError();
const response = await userApi.create(data);
// 如果当前页面能容纳新用户,则添加到列表
if (userList.value.length < pagination.value.pageSize) {
userList.value.push(response.data);
}
// 更新总数
pagination.value.total += 1;
return response.data;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create user');
throw err;
} finally {
loading.value = false;
}
};
/**
* 更新用户
*/
const updateUser = async (
id: string,
data: UpdateUserData,
): Promise<UserInfo> => {
try {
loading.value = true;
clearError();
const response = await userApi.update(id, data);
// 更新列表中的用户
const index = userList.value.findIndex((user) => user.id === id);
if (index !== -1) {
userList.value[index] = response.data;
}
// 更新当前用户
if (currentUser.value?.id === id) {
currentUser.value = response.data;
}
return response.data;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update user');
throw err;
} finally {
loading.value = false;
}
};
/**
* 删除用户
*/
const deleteUser = async (id: string): Promise<void> => {
try {
loading.value = true;
clearError();
await userApi.delete(id);
// 从列表中移除
const index = userList.value.findIndex((user) => user.id === id);
if (index !== -1) {
userList.value.splice(index, 1);
}
// 从选中列表中移除
const selectedIndex = selectedUsers.value.findIndex(
(user) => user.id === id,
);
if (selectedIndex !== -1) {
selectedUsers.value.splice(selectedIndex, 1);
}
// 清除当前用户
if (currentUser.value?.id === id) {
currentUser.value = null;
}
// 更新总数
pagination.value.total -= 1;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete user');
throw err;
} finally {
loading.value = false;
}
};
/**
* 批量删除用户
*/
const batchDeleteUsers = async (userIds: string[]): Promise<void> => {
try {
loading.value = true;
clearError();
const response = await userApi.batchAction('delete', userIds);
// 从列表中移除成功删除的用户
userList.value = userList.value.filter(
(user) => !userIds.includes(user.id),
);
// 清空选中列表
selectedUsers.value = [];
// 更新总数
pagination.value.total -= response.data.success;
} catch (err) {
setError(
err instanceof Error ? err.message : 'Failed to batch delete users',
);
throw err;
} finally {
loading.value = false;
}
};
/**
* 选择用户
*/
const selectUser = (user: UserInfo) => {
const index = selectedUsers.value.findIndex((u) => u.id === user.id);
if (index === -1) {
selectedUsers.value.push(user);
}
};
/**
* 取消选择用户
*/
const unselectUser = (userId: string) => {
const index = selectedUsers.value.findIndex((user) => user.id === userId);
if (index !== -1) {
selectedUsers.value.splice(index, 1);
}
};
/**
* 切换用户选择状态
*/
const toggleUserSelection = (user: UserInfo) => {
const index = selectedUsers.value.findIndex((u) => u.id === user.id);
if (index === -1) {
selectedUsers.value.push(user);
} else {
selectedUsers.value.splice(index, 1);
}
};
/**
* 全选/取消全选
*/
const toggleSelectAll = () => {
if (selectedUsers.value.length === userList.value.length) {
selectedUsers.value = [];
} else {
selectedUsers.value = [...userList.value];
}
};
/**
* 清空选择
*/
const clearSelection = () => {
selectedUsers.value = [];
};
/**
* 更新筛选条件
*/
const updateFilters = (newFilters: UserListParams['filters']) => {
filters.value = { ...filters.value, ...newFilters };
};
/**
* 重置筛选条件
*/
const resetFilters = () => {
filters.value = {};
};
/**
* 刷新当前页面
*/
const refresh = async () => {
await fetchUsers({
page: pagination.value.page,
pageSize: pagination.value.pageSize,
filters: filters.value,
});
};
/**
* 重置所有状态
*/
const resetState = () => {
userList.value = [];
currentUser.value = null;
selectedUsers.value = [];
loading.value = false;
error.value = null;
pagination.value = {
page: 1,
pageSize: 20,
total: 0,
totalPages: 0,
};
filters.value = {};
};
return {
// 状态
userList,
currentUser,
selectedUsers,
loading,
error,
pagination,
filters,
// 计算属性
activeUsers,
userCount,
hasSelectedUsers,
selectedUserIds,
// Actions
fetchUsers,
fetchUser,
createUser,
updateUser,
deleteUser,
batchDeleteUsers,
selectUser,
unselectUser,
toggleUserSelection,
toggleSelectAll,
clearSelection,
updateFilters,
resetFilters,
refresh,
resetState,
};
});
```
### Store组合使用
```typescript
// stores/index.ts
import type { App } from 'vue';
import { createPinia } from 'pinia';
// 创建pinia实例
export const pinia = createPinia();
// 插件安装
export function setupStore(app: App) {
app.use(pinia);
}
// 导出所有store
export { useAuthStore } from './modules/auth';
export { useUserStore } from './modules/user';
export { usePermissionStore } from './modules/permission';
export { useSettingStore } from './modules/setting';
```
---
## 🛣️ 路由规范
### 路由配置
```typescript
// router/modules/user.ts
import type { RouteRecordRaw } from 'vue-router';
const userRoutes: RouteRecordRaw[] = [
{
path: '/user',
name: 'User',
redirect: '/user/list',
component: () => import('@/layouts/default/index.vue'),
meta: {
title: 'route.user.title',
icon: 'mdi:account-group',
orderNo: 10,
requiresAuth: true,
permissions: ['user:read'],
},
children: [
{
path: 'list',
name: 'UserList',
component: () => import('@/views/user/list/index.vue'),
meta: {
title: 'route.user.list',
icon: 'mdi:account-multiple',
permissions: ['user:read'],
keepAlive: true,
},
},
{
path: 'create',
name: 'UserCreate',
component: () => import('@/views/user/create/index.vue'),
meta: {
title: 'route.user.create',
icon: 'mdi:account-plus',
permissions: ['user:create'],
hideInMenu: true,
},
},
{
path: 'edit/:id',
name: 'UserEdit',
component: () => import('@/views/user/edit/index.vue'),
meta: {
title: 'route.user.edit',
permissions: ['user:update'],
hideInMenu: true,
activeMenu: '/user/list',
},
},
{
path: 'detail/:id',
name: 'UserDetail',
component: () => import('@/views/user/detail/index.vue'),
meta: {
title: 'route.user.detail',
permissions: ['user:read'],
hideInMenu: true,
activeMenu: '/user/list',
},
},
],
},
];
export default userRoutes;
```
### 路由守卫
```typescript
// router/guards/permission.ts
import type { Router } from 'vue-router';
import { useAuthStore } from '@/stores/modules/auth';
import { usePermissionStore } from '@/stores/modules/permission';
export function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore();
const permissionStore = usePermissionStore();
// 1. 检查登录状态
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
next({
path: '/login',
query: { redirect: to.fullPath },
});
return;
}
// 2. 检查权限
if (to.meta.permissions && to.meta.permissions.length > 0) {
const hasPermission = to.meta.permissions.some((permission: string) =>
permissionStore.hasPermission(permission),
);
if (!hasPermission) {
next({ path: '/403' });
return;
}
}
// 3. 动态加载用户权限(如果还未加载)
if (authStore.isLoggedIn && !permissionStore.isPermissionsLoaded) {
try {
await permissionStore.loadPermissions();
// 重新检查权限
if (to.meta.permissions && to.meta.permissions.length > 0) {
const hasPermission = to.meta.permissions.some((permission: string) =>
permissionStore.hasPermission(permission),
);
if (!hasPermission) {
next({ path: '/403' });
return;
}
}
} catch (error) {
console.error('Failed to load permissions:', error);
next({ path: '/login' });
return;
}
}
next();
});
}
```
### 动态路由
```typescript
// router/dynamic.ts
import type { RouteRecordRaw } from 'vue-router';
import { usePermissionStore } from '@/stores/modules/permission';
/**
* 根据权限过滤路由
*/
export function filterRoutesByPermission(
routes: RouteRecordRaw[],
permissions: string[],
): RouteRecordRaw[] {
return routes.filter((route) => {
// 检查当前路由权限
if (route.meta?.permissions) {
const hasPermission = route.meta.permissions.some((permission: string) =>
permissions.includes(permission),
);
if (!hasPermission) {
return false;
}
}
// 递归处理子路由
if (route.children) {
route.children = filterRoutesByPermission(route.children, permissions);
}
return true;
});
}
/**
* 生成动态路由
*/
export async function generateDynamicRoutes(): Promise<RouteRecordRaw[]> {
const permissionStore = usePermissionStore();
// 获取用户权限
const permissions = permissionStore.permissions;
// 导入所有路由模块
const routeModules = import.meta.glob('./modules/*.ts', { eager: true });
let routes: RouteRecordRaw[] = [];
// 合并所有路由
Object.values(routeModules).forEach((module: any) => {
const moduleRoutes = module.default || module;
if (Array.isArray(moduleRoutes)) {
routes = routes.concat(moduleRoutes);
}
});
// 根据权限过滤路由
return filterRoutesByPermission(routes, permissions);
}
```
---
## 🎨 样式规范
### CSS架构
采用 `TailwindCSS + SCSS` 的混合方案:
```scss
// styles/variables.scss
// 颜色系统
:root {
// 主色调
--color-primary: #1890ff;
--color-primary-light: #40a9ff;
--color-primary-dark: #096dd9;
// 功能色
--color-success: #52c41a;
--color-warning: #faad14;
--color-error: #f5222d;
--color-info: #1890ff;
// 中性色
--color-text-primary: #262626;
--color-text-secondary: #595959;
--color-text-disabled: #bfbfbf;
// 背景色
--color-bg-primary: #ffffff;
--color-bg-secondary: #fafafa;
--color-bg-tertiary: #f5f5f5;
// 边框色
--color-border-primary: #d9d9d9;
--color-border-secondary: #f0f0f0;
// 阴影
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
// 圆角
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
// 间距
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
}
// 暗色主题
[data-theme='dark'] {
--color-bg-primary: #141414;
--color-bg-secondary: #1f1f1f;
--color-bg-tertiary: #262626;
--color-text-primary: #ffffff;
--color-text-secondary: #a6a6a6;
--color-border-primary: #434343;
--color-border-secondary: #303030;
}
```
### 组件样式规范
```scss
// 使用BEM命名规范 + TailwindCSS工具类
.user-profile-card {
@apply rounded-lg border border-gray-200 bg-white shadow-sm;
// 组件状态
&--loading {
@apply pointer-events-none opacity-50;
}
&--disabled {
@apply bg-gray-50 text-gray-400;
}
// 子元素
&__header {
@apply flex items-center justify-between border-b border-gray-100 p-4;
}
&__title {
@apply text-lg font-semibold text-gray-900;
}
&__subtitle {
@apply text-sm text-gray-500;
}
&__body {
@apply p-4;
}
&__actions {
@apply flex justify-end space-x-2 border-t border-gray-100 p-4;
}
// 修饰符
&--compact {
.user-profile-card__header,
.user-profile-card__body,
.user-profile-card__actions {
@apply p-2;
}
}
// 响应式
@screen md {
@apply p-6;
}
// 暗色主题
@apply dark:border-gray-700 dark:bg-gray-800;
&__title {
@apply dark:text-gray-100;
}
&__subtitle {
@apply dark:text-gray-400;
}
}
```
### 全局样式
```scss
// styles/global.scss
// 重置样式
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--color-text-primary);
background-color: var(--color-bg-primary);
}
// 工具类
.text-ellipsis {
@apply truncate;
}
.text-ellipsis-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.text-ellipsis-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
// 滚动条样式
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: var(--color-border-primary) transparent;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: var(--color-border-primary);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: var(--color-text-disabled);
}
}
// 动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter-from {
transform: translateY(20px);
opacity: 0;
}
.slide-up-leave-to {
transform: translateY(-20px);
opacity: 0;
}
```
### TailwindCSS配置
```javascript
// tailwind.config.js
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: ['class', '[data-theme="dark"]'],
theme: {
extend: {
colors: {
primary: {
50: '#e6f7ff',
100: '#bae7ff',
200: '#91d5ff',
300: '#69c0ff',
400: '#40a9ff',
500: '#1890ff',
600: '#096dd9',
700: '#0050b3',
800: '#003a8c',
900: '#002766',
},
gray: {
50: '#fafafa',
100: '#f5f5f5',
200: '#f0f0f0',
300: '#d9d9d9',
400: '#bfbfbf',
500: '#8c8c8c',
600: '#595959',
700: '#434343',
800: '#262626',
900: '#1f1f1f',
},
},
fontFamily: {
sans: [
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'Helvetica Neue',
'Arial',
'sans-serif',
],
mono: [
'SFMono-Regular',
'Menlo',
'Monaco',
'Consolas',
'Liberation Mono',
'Courier New',
'monospace',
],
},
spacing: {
18: '4.5rem',
88: '22rem',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
'bounce-in': 'bounceIn 0.6s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(20px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
bounceIn: {
'0%': { transform: 'scale(0.3)', opacity: '0' },
'50%': { transform: 'scale(1.05)' },
'70%': { transform: 'scale(0.9)' },
'100%': { transform: 'scale(1)', opacity: '1' },
},
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
],
};
```
---
## 🧪 测试规范
### 单元测试规范
```typescript
// tests/unit/composables/useUser.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useUser } from '@/composables/useUser';
import { userApi } from '@/api/modules/user';
// Mock API
vi.mock('@/api/modules/user', () => ({
userApi: {
getList: vi.fn(),
getById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
}));
describe('useUser', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should initialize with default values', () => {
const { userList, loading, error } = useUser();
expect(userList.value).toEqual([]);
expect(loading.value).toBe(false);
expect(error.value).toBeNull();
});
it('should fetch users successfully', async () => {
const mockUsers = [
{ id: '1', username: 'user1', email: 'user1@test.com' },
{ id: '2', username: 'user2', email: 'user2@test.com' },
];
vi.mocked(userApi.getList).mockResolvedValue({
success: true,
data: {
items: mockUsers,
pagination: { page: 1, pageSize: 20, total: 2, totalPages: 1 },
},
});
const { userList, fetchUsers, loading } = useUser();
await fetchUsers();
expect(loading.value).toBe(false);
expect(userList.value).toEqual(mockUsers);
expect(userApi.getList).toHaveBeenCalledOnce();
});
it('should handle fetch users error', async () => {
const errorMessage = 'Failed to fetch users';
vi.mocked(userApi.getList).mockRejectedValue(new Error(errorMessage));
const { fetchUsers, error } = useUser();
await expect(fetchUsers()).rejects.toThrow(errorMessage);
expect(error.value).toBe(errorMessage);
});
it('should create user successfully', async () => {
const newUser = { id: '3', username: 'user3', email: 'user3@test.com' };
const createData = {
username: 'user3',
email: 'user3@test.com',
password: '123456',
};
vi.mocked(userApi.create).mockResolvedValue({
success: true,
data: newUser,
});
const { userList, createUser } = useUser();
const result = await createUser(createData);
expect(result).toEqual(newUser);
expect(userList.value).toContain(newUser);
expect(userApi.create).toHaveBeenCalledWith(createData);
});
});
```
### 组件测试规范
```typescript
// tests/unit/components/UserCard.test.ts
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import UserCard from '@/components/UserCard.vue';
import type { UserInfo } from '@/types/user';
const mockUser: UserInfo = {
id: '1',
username: 'testuser',
email: 'test@example.com',
profile: {
firstName: 'Test',
lastName: 'User',
avatar: 'https://example.com/avatar.jpg',
},
roles: ['user'],
status: 'active',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
};
describe('UserCard', () => {
it('should render user information correctly', () => {
const wrapper = mount(UserCard, {
props: {
userInfo: mockUser,
},
});
expect(wrapper.find('.user-card__username').text()).toBe(mockUser.username);
expect(wrapper.find('.user-card__email').text()).toBe(mockUser.email);
expect(wrapper.find('.user-card__status').text()).toContain('active');
});
it('should emit user-click when card is clicked', async () => {
const wrapper = mount(UserCard, {
props: {
userInfo: mockUser,
},
});
await wrapper.find('.user-card').trigger('click');
expect(wrapper.emitted('user-click')).toBeTruthy();
expect(wrapper.emitted('user-click')?.[0]).toEqual([mockUser]);
});
it('should not show actions when readonly is true', () => {
const wrapper = mount(UserCard, {
props: {
userInfo: mockUser,
readonly: true,
},
});
expect(wrapper.find('.user-card__actions').exists()).toBe(false);
});
it('should show actions when readonly is false', () => {
const wrapper = mount(UserCard, {
props: {
userInfo: mockUser,
readonly: false,
showActions: true,
},
});
expect(wrapper.find('.user-card__actions').exists()).toBe(true);
});
it('should handle null user info gracefully', () => {
const wrapper = mount(UserCard, {
props: {
userInfo: null,
},
});
expect(wrapper.find('.user-card__username').text()).toBe('Unknown');
});
});
```
### E2E测试规范
```typescript
// tests/e2e/user-management.test.ts
import { test, expect } from '@playwright/test';
test.describe('用户管理功能', () => {
test.beforeEach(async ({ page }) => {
// 登录
await page.goto('/login');
await page.fill('[data-testid="username-input"]', 'admin');
await page.fill('[data-testid="password-input"]', '111111');
await page.click('[data-testid="login-button"]');
await page.waitForURL('/dashboard');
// 导航到用户管理页面
await page.click('[data-testid="menu-user"]');
await page.click('[data-testid="menu-user-list"]');
await page.waitForURL('/user/list');
});
test('应该显示用户列表', async ({ page }) => {
// 等待用户列表加载
await page.waitForSelector('[data-testid="user-table"]');
// 检查表格是否显示
const table = page.locator('[data-testid="user-table"]');
await expect(table).toBeVisible();
// 检查表格头部
await expect(page.locator('th')).toContainText([
'用户名',
'邮箱',
'状态',
'创建时间',
'操作',
]);
// 检查是否有用户数据
const rows = page.locator('tbody tr');
await expect(rows).toHaveCountGreaterThan(0);
});
test('应该能够搜索用户', async ({ page }) => {
// 输入搜索关键词
await page.fill('[data-testid="search-input"]', 'admin');
await page.click('[data-testid="search-button"]');
// 等待搜索结果
await page.waitForTimeout(1000);
// 检查搜索结果
const userRows = page.locator('tbody tr');
const firstUserName = userRows.first().locator('td').nth(1);
await expect(firstUserName).toContainText('admin');
});
test('应该能够创建新用户', async ({ page }) => {
// 点击添加用户按钮
await page.click('[data-testid="add-user-button"]');
// 填写用户信息
await page.fill('[data-testid="username-input"]', 'newuser');
await page.fill('[data-testid="email-input"]', 'newuser@example.com');
await page.fill('[data-testid="password-input"]', '123456');
await page.fill('[data-testid="firstName-input"]', 'New');
await page.fill('[data-testid="lastName-input"]', 'User');
// 提交表单
await page.click('[data-testid="submit-button"]');
// 等待成功消息
await expect(page.locator('.ant-message-success')).toBeVisible();
// 检查用户是否已添加到列表
await page.waitForTimeout(1000);
const userRows = page.locator('tbody tr');
await expect(userRows).toContainText('newuser');
});
test('应该能够编辑用户', async ({ page }) => {
// 点击第一个用户的编辑按钮
await page.click('tbody tr:first-child [data-testid="edit-button"]');
// 修改用户信息
await page.fill('[data-testid="firstName-input"]', 'Updated');
await page.fill('[data-testid="lastName-input"]', 'Name');
// 提交更改
await page.click('[data-testid="submit-button"]');
// 等待成功消息
await expect(page.locator('.ant-message-success')).toBeVisible();
// 返回列表页面
await page.click('[data-testid="back-button"]');
// 检查更改是否生效
const firstUserRow = page.locator('tbody tr:first-child');
await expect(firstUserRow).toContainText('Updated Name');
});
test('应该能够删除用户', async ({ page }) => {
// 获取删除前的用户数量
const initialCount = await page.locator('tbody tr').count();
// 点击第一个用户的删除按钮
await page.click('tbody tr:first-child [data-testid="delete-button"]');
// 确认删除
await page.click('[data-testid="confirm-delete-button"]');
// 等待成功消息
await expect(page.locator('.ant-message-success')).toBeVisible();
// 检查用户数量是否减少
await page.waitForTimeout(1000);
const finalCount = await page.locator('tbody tr').count();
expect(finalCount).toBe(initialCount - 1);
});
});
```
### 测试配置
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.config.*',
'build/',
'dist/',
],
},
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
});
```
---
## 🔀 Git工作流
### 分支策略
采用 Git Flow 分支策略:
```bash
# 主要分支
main # 生产环境分支
develop # 开发环境分支
# 支持分支
feature/* # 功能分支
release/* # 发布分支
hotfix/* # 热修复分支
```
### 分支命名规范
```bash
# 功能分支
feature/user-management
feature/message-template
feature/api-optimization
# 修复分支
bugfix/login-validation
bugfix/table-pagination
# 热修复分支
hotfix/security-patch
hotfix/performance-issue
# 发布分支
release/v2.1.0
release/v2.2.0
```
### 提交信息规范
采用 Conventional Commits 规范:
```bash
# 格式
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
# 类型说明
feat: 新功能
fix: 修复bug
docs: 文档更新
style: 格式调整(不影响代码运行)
refactor: 重构代码
perf: 性能优化
test: 测试相关
build: 构建系统相关
ci: CI配置相关
chore: 其他修改
# 示例
feat(user): add user profile management
Add comprehensive user profile management functionality including:
- User profile editing
- Avatar upload
- Profile validation
- Permission checks
Closes #123
```
### Git Hooks配置
```javascript
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 代码检查
pnpm lint-staged
# 类型检查
pnpm typecheck
```
```javascript
// .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 提交信息验证
pnpm commitlint --edit $1
```
```json
// lint-staged.config.js
module.exports = {
'*.{js,jsx,ts,tsx,vue}': [
'eslint --fix',
'prettier --write',
'git add'
],
'*.{css,scss,less}': [
'stylelint --fix',
'prettier --write',
'git add'
],
'*.{json,md}': [
'prettier --write',
'git add'
]
};
```
---
## 👀 代码审查
### 审查清单
#### 功能性审查
- [ ] **功能完整性**: 功能是否按需求完整实现
- [ ] **边界条件**: 是否处理了边界情况和异常情况
- [ ] **错误处理**: 是否有适当的错误处理和用户提示
- [ ] **性能影响**: 是否会对系统性能产生负面影响
- [ ] **安全性**: 是否存在安全漏洞或风险
#### 代码质量审查
- [ ] **代码规范**: 是否遵循项目的代码规范和最佳实践
- [ ] **命名规范**: 变量、函数、类名是否清晰且有意义
- [ ] **代码复用**: 是否有重复的代码可以提取复用
- [ ] **注释质量**: 复杂逻辑是否有适当的注释说明
- [ ] **类型安全**: TypeScript类型定义是否完整和准确
#### 架构设计审查
- [ ] **设计模式**: 是否使用了合适的设计模式
- [ ] **组件设计**: 组件职责是否单一,是否可复用
- [ ] **API设计**: API接口设计是否合理和一致
- [ ] **状态管理**: 状态管理是否合理,是否有不必要的状态
- [ ] **依赖关系**: 模块间依赖是否合理,是否有循环依赖
#### 测试覆盖审查
- [ ] **单元测试**: 是否有足够的单元测试覆盖
- [ ] **集成测试**: 关键功能是否有集成测试
- [ ] **测试质量**: 测试用例是否覆盖了主要场景和边界情况
- [ ] **测试维护**: 测试代码是否易于维护和理解
### 审查流程
1. **自检阶段**
- 提交前自我审查代码
- 运行所有测试确保通过
- 检查代码规范和类型检查
2. **同伴审查**
- 指定至少一名同伴进行代码审查
- 审查者需要理解业务需求和技术方案
- 提供建设性的反馈和建议
3. **技术负责人审查**
- 涉及架构变更需要技术负责人审查
- 检查技术方案的合理性和可维护性
- 评估对系统整体的影响
4. **合并前检查**
- 确保所有CI检查通过
- 解决所有审查意见
- 更新相关文档
---
## ⚡ 性能优化规范
### 前端性能优化
#### 1. 构建优化
```typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
// 代码分割
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
antd: ['ant-design-vue'],
utils: ['lodash-es', 'dayjs'],
},
},
},
// 启用压缩
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log'],
},
},
},
// 开发服务器优化
server: {
hmr: {
overlay: false,
},
},
// 预构建优化
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia', 'ant-design-vue', '@vueuse/core'],
},
});
```
#### 2. 组件优化
```vue
<template>
<div class="user-list">
<!-- 虚拟滚动 -->
<VirtualList
:items="userList"
:item-height="60"
:visible-count="20"
:buffer-size="5"
@scroll-end="loadMore"
>
<template #default="{ item, index }">
<UserCard
:key="item.id"
:user="item"
:index="index"
@click="handleUserClick"
/>
</template>
</VirtualList>
</div>
</template>
<script setup lang="ts">
// 懒加载组件
const UserCard = defineAsyncComponent(() => import('./UserCard.vue'));
// 响应式性能优化
const userList = shallowRef<UserInfo[]>([]);
const selectedUsers = shallowRef<Set<string>>(new Set());
// 计算属性缓存
const filteredUsers = computed(() => {
if (!searchKeyword.value) return userList.value;
return userList.value.filter((user) =>
user.username.toLowerCase().includes(searchKeyword.value.toLowerCase()),
);
});
// 防抖搜索
const searchKeyword = ref('');
const debouncedSearch = useDebounceFn((keyword: string) => {
// 执行搜索逻辑
searchUsers(keyword);
}, 300);
watch(searchKeyword, debouncedSearch);
// 性能监控
const startTime = performance.now();
onMounted(() => {
const endTime = performance.now();
console.log(`Component mounted in ${endTime - startTime}ms`);
});
</script>
```
#### 3. 图片优化
```typescript
// utils/image.ts
/**
* 图片懒加载指令
*/
export const vLazyLoad = {
mounted(el: HTMLImageElement, binding: DirectiveBinding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = binding.value;
img.onload = () => {
img.classList.add('loaded');
};
observer.unobserve(img);
}
});
});
observer.observe(el);
},
};
/**
* 图片压缩
*/
export function compressImage(
file: File,
quality: number = 0.8,
maxWidth: number = 1920,
): Promise<Blob> {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
const img = new Image();
img.onload = () => {
const { width, height } = img;
const ratio = Math.min(maxWidth / width, maxWidth / height);
canvas.width = width * ratio;
canvas.height = height * ratio;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(resolve as BlobCallback, 'image/jpeg', quality);
};
img.src = URL.createObjectURL(file);
});
}
```
#### 4. 缓存策略
```typescript
// utils/cache.ts
class CacheManager {
private cache = new Map<string, { data: any; expiry: number }>();
set(key: string, data: any, ttl: number = 5 * 60 * 1000) {
const expiry = Date.now() + ttl;
this.cache.set(key, { data, expiry });
}
get<T = any>(key: string): T | null {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.data;
}
clear() {
this.cache.clear();
}
// 清理过期缓存
cleanup() {
const now = Date.now();
for (const [key, value] of this.cache.entries()) {
if (now > value.expiry) {
this.cache.delete(key);
}
}
}
}
export const cacheManager = new CacheManager();
// 定期清理过期缓存
setInterval(
() => {
cacheManager.cleanup();
},
5 * 60 * 1000,
); // 每5分钟清理一次
```
### 性能监控
```typescript
// utils/performance.ts
class PerformanceMonitor {
private metrics: Map<string, number[]> = new Map();
// 开始性能监控
start(name: string): () => void {
const startTime = performance.now();
return () => {
const endTime = performance.now();
const duration = endTime - startTime;
this.record(name, duration);
};
}
// 记录性能数据
record(name: string, value: number) {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
const values = this.metrics.get(name)!;
values.push(value);
// 只保留最近100条记录
if (values.length > 100) {
values.shift();
}
}
// 获取性能统计
getStats(name: string) {
const values = this.metrics.get(name);
if (!values || values.length === 0) return null;
const sorted = [...values].sort((a, b) => a - b);
const len = sorted.length;
return {
count: len,
min: sorted[0],
max: sorted[len - 1],
avg: values.reduce((a, b) => a + b, 0) / len,
p50: sorted[Math.floor(len * 0.5)],
p90: sorted[Math.floor(len * 0.9)],
p95: sorted[Math.floor(len * 0.95)],
};
}
// 获取所有统计数据
getAllStats() {
const stats: Record<string, any> = {};
for (const name of this.metrics.keys()) {
stats[name] = this.getStats(name);
}
return stats;
}
// 发送性能数据到服务器
async report() {
const stats = this.getAllStats();
try {
await fetch('/api/performance/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
stats,
}),
});
} catch (error) {
console.warn('Failed to report performance metrics:', error);
}
}
}
export const performanceMonitor = new PerformanceMonitor();
// 定期上报性能数据
setInterval(() => {
performanceMonitor.report();
}, 60 * 1000); // 每分钟上报一次
```
---
## 📚 总结
本开发规范文档涵盖了Telegram管理系统开发的各个方面包括
### 🎯 核心规范
- **开发环境**: 统一的开发工具和配置
- **代码规范**: TypeScript、ESLint、Prettier配置
- **项目结构**: 清晰的目录组织和文件命名
### 🛠️ 技术规范
- **组件开发**: Vue 3组合式API最佳实践
- **API开发**: 统一的接口调用和错误处理
- **状态管理**: Pinia状态管理规范
- **路由管理**: 动态路由和权限控制
### 🎨 质量保证
- **样式规范**: TailwindCSS + SCSS混合方案
- **测试规范**: 单元测试、组件测试、E2E测试
- **Git工作流**: 分支策略和提交规范
- **代码审查**: 完整的审查流程和清单
### ⚡ 性能优化
- **构建优化**: Vite配置和代码分割
- **运行时优化**: 懒加载、虚拟滚动、缓存策略
- **性能监控**: 性能指标收集和上报
遵循这些规范将确保代码质量、提高开发效率、降低维护成本。所有团队成员都应该熟悉并严格执行这些规范。
---
**维护说明**: 本规范文档会随着项目发展不断更新和完善,请定期查看最新版本。如有疑问或建议,请联系技术团队。