639 lines
10 KiB
Markdown
639 lines
10 KiB
Markdown
# 开发指南
|
|
|
|
## 开发环境搭建
|
|
|
|
### 1. 环境准备
|
|
|
|
```bash
|
|
# 安装 Node.js (版本 16.0+)
|
|
# 下载地址: https://nodejs.org/
|
|
|
|
# 安装 pnpm
|
|
npm install -g pnpm
|
|
|
|
# 验证安装
|
|
node --version
|
|
pnpm --version
|
|
```
|
|
|
|
### 2. 项目初始化
|
|
|
|
```bash
|
|
# 克隆项目
|
|
git clone <repository-url>
|
|
cd shop-front-end
|
|
|
|
# 安装依赖
|
|
pnpm install
|
|
|
|
# 启动开发服务器
|
|
pnpm dev
|
|
```
|
|
|
|
### 3. 开发工具配置
|
|
|
|
推荐使用 VSCode 并安装以下插件:
|
|
- Vue Language Features (Volar)
|
|
- TypeScript Vue Plugin (Volar)
|
|
- ESLint
|
|
- Prettier
|
|
- Stylelint
|
|
|
|
## 代码规范
|
|
|
|
### 1. 命名规范
|
|
|
|
#### 文件命名
|
|
- 组件文件: `PascalCase.vue`
|
|
- 工具文件: `camelCase.ts`
|
|
- 配置文件: `kebab-case.ts`
|
|
- 样式文件: `kebab-case.scss`
|
|
|
|
#### 变量命名
|
|
```typescript
|
|
// 正确
|
|
const userName = '张三'
|
|
const MAX_COUNT = 100
|
|
const userList = []
|
|
|
|
// 避免
|
|
const username = '张三' // 驼峰
|
|
const max_count = 100 // 下划线
|
|
```
|
|
|
|
#### 组件命名
|
|
```vue
|
|
<!-- 正确 -->
|
|
<template>
|
|
<UserProfile />
|
|
<GoodsList />
|
|
</template>
|
|
|
|
<!-- 避免 -->
|
|
<template>
|
|
<user-profile />
|
|
<goods-list />
|
|
</template>
|
|
```
|
|
|
|
### 2. 代码结构
|
|
|
|
#### Vue 组件结构
|
|
```vue
|
|
<template>
|
|
<!-- 模板内容 -->
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
// 导入
|
|
import { ref, onMounted } from 'vue'
|
|
import { getUserInfo } from '@/api/user'
|
|
|
|
// 类型定义
|
|
interface User {
|
|
id: number
|
|
name: string
|
|
}
|
|
|
|
// 响应式数据
|
|
const userInfo = ref<User>()
|
|
const loading = ref(false)
|
|
|
|
// 方法
|
|
const fetchUser = async () => {
|
|
loading.value = true
|
|
try {
|
|
const { data } = await getUserInfo()
|
|
userInfo.value = data
|
|
} catch (error) {
|
|
console.error(error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
fetchUser()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
/* 样式 */
|
|
</style>
|
|
```
|
|
|
|
### 3. TypeScript 规范
|
|
|
|
#### 类型定义
|
|
```typescript
|
|
// 接口定义
|
|
interface Goods {
|
|
id: number
|
|
name: string
|
|
price: number
|
|
stock: number
|
|
}
|
|
|
|
// 类型别名
|
|
type GoodsStatus = 'available' | 'sold_out' | 'disabled'
|
|
|
|
// 枚举
|
|
enum OrderStatus {
|
|
PENDING = 1,
|
|
PROCESSING = 2,
|
|
COMPLETED = 3
|
|
}
|
|
```
|
|
|
|
#### 函数定义
|
|
```typescript
|
|
// 带类型的函数
|
|
const formatPrice = (price: number): string => {
|
|
return `¥${price.toFixed(2)}`
|
|
}
|
|
|
|
// 异步函数
|
|
const fetchGoodsList = async (params: SearchParams): Promise<Goods[]> => {
|
|
const { data } = await getGoodsList(params)
|
|
return data
|
|
}
|
|
```
|
|
|
|
## API 开发规范
|
|
|
|
### 1. API 文件结构
|
|
|
|
```typescript
|
|
// src/api/shop/goods.ts
|
|
import request from '@/utils/request'
|
|
|
|
export interface Goods {
|
|
id: number
|
|
name: string
|
|
price: number
|
|
stock: number
|
|
}
|
|
|
|
export interface GoodsParams {
|
|
page?: number
|
|
size?: number
|
|
keyword?: string
|
|
}
|
|
|
|
export interface GoodsListResponse {
|
|
list: Goods[]
|
|
total: number
|
|
}
|
|
|
|
// 获取商品列表
|
|
export const getGoodsList = (params: GoodsParams) => {
|
|
return request.get<GoodsListResponse>('/api/goods/list', { params })
|
|
}
|
|
|
|
// 创建商品
|
|
export const createGoods = (data: Partial<Goods>) => {
|
|
return request.post<Goods>('/api/goods', data)
|
|
}
|
|
|
|
// 更新商品
|
|
export const updateGoods = (id: number, data: Partial<Goods>) => {
|
|
return request.put<Goods>(`/api/goods/${id}`, data)
|
|
}
|
|
|
|
// 删除商品
|
|
export const deleteGoods = (id: number) => {
|
|
return request.delete(`/api/goods/${id}`)
|
|
}
|
|
```
|
|
|
|
### 2. 请求封装
|
|
|
|
```typescript
|
|
// src/utils/request.ts
|
|
import axios from 'axios'
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
const request = axios.create({
|
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
timeout: 10000
|
|
})
|
|
|
|
// 请求拦截器
|
|
request.interceptors.request.use(
|
|
config => {
|
|
// 添加 token
|
|
const token = localStorage.getItem('token')
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`
|
|
}
|
|
return config
|
|
},
|
|
error => {
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
// 响应拦截器
|
|
request.interceptors.response.use(
|
|
response => {
|
|
return response.data
|
|
},
|
|
error => {
|
|
// 统一错误处理
|
|
ElMessage.error(error.response?.data?.message || '请求失败')
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
export default request
|
|
```
|
|
|
|
## 组件开发规范
|
|
|
|
### 1. 组件设计原则
|
|
|
|
#### 单一职责
|
|
每个组件只负责一个特定的功能。
|
|
|
|
#### 可复用性
|
|
组件应该设计为可复用的,通过 props 接收配置。
|
|
|
|
#### 组合优于继承
|
|
通过组合多个小组件来构建复杂功能。
|
|
|
|
### 2. Props 定义
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
// 必填属性
|
|
title: string
|
|
// 可选属性
|
|
size?: 'small' | 'medium' | 'large'
|
|
// 带默认值
|
|
disabled?: boolean
|
|
// 复杂对象
|
|
data?: Record<string, any>
|
|
}
|
|
|
|
// 定义 props
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
size: 'medium',
|
|
disabled: false,
|
|
data: () => ({})
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### 3. Emits 定义
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
interface Emits {
|
|
(e: 'update:modelValue', value: string): void
|
|
(e: 'submit', data: Record<string, any>): void
|
|
(e: 'cancel'): void
|
|
}
|
|
|
|
// 定义 emits
|
|
const emit = defineEmits<Emits>()
|
|
|
|
const handleSubmit = () => {
|
|
emit('submit', formData)
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
emit('cancel')
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## 状态管理规范
|
|
|
|
### 1. Store 定义
|
|
|
|
```typescript
|
|
// src/store/modules/user.ts
|
|
import { defineStore } from 'pinia'
|
|
|
|
export interface UserState {
|
|
userInfo: User | null
|
|
token: string | null
|
|
}
|
|
|
|
export const useUserStore = defineStore('user', {
|
|
state: (): UserState => ({
|
|
userInfo: null,
|
|
token: null
|
|
}),
|
|
|
|
getters: {
|
|
isLogin: state => !!state.token,
|
|
userName: state => state.userInfo?.name || ''
|
|
},
|
|
|
|
actions: {
|
|
setUserInfo(userInfo: User) {
|
|
this.userInfo = userInfo
|
|
},
|
|
|
|
setToken(token: string) {
|
|
this.token = token
|
|
},
|
|
|
|
async login(credentials: LoginParams) {
|
|
const { data } = await loginApi(credentials)
|
|
this.setToken(data.token)
|
|
this.setUserInfo(data.user)
|
|
},
|
|
|
|
logout() {
|
|
this.userInfo = null
|
|
this.token = null
|
|
}
|
|
},
|
|
|
|
persist: {
|
|
enabled: true,
|
|
strategies: [
|
|
{
|
|
key: 'user',
|
|
storage: localStorage
|
|
}
|
|
]
|
|
}
|
|
})
|
|
```
|
|
|
|
### 2. Store 使用
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { useUserStore } from '@/store/modules/user'
|
|
|
|
const userStore = useUserStore()
|
|
|
|
// 使用状态
|
|
const userName = computed(() => userStore.userName)
|
|
const isLogin = computed(() => userStore.isLogin)
|
|
|
|
// 调用 action
|
|
const handleLogin = async () => {
|
|
try {
|
|
await userStore.login(loginForm)
|
|
// 登录成功处理
|
|
} catch (error) {
|
|
// 错误处理
|
|
}
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## 路由开发规范
|
|
|
|
### 1. 路由定义
|
|
|
|
```typescript
|
|
// src/router/modules/shop.ts
|
|
export default {
|
|
path: '/shop',
|
|
name: 'Shop',
|
|
redirect: '/shop/goods',
|
|
meta: {
|
|
title: '店铺管理',
|
|
icon: 'shop',
|
|
rank: 10
|
|
},
|
|
children: [
|
|
{
|
|
path: '/shop/goods',
|
|
name: 'Goods',
|
|
component: () => import('@/views/shop/goods/index.vue'),
|
|
meta: {
|
|
title: '商品管理',
|
|
roles: ['admin', 'shop_manager']
|
|
}
|
|
},
|
|
{
|
|
path: '/shop/orders',
|
|
name: 'Orders',
|
|
component: () => import('@/views/shop/orders/index.vue'),
|
|
meta: {
|
|
title: '订单管理',
|
|
roles: ['admin', 'shop_manager']
|
|
}
|
|
}
|
|
]
|
|
} as RouteConfigsTable
|
|
```
|
|
|
|
### 2. 路由守卫
|
|
|
|
```typescript
|
|
// 在路由守卫中检查权限
|
|
router.beforeEach((to, from, next) => {
|
|
const userStore = useUserStore()
|
|
|
|
// 检查登录状态
|
|
if (!userStore.isLogin && to.path !== '/login') {
|
|
next('/login')
|
|
return
|
|
}
|
|
|
|
// 检查页面权限
|
|
if (to.meta.roles && !hasPermission(to.meta.roles)) {
|
|
next('/error/403')
|
|
return
|
|
}
|
|
|
|
next()
|
|
})
|
|
```
|
|
|
|
## 样式开发规范
|
|
|
|
### 1. SCSS 规范
|
|
|
|
```scss
|
|
// 变量定义
|
|
$primary-color: #409eff;
|
|
$success-color: #67c23a;
|
|
$warning-color: #e6a23c;
|
|
$danger-color: #f56c6c;
|
|
|
|
// 混入
|
|
@mixin flex-center {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
// 组件样式
|
|
.goods-card {
|
|
border: 1px solid #ebeef5;
|
|
border-radius: 4px;
|
|
padding: 20px;
|
|
|
|
&__header {
|
|
@include flex-center;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
&__title {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
color: $primary-color;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. CSS 类命名
|
|
|
|
使用 BEM 命名规范:
|
|
```scss
|
|
.block {}
|
|
.block__element {}
|
|
.block--modifier {}
|
|
```
|
|
|
|
## 测试规范
|
|
|
|
### 1. 单元测试
|
|
|
|
```typescript
|
|
// tests/unit/utils/format.spec.ts
|
|
import { formatPrice } from '@/utils/format'
|
|
|
|
describe('formatPrice', () => {
|
|
it('should format price correctly', () => {
|
|
expect(formatPrice(100)).toBe('¥100.00')
|
|
expect(formatPrice(99.9)).toBe('¥99.90')
|
|
})
|
|
})
|
|
```
|
|
|
|
### 2. 组件测试
|
|
|
|
```typescript
|
|
// tests/unit/components/GoodsCard.spec.ts
|
|
import { mount } from '@vue/test-utils'
|
|
import GoodsCard from '@/components/GoodsCard.vue'
|
|
|
|
describe('GoodsCard', () => {
|
|
it('should render goods info', () => {
|
|
const wrapper = mount(GoodsCard, {
|
|
props: {
|
|
goods: {
|
|
id: 1,
|
|
name: '测试商品',
|
|
price: 100
|
|
}
|
|
}
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('测试商品')
|
|
expect(wrapper.text()).toContain('¥100.00')
|
|
})
|
|
})
|
|
```
|
|
|
|
## 提交规范
|
|
|
|
### 1. Commit Message 格式
|
|
|
|
```
|
|
<type>(<scope>): <subject>
|
|
|
|
<body>
|
|
|
|
<footer>
|
|
```
|
|
|
|
### 2. 类型说明
|
|
|
|
- `feat`: 新功能
|
|
- `fix`: 修复 bug
|
|
- `docs`: 文档更新
|
|
- `style`: 代码格式调整
|
|
- `refactor`: 代码重构
|
|
- `test`: 测试相关
|
|
- `chore`: 构建过程或辅助工具变动
|
|
|
|
### 3. 示例
|
|
|
|
```
|
|
feat(shop): 添加商品批量导入功能
|
|
|
|
- 支持 Excel 文件导入商品
|
|
- 添加导入进度显示
|
|
- 优化导入错误处理
|
|
|
|
Closes #123
|
|
```
|
|
|
|
## 性能优化
|
|
|
|
### 1. 代码分割
|
|
|
|
```typescript
|
|
// 路由懒加载
|
|
const GoodsList = () => import('@/views/shop/goods/index.vue')
|
|
```
|
|
|
|
### 2. 组件优化
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// 使用 defineAsyncComponent 异步加载组件
|
|
const AsyncComponent = defineAsyncComponent(() =>
|
|
import('@/components/HeavyComponent.vue')
|
|
)
|
|
|
|
// 使用 computed 缓存计算结果
|
|
const filteredList = computed(() => {
|
|
return list.value.filter(item => item.status === 'active')
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### 3. 图片优化
|
|
|
|
```vue
|
|
<template>
|
|
<!-- 使用懒加载 -->
|
|
<el-image
|
|
:src="imageUrl"
|
|
lazy
|
|
:preview-src-list="[imageUrl]"
|
|
/>
|
|
</template>
|
|
```
|
|
|
|
## 常见问题
|
|
|
|
### 1. 类型错误
|
|
|
|
如果遇到 TypeScript 类型错误,检查:
|
|
- 是否正确导入类型定义
|
|
- 是否正确定义接口
|
|
- 是否使用正确的类型注解
|
|
|
|
### 2. 样式不生效
|
|
|
|
检查:
|
|
- 是否使用了正确的 scoped
|
|
- 是否正确定义了样式类名
|
|
- 是否引入了必要的样式文件
|
|
|
|
### 3. 路由跳转问题
|
|
|
|
检查:
|
|
- 路由路径是否正确
|
|
- 路由守卫是否阻止了跳转
|
|
- 组件是否正确定义
|
|
|
|
---
|
|
|
|
**文档版本**: 1.0
|
|
**最后更新**: 2025-10-15 |