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

63 KiB
Raw Blame History

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接口设计是否合理和一致
  • 状态管理: 状态管理是否合理,是否有不必要的状态
  • 依赖关系: 模块间依赖是否合理,是否有循环依赖

测试覆盖审查

  • 单元测试: 是否有足够的单元测试覆盖
  • 集成测试: 关键功能是否有集成测试
  • 测试质量: 测试用例是否覆盖了主要场景和边界情况
  • 测试维护: 测试代码是否易于维护和理解

审查流程

  1. 自检阶段

    • 提交前自我审查代码
    • 运行所有测试确保通过
    • 检查代码规范和类型检查
  2. 同伴审查

    • 指定至少一名同伴进行代码审查
    • 审查者需要理解业务需求和技术方案
    • 提供建设性的反馈和建议
  3. 技术负责人审查

    • 涉及架构变更需要技术负责人审查
    • 检查技术方案的合理性和可维护性
    • 评估对系统整体的影响
  4. 合并前检查

    • 确保所有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配置和代码分割
  • 运行时优化: 懒加载、虚拟滚动、缓存策略
  • 性能监控: 性能指标收集和上报

遵循这些规范将确保代码质量、提高开发效率、降低维护成本。所有团队成员都应该熟悉并严格执行这些规范。


维护说明: 本规范文档会随着项目发展不断更新和完善,请定期查看最新版本。如有疑问或建议,请联系技术团队。