Some checks failed
Deploy / deploy (push) Has been cancelled
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>
63 KiB
63 KiB
Telegram 管理系统开发规范文档
版本: v2.0.0
更新时间: 2024-01-20
维护者: 开发团队
📋 文档目录
🛠️ 开发环境配置
必备工具
1. 基础开发工具
# 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插件配置
推荐安装以下插件:
{
"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设置
{
"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. 环境变量设置
# 开发环境配置
cp .env.development.example .env.development
# 本地环境配置
cp .env.local.example .env.local
2. 依赖安装
# 安装项目依赖
pnpm install
# 安装Playwright浏览器
pnpm test:install
# 验证环境
pnpm typecheck
pnpm lint
pnpm test:unit
📝 代码规范
TypeScript规范
1. 类型定义
// ✅ 推荐:使用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. 函数定义
// ✅ 推荐:明确的参数和返回类型
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类型定义
// ✅ 推荐:为组合式函数定义返回类型
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. 基础规则
// .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规范
// .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. 文件命名
# ✅ 推荐:组件使用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. 变量和函数命名
// ✅ 推荐:变量使用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. 组件命名
// ✅ 推荐:组件使用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. 组件文件
# 页面组件 - PascalCase
UserList.vue
AccountManagement.vue
PrivateMessageSend.vue
# 业务组件 - PascalCase
UserCard.vue
MessageTemplate.vue
StatisticsChart.vue
# 基础组件 - 带前缀的PascalCase
BaseButton.vue
BaseTable.vue
BaseForm.vue
2. 工具文件
# 工具函数 - 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. 存储和组合式函数
# Store文件 - camelCase
userStore.ts
authStore.ts
permissionStore.ts
# 组合式函数 - camelCase with use前缀
useUser.ts
useAuth.ts
usePermission.ts
useApi.ts
🧩 组件开发规范
Vue 3 组合式API规范
1. 组件结构
<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. 组合式函数规范
// 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. 组件通信规范
// 父子组件通信 - 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客户端配置
// 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模块定义
// 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类型定义
// 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规范
// 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组合使用
// 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';
🛣️ 路由规范
路由配置
// 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;
路由守卫
// 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();
});
}
动态路由
// 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 的混合方案:
// 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;
}
组件样式规范
// 使用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;
}
}
全局样式
// 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配置
// 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'),
],
};
🧪 测试规范
单元测试规范
// 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);
});
});
组件测试规范
// 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测试规范
// 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);
});
});
测试配置
// 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 分支策略:
# 主要分支
main # 生产环境分支
develop # 开发环境分支
# 支持分支
feature/* # 功能分支
release/* # 发布分支
hotfix/* # 热修复分支
分支命名规范
# 功能分支
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 规范:
# 格式
<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配置
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 代码检查
pnpm lint-staged
# 类型检查
pnpm typecheck
// .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 提交信息验证
pnpm commitlint --edit $1
// 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接口设计是否合理和一致
- 状态管理: 状态管理是否合理,是否有不必要的状态
- 依赖关系: 模块间依赖是否合理,是否有循环依赖
测试覆盖审查
- 单元测试: 是否有足够的单元测试覆盖
- 集成测试: 关键功能是否有集成测试
- 测试质量: 测试用例是否覆盖了主要场景和边界情况
- 测试维护: 测试代码是否易于维护和理解
审查流程
-
自检阶段
- 提交前自我审查代码
- 运行所有测试确保通过
- 检查代码规范和类型检查
-
同伴审查
- 指定至少一名同伴进行代码审查
- 审查者需要理解业务需求和技术方案
- 提供建设性的反馈和建议
-
技术负责人审查
- 涉及架构变更需要技术负责人审查
- 检查技术方案的合理性和可维护性
- 评估对系统整体的影响
-
合并前检查
- 确保所有CI检查通过
- 解决所有审查意见
- 更新相关文档
⚡ 性能优化规范
前端性能优化
1. 构建优化
// 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. 组件优化
<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. 图片优化
// 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. 缓存策略
// 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分钟清理一次
性能监控
// 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配置和代码分割
- 运行时优化: 懒加载、虚拟滚动、缓存策略
- 性能监控: 性能指标收集和上报
遵循这些规范将确保代码质量、提高开发效率、降低维护成本。所有团队成员都应该熟悉并严格执行这些规范。
维护说明: 本规范文档会随着项目发展不断更新和完善,请定期查看最新版本。如有疑问或建议,请联系技术团队。