目录
项目概述
项目定位
后台管理系统是一个基于 Vue 3 + TypeScript + Vite 的现代化前端应用,主要用于xx业务的管理和操作。
核心特性
- 🚀 基于 Vue 3 Composition API
- 💪 TypeScript 全面支持
- ⚡ Vite 构建工具
- 🎨 Element Plus UI 组件库
- 🌍 完整的国际化支持
- 📱 响应式设计
- 🔒 完善的权限控制
- 🧪 单元测试覆盖
技术栈规范
核心技术栈
{
"framework": "Vue 3.3+",
"language": "TypeScript 5.0+",
"buildTool": "Vite 4.0+",
"uiLibrary": "Element Plus 2.3+",
"stateManagement": "Pinia 2.1+",
"router": "Vue Router 4.2+",
"i18n": "Vue I18n 9.2+",
"httpClient": "Axios 1.4+",
"cssFramework": "Tailwind CSS 3.3+",
"testing": "Vitest + Vue Test Utils",
"linting": "ESLint + Prettier"
}
开发工具要求
- Node.js: >= 18.0.0
- 包管理器: npm >= 9.0.0 或 yarn >= 1.22.0
- 编辑器: VS Code (推荐)
- 浏览器: Chrome >= 90, Firefox >= 88, Safari >= 14
VS Code 扩展推荐
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"lokalise.i18n-ally",
"ms-vscode.vscode-typescript-next"
]
}
项目结构规范
目录结构
cmclink-ibs-web/
├── public/ # 静态资源
│ ├── favicon.ico
│ └── index.html
├── src/ # 源代码
│ ├── api/ # API 接口
│ │ ├── modules/ # 按模块分类的接口
│ │ │ ├── auth.ts
│ │ │ ├── booking.ts
│ │ │ └── user.ts
│ │ ├── types/ # API 类型定义
│ │ └── index.ts # API 统一导出
│ ├── assets/ # 静态资源
│ │ ├── images/
│ │ ├── icons/
│ │ └── styles/
│ ├── components/ # 公共组件
│ │ ├── Common/ # 通用组件
│ │ ├── Business/ # 业务组件
│ │ └── Layout/ # 布局组件
│ ├── composables/ # 组合式函数
│ │ ├── useAuth.ts
│ │ ├── useTable.ts
│ │ └── useForm.ts
│ ├── directives/ # 自定义指令
│ ├── i18n/ # 国际化
│ │ ├── zh/ # 中文语言包
│ │ ├── en/ # 英文语言包
│ │ └── index.ts # i18n 配置
│ ├── router/ # 路由配置
│ │ ├── modules/ # 路由模块
│ │ ├── guards/ # 路由守卫
│ │ └── index.ts
│ ├── store/ # 状态管理
│ │ ├── modules/ # store 模块
│ │ └── index.ts
│ ├── styles/ # 样式文件
│ │ ├── variables.scss # 变量定义
│ │ ├── mixins.scss # 混入
│ │ └── global.scss # 全局样式
│ ├── types/ # 类型定义
│ │ ├── api.ts # API 类型
│ │ ├── common.ts # 通用类型
│ │ └── business.ts # 业务类型
│ ├── utils/ # 工具函数
│ │ ├── request.ts # 请求封装
│ │ ├── storage.ts # 存储工具
│ │ ├── validators.ts # 验证工具
│ │ └── formatters.ts # 格式化工具
│ ├── views/ # 页面组件
│ │ ├── auth/ # 认证相关页面
│ │ ├── booking/ # 订舱相关页面
│ │ ├── dashboard/ # 仪表盘
│ │ └── system/ # 系统管理
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── tests/ # 测试文件
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── utils/ # 测试工具
├── docs/ # 项目文档
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── .eslintrc.js # ESLint 配置
├── .prettierrc # Prettier 配置
├── tailwind.config.js # Tailwind 配置
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 配置
└── package.json # 项目配置
文件命名规范
- 组件文件: PascalCase (如
UserProfile.vue) - 页面文件: PascalCase (如
BookingList.vue) - 工具文件: camelCase (如
formatters.ts) - 常量文件: UPPER_SNAKE_CASE (如
API_CONSTANTS.ts) - 类型文件: camelCase (如
userTypes.ts) - 样式文件: kebab-case (如
user-profile.scss)
代码编写规范
TypeScript 规范
1. 类型定义
// ✅ 推荐:使用 interface 定义对象类型
interface User {
id: number;
name: string;
email: string;
avatar?: string;
createdAt: Date;
}
// ✅ 推荐:使用 type 定义联合类型
type UserStatus = 'active' | 'inactive' | 'pending';
// ✅ 推荐:使用泛型提高复用性
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// ✅ 推荐:使用枚举定义常量
enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest'
}
2. 函数定义
// ✅ 推荐:明确的参数和返回值类型
function formatUser(user: User): FormattedUser {
return {
id: user.id,
displayName: user.name,
emailAddress: user.email
};
}
// ✅ 推荐:使用箭头函数和类型推断
const calculateTotal = (items: OrderItem[]): number => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
};
// ✅ 推荐:异步函数的类型定义
async function fetchUserData(id: number): Promise<User> {
const response = await api.get<ApiResponse<User>>(`/users/${id}`);
return response.data.data;
}
3. 组件 Props 类型
// ✅ 推荐:使用 interface 定义 Props
interface UserCardProps {
user: User;
showAvatar?: boolean;
onEdit?: (user: User) => void;
onDelete?: (id: number) => void;
}
// ✅ 推荐:使用 withDefaults 设置默认值
const props = withDefaults(defineProps<UserCardProps>(), {
showAvatar: true
});
Vue 3 Composition API 规范
1. 组件结构
<template>
<!-- 模板内容 -->
</template>
<script setup lang="ts">
// 1. 导入依赖
import { ref, computed, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
// 2. 类型定义
interface Props {
// props 类型
}
interface Emits {
// emits 类型
}
// 3. Props 和 Emits
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// 4. 组合式函数
const { t } = useI18n();
const router = useRouter();
// 5. 响应式数据
const loading = ref(false);
const formData = ref({});
// 6. 计算属性
const computedValue = computed(() => {
// 计算逻辑
});
// 7. 方法定义
const handleSubmit = async () => {
// 方法实现
};
// 8. 监听器
watch(
() => props.value,
(newValue) => {
// 监听逻辑
}
);
// 9. 生命周期
onMounted(() => {
// 初始化逻辑
});
// 10. 暴露给模板的内容
defineExpose({
// 暴露的方法或属性
});
</script>
<style scoped>
/* 组件样式 */
</style>
2. 响应式数据规范
// ✅ 推荐:使用 ref 定义基础类型
const count = ref(0);
const message = ref('');
const isVisible = ref(false);
// ✅ 推荐:使用 reactive 定义对象
const formData = reactive({
name: '',
email: '',
age: 0
});
// ✅ 推荐:使用 computed 定义计算属性
const fullName = computed(() => {
return `${formData.firstName} ${formData.lastName}`;
});
// ✅ 推荐:使用 readonly 保护数据
const readonlyData = readonly(formData);
错误处理规范
// ✅ 推荐:统一的错误处理
try {
const result = await apiCall();
// 处理成功结果
} catch (error) {
// 记录错误
console.error('API call failed:', error);
// 显示用户友好的错误信息
if (error instanceof ApiError) {
showError(error.message);
} else {
showError(t('error.unexpectedError'));
}
}
// ✅ 推荐:自定义错误类
class ApiError extends Error {
constructor(
message: string,
public code: string,
public status: number
) {
super(message);
this.name = 'ApiError';
}
}
组件开发规范
组件分类
-
基础组件 (Base Components)
- 纯 UI 组件,无业务逻辑
- 高度可复用
- 命名以
Base开头
-
通用组件 (Common Components)
- 包含少量业务逻辑
- 跨模块复用
- 放在
components/Common/目录
-
业务组件 (Business Components)
- 特定业务逻辑
- 模块内复用
- 放在
components/Business/目录
-
页面组件 (Page Components)
- 页面级组件
- 不可复用
- 放在
views/目录
组件开发示例
<!-- components/Common/DataTable.vue -->
<template>
<div class="data-table">
<div v-if="showHeader" class="table-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
</div>
<el-table
:data="data"
:loading="loading"
v-bind="$attrs"
@selection-change="handleSelectionChange"
>
<slot />
</el-table>
<div v-if="showPagination" class="table-pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
data: any[];
loading?: boolean;
title?: string;
showHeader?: boolean;
showPagination?: boolean;
total?: number;
pageSize?: number;
currentPage?: number;
}
interface Emits {
'selection-change': [selection: any[]];
'size-change': [size: number];
'current-change': [page: number];
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
showHeader: true,
showPagination: true,
pageSize: 20,
currentPage: 1
});
const emit = defineEmits<Emits>();
// 事件处理
const handleSelectionChange = (selection: any[]) => {
emit('selection-change', selection);
};
const handleSizeChange = (size: number) => {
emit('size-change', size);
};
const handleCurrentChange = (page: number) => {
emit('current-change', page);
};
</script>
<style scoped>
.data-table {
@apply bg-white rounded-lg shadow;
}
.table-header {
@apply p-4 border-b border-gray-200;
}
.table-pagination {
@apply p-4 flex justify-end;
}
</style>
组件文档规范
<!--
组件名称: DataTable
组件描述: 通用数据表格组件,支持分页、选择、自定义列等功能
使用示例:
<DataTable
:data="tableData"
:loading="loading"
title="用户列表"
@selection-change="handleSelectionChange"
>
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
</DataTable>
Props:
- data: 表格数据数组
- loading: 加载状态
- title: 表格标题
- showHeader: 是否显示头部
- showPagination: 是否显示分页
Emits:
- selection-change: 选择项变化事件
- size-change: 分页大小变化事件
- current-change: 当前页变化事件
Slots:
- default: 表格列定义
- header: 自定义头部内容
-->
状态管理规范
Pinia Store 结构
// store/modules/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { User, UserProfile } from '@/types/user';
import { userApi } from '@/api/modules/user';
export const useUserStore = defineStore('user', () => {
// State
const currentUser = ref<User | null>(null);
const userProfile = ref<UserProfile | null>(null);
const loading = ref(false);
// Getters
const isLoggedIn = computed(() => !!currentUser.value);
const userName = computed(() => currentUser.value?.name || '');
const userRole = computed(() => currentUser.value?.role || 'guest');
// Actions
const login = async (credentials: LoginCredentials) => {
loading.value = true;
try {
const response = await userApi.login(credentials);
currentUser.value = response.data.user;
// 存储 token
localStorage.setItem('token', response.data.token);
return response;
} catch (error) {
throw error;
} finally {
loading.value = false;
}
};
const logout = async () => {
try {
await userApi.logout();
} finally {
currentUser.value = null;
userProfile.value = null;
localStorage.removeItem('token');
}
};
const fetchProfile = async () => {
if (!currentUser.value) return;
loading.value = true;
try {
const response = await userApi.getProfile(currentUser.value.id);
userProfile.value = response.data;
} catch (error) {
console.error('Failed to fetch user profile:', error);
} finally {
loading.value = false;
}
};
const updateProfile = async (data: Partial<UserProfile>) => {
if (!userProfile.value) return;
loading.value = true;
try {
const response = await userApi.updateProfile(userProfile.value.id, data);
userProfile.value = { ...userProfile.value, ...response.data };
return response;
} catch (error) {
throw error;
} finally {
loading.value = false;
}
};
// 重置状态
const $reset = () => {
currentUser.value = null;
userProfile.value = null;
loading.value = false;
};
return {
// State
currentUser,
userProfile,
loading,
// Getters
isLoggedIn,
userName,
userRole,
// Actions
login,
logout,
fetchProfile,
updateProfile,
$reset
};
});
Store 使用规范
<script setup lang="ts">
import { useUserStore } from '@/store/modules/user';
import { storeToRefs } from 'pinia';
// ✅ 推荐:使用 storeToRefs 保持响应性
const userStore = useUserStore();
const { currentUser, loading, isLoggedIn } = storeToRefs(userStore);
const { login, logout } = userStore;
// ✅ 推荐:在组件中调用 store actions
const handleLogin = async (credentials: LoginCredentials) => {
try {
await login(credentials);
// 登录成功后的处理
} catch (error) {
// 错误处理
}
};
</script>
路由管理规范
路由配置结构
// router/modules/booking.ts
import type { RouteRecordRaw } from 'vue-router';
const bookingRoutes: RouteRecordRaw[] = [
{
path: '/booking',
name: 'Booking',
component: () => import('@/views/booking/index.vue'),
meta: {
titleKey: 'router.booking.title',
icon: 'booking',
requiresAuth: true,
permissions: ['booking:read']
},
children: [
{
path: '',
redirect: '/booking/list'
},
{
path: 'list',
name: 'BookingList',
component: () => import('@/views/booking/BookingList.vue'),
meta: {
titleKey: 'router.booking.list',
keepAlive: true
}
},
{
path: 'create',
name: 'BookingCreate',
component: () => import('@/views/booking/BookingCreate.vue'),
meta: {
titleKey: 'router.booking.create'
}
},
{
path: ':id/edit',
name: 'BookingEdit',
component: () => import('@/views/booking/BookingEdit.vue'),
meta: {
titleKey: 'router.booking.edit'
},
props: true
}
]
}
];
export default bookingRoutes;
路由守卫
// router/guards/auth.ts
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import { showError } from '@/utils/message';
export function createAuthGuard() {
return async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
const userStore = useUserStore();
// 检查是否需要认证
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
showError('auth.loginRequired');
next({ name: 'Login', query: { redirect: to.fullPath } });
return;
}
// 检查权限
if (to.meta.permissions && userStore.isLoggedIn) {
const hasPermission = to.meta.permissions.some((permission: string) =>
userStore.hasPermission(permission)
);
if (!hasPermission) {
showError('auth.permissionDenied');
next({ name: 'Forbidden' });
return;
}
}
next();
};
}
API 接口规范
接口封装
// api/modules/booking.ts
import { request } from '@/utils/request';
import type {
BookingListParams,
BookingListResponse,
BookingDetail,
CreateBookingData,
UpdateBookingData
} from '@/types/booking';
export const bookingApi = {
// 获取订舱列表
getList(params: BookingListParams): Promise<BookingListResponse> {
return request.get('/bookings', { params });
},
// 获取订舱详情
getDetail(id: number): Promise<BookingDetail> {
return request.get(`/bookings/${id}`);
},
// 创建订舱
create(data: CreateBookingData): Promise<BookingDetail> {
return request.post('/bookings', data);
},
// 更新订舱
update(id: number, data: UpdateBookingData): Promise<BookingDetail> {
return request.put(`/bookings/${id}`, data);
},
// 删除订舱
delete(id: number): Promise<void> {
return request.delete(`/bookings/${id}`);
},
// 批量删除
batchDelete(ids: number[]): Promise<void> {
return request.delete('/bookings/batch', { data: { ids } });
}
};
请求拦截器
// utils/request.ts
import axios from 'axios';
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import { useUserStore } from '@/store/modules/user';
import { showError } from '@/utils/message';
import { ApiErrorHandler } from '@/utils/api-error';
// 创建 axios 实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器
request.interceptors.request.use(
(config: AxiosRequestConfig) => {
const userStore = useUserStore();
// 添加认证 token
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`
};
}
// 添加语言标识
const locale = localStorage.getItem('locale') || 'zh';
config.headers = {
...config.headers,
'Accept-Language': locale
};
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
const { code, message, data } = response.data;
// 业务成功
if (code === 200) {
return data;
}
// 业务失败
const error = new Error(message);
error.code = code;
return Promise.reject(error);
},
(error) => {
// 处理 HTTP 错误
ApiErrorHandler.handle(error);
return Promise.reject(error);
}
);
export { request };
样式编写规范
Tailwind CSS 使用规范
<template>
<!-- ✅ 推荐:使用 Tailwind 原子类 -->
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow">
<h2 class="text-lg font-semibold text-gray-900">
{{ title }}
</h2>
<button class="px-4 py-2 text-white bg-blue-600 rounded hover:bg-blue-700">
{{ t('common.save') }}
</button>
</div>
<!-- ✅ 推荐:复杂样式使用自定义类 -->
<div class="user-card">
<div class="user-avatar">
<img :src="user.avatar" :alt="user.name" />
</div>
<div class="user-info">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
</div>
</template>
<style scoped>
/* ✅ 推荐:使用 @apply 指令组合 Tailwind 类 */
.user-card {
@apply flex items-center p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow;
}
.user-avatar {
@apply w-12 h-12 mr-4 overflow-hidden rounded-full;
}
.user-avatar img {
@apply w-full h-full object-cover;
}
.user-info h3 {
@apply text-lg font-semibold text-gray-900;
}
.user-info p {
@apply text-sm text-gray-600;
}
</style>
SCSS 变量和混入
// styles/variables.scss
:root {
// 颜色变量
--color-primary: #3b82f6;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-danger: #ef4444;
--color-info: #6b7280;
// 间距变量
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
// 字体变量
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
// 阴影变量
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
// styles/mixins.scss
// 响应式断点混入
@mixin mobile {
@media (max-width: 767px) {
@content;
}
}
@mixin tablet {
@media (min-width: 768px) and (max-width: 1023px) {
@content;
}
}
@mixin desktop {
@media (min-width: 1024px) {
@content;
}
}
// 文本省略混入
@mixin text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin text-ellipsis-multiline($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}
// 居中混入
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
国际化规范
语言包组织
// i18n/zh/index.ts
import common from './common';
import auth from './auth';
import booking from './booking';
import user from './user';
import error from './error';
export default {
common,
auth,
booking,
user,
error
};
// i18n/zh/common.ts
export default {
// 通用操作
save: '保存',
cancel: '取消',
confirm: '确认',
delete: '删除',
edit: '编辑',
add: '添加',
search: '搜索',
reset: '重置',
submit: '提交',
// 状态
loading: '加载中...',
noData: '暂无数据',
success: '操作成功',
failed: '操作失败',
// 表单
required: '此字段为必填项',
invalidFormat: '格式不正确',
// 分页
total: '共 {count} 条',
pageSize: '每页显示',
// 时间
today: '今天',
yesterday: '昨天',
thisWeek: '本周',
thisMonth: '本月'
};
国际化使用规范
<template>
<!-- ✅ 推荐:使用 $t 函数 -->
<h1>{{ $t('booking.title') }}</h1>
<!-- ✅ 推荐:带参数的翻译 -->
<p>{{ $t('common.total', { count: totalCount }) }}</p>
<!-- ✅ 推荐:复数形式 -->
<span>{{ $tc('booking.item', itemCount) }}</span>
<!-- ✅ 推荐:属性中使用翻译 -->
<el-input :placeholder="$t('user.enterName')" />
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t, tc, locale } = useI18n();
// ✅ 推荐:在 JavaScript 中使用翻译
const showSuccessMessage = () => {
ElMessage.success(t('common.saveSuccess'));
};
// ✅ 推荐:动态翻译键
const getStatusText = (status: string) => {
return t(`booking.status.${status}`);
};
</script>
测试规范
单元测试
// tests/unit/components/UserCard.test.ts
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import UserCard from '@/components/Common/UserCard.vue';
import { createTestI18n } from '../utils/test-utils';
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://example.com/avatar.jpg'
};
describe('UserCard', () => {
it('should render user information correctly', () => {
const wrapper = mount(UserCard, {
props: {
user: mockUser
},
global: {
plugins: [createTestI18n()]
}
});
expect(wrapper.find('.user-name').text()).toBe(mockUser.name);
expect(wrapper.find('.user-email').text()).toBe(mockUser.email);
expect(wrapper.find('img').attributes('src')).toBe(mockUser.avatar);
});
it('should emit edit event when edit button is clicked', async () => {
const wrapper = mount(UserCard, {
props: {
user: mockUser,
showActions: true
},
global: {
plugins: [createTestI18n()]
}
});
await wrapper.find('.edit-button').trigger('click');
expect(wrapper.emitted('edit')).toBeTruthy();
expect(wrapper.emitted('edit')?.[0]).toEqual([mockUser]);
});
it('should not show actions when showActions is false', () => {
const wrapper = mount(UserCard, {
props: {
user: mockUser,
showActions: false
},
global: {
plugins: [createTestI18n()]
}
});
expect(wrapper.find('.user-actions').exists()).toBe(false);
});
});
集成测试
// tests/integration/booking.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { createRouter, createWebHistory } from 'vue-router';
import BookingList from '@/views/booking/BookingList.vue';
import { createTestPinia } from '@pinia/testing';
import { createTestI18n } from '../utils/test-utils';
// Mock API
vi.mock('@/api/modules/booking', () => ({
bookingApi: {
getList: vi.fn().mockResolvedValue({
data: [],
total: 0
})
}
}));
describe('BookingList Integration', () => {
let router: any;
beforeEach(() => {
router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/booking/list', component: BookingList }
]
});
});
it('should load and display booking list', async () => {
const wrapper = mount(BookingList, {
global: {
plugins: [
router,
createTestPinia(),
createTestI18n()
]
}
});
// 等待组件加载完成
await wrapper.vm.$nextTick();
expect(wrapper.find('.booking-list').exists()).toBe(true);
});
});
构建部署规范
环境配置
# .env.development
VITE_APP_TITLE=集运后台管理系统(开发环境)
VITE_API_BASE_URL=http://localhost:3000/api
VITE_UPLOAD_URL=http://localhost:3000/upload
VITE_APP_ENV=development
# .env.production
VITE_APP_TITLE=集运后台管理系统
VITE_API_BASE_URL=https://api.example.com
VITE_UPLOAD_URL=https://upload.example.com
VITE_APP_ENV=production
Vite 配置优化
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production';
return {
plugins: [
vue(),
// 生产环境下生成打包分析报告
isProduction && visualizer({
filename: 'dist/stats.html',
open: true
})
].filter(Boolean),
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'~': resolve(__dirname, 'src')
}
},
build: {
// 生产环境优化
minify: 'terser',
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
}
},
// 代码分割
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
elementPlus: ['element-plus'],
utils: ['axios', 'dayjs']
}
}
},
// 资源内联限制
assetsInlineLimit: 4096
},
// 开发服务器配置
server: {
port: 3001,
open: true,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
};
});
部署脚本
{
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build:dev": "vite build --mode development",
"build:prod": "vite build --mode production",
"preview": "vite preview",
"lint": "eslint src --ext .vue,.js,.ts --fix",
"lint:style": "stylelint src/**/*.{vue,css,scss} --fix",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"type-check": "vue-tsc --noEmit",
"deploy:dev": "npm run build:dev && npm run upload:dev",
"deploy:prod": "npm run build:prod && npm run upload:prod"
}
}
Git 工作流规范
分支管理
master/main # 主分支,用于生产环境
├── develop # 开发分支,用于集成测试
├── feature/* # 功能分支
├── bugfix/* # 修复分支
├── hotfix/* # 热修复分支
└── release/* # 发布分支
提交信息规范
<type>(<scope>): <subject>
<body>
<footer>
Type 类型:
feat: 新功能fix: 修复 bugdocs: 文档更新style: 代码格式调整refactor: 代码重构test: 测试相关chore: 构建过程或辅助工具的变动
示例:
feat(booking): add booking list pagination
- Add pagination component to booking list
- Update API to support pagination parameters
- Add loading state for better UX
Closes #123
Git Hooks
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.{vue,js,ts}": [
"eslint --fix",
"git add"
],
"*.{vue,css,scss}": [
"stylelint --fix",
"git add"
]
}
}
文档编写规范
README 结构
# 项目名称
项目简介和主要功能描述
## 技术栈
- Vue 3.3+
- TypeScript 5.0+
- Vite 4.0+
- Element Plus 2.3+
- Pinia 2.1+
## 快速开始
### 环境要求
- Node.js >= 18.0.0
- npm >= 9.0.0
### 安装依赖
```bash
npm install
启动开发服务器
npm run dev
构建生产版本
npm run build
项目结构
src/
├── api/ # API 接口
├── components/ # 组件
├── views/ # 页面
├── store/ # 状态管理
├── router/ # 路由配置
├── utils/ # 工具函数
└── types/ # 类型定义
开发规范
详见 开发规范文档
部署说明
详见 部署文档
贡献指南
- Fork 项目
- 创建功能分支
- 提交代码
- 创建 Pull Request
许可证
MIT License
### API 文档
```markdown
# API 接口文档
## 基础信息
- 基础 URL: `https://api.example.com`
- 认证方式: Bearer Token
- 数据格式: JSON
## 通用响应格式
```json
{
"code": 200,
"message": "success",
"data": {}
}
用户相关接口
用户登录
POST /auth/login
请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 用户名 |
| password | string | 是 | 密码 |
响应示例:
{
"code": 200,
"message": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"username": "admin",
"name": "管理员",
"role": "admin"
}
}
}
## 总结
本开发规范文档涵盖了集运后台管理系统开发的各个方面,包括:
1. **技术栈选择**:明确了项目使用的核心技术和版本要求
2. **项目结构**:规范了目录组织和文件命名
3. **代码规范**:定义了 TypeScript、Vue 3、组件开发的最佳实践
4. **架构设计**:规范了状态管理、路由管理、API 接口的设计模式
5. **样式规范**:统一了 CSS 编写和 Tailwind 使用规范
6. **国际化**:完整的多语言支持方案
7. **测试规范**:单元测试和集成测试的编写标准
8. **构建部署**:生产环境优化和部署流程
9. **团队协作**:Git 工作流和代码审查规范
10. **文档维护**:项目文档的编写和维护标准
遵循这些规范可以确保项目代码质量、团队协作效率和系统可维护性。
---
**文档版本**: v1.0
**最后更新**: 2024年12月
**维护者**: 前端开发团队