mi-assessment/uniapp/docs/开发规范.md
2026-02-09 08:02:54 +08:00

896 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 学业邑规划 - 小程序开发规范
本文档定义了小程序前端的开发规范和编码标准,所有开发工作必须遵循这些规范。
---
## 一、命名规范
### 1.1 文件命名
| 类型 | 规范 | 示例 |
|------|------|------|
| 页面文件夹 | kebab-case | `assessment-info/`, `order-list/` |
| 页面文件 | index.vue | `pages/login/index.vue` |
| 组件文件 | PascalCase | `UserAvatar.vue`, `OrderCard.vue` |
| 组合式函数 | use + camelCase | `useAuth.ts`, `usePayment.ts` |
| 工具函数 | camelCase | `format.ts`, `validate.ts` |
| 类型定义 | camelCase + .d.ts | `user.d.ts`, `api.d.ts` |
| 样式文件 | kebab-case | `variables.scss`, `common.scss` |
### 1.2 代码命名
| 类型 | 规范 | 示例 |
|------|------|------|
| 组件名 | PascalCase | `UserAvatar`, `OrderCard` |
| 变量/函数 | camelCase | `userInfo`, `handleSubmit` |
| 常量 | UPPER_SNAKE_CASE | `API_BASE_URL`, `MAX_PAGE_SIZE` |
| 类型/接口 | PascalCase | `UserInfo`, `OrderStatus` |
| 枚举 | PascalCase | `OrderStatus`, `UserLevel` |
| CSS 类名 | kebab-case | `user-avatar`, `order-card` |
| 事件名 | on + 动词 | `onClick`, `onSubmit`, `onChange` |
| Props | camelCase | `userId`, `showHeader` |
| Emits | kebab-case | `@update:value`, `@item-click` |
### 1.3 命名示例
```typescript
// ✅ 建议
const userInfo = ref<UserInfo | null>(null)
const isLoading = ref(false)
const orderList = ref<OrderItem[]>([])
function handleLogin() { }
function fetchUserInfo() { }
function formatPrice(price: number) { }
// ❌ 不建议
const user_info = ref(null) // 不使用下划线
const data = ref([]) // 命名太模糊
const info = ref(null) // 命名太模糊
function login() { } // 缺少动词前缀
```
---
## 二、项目结构规范
### 2.1 页面结构
每个页面文件夹包含:
```
pages/assessment/info/
├── index.vue # 页面主文件
├── components/ # 页面私有组件(可选)
│ └── InfoForm.vue
└── composables/ # 页面私有组合式函数(可选)
└── useInfoForm.ts
```
### 2.2 组件分类
```
components/
├── common/ # 通用组件(与业务无关)
│ ├── AppButton.vue # 按钮
│ ├── AppModal.vue # 弹窗
│ ├── AppEmpty.vue # 空状态
│ ├── AppLoading.vue # 加载
│ └── AppNavbar.vue # 导航栏
├── business/ # 业务组件(与业务相关)
│ ├── UserAvatar.vue # 用户头像
│ ├── OrderCard.vue # 订单卡片
│ ├── AssessmentCard.vue # 测评卡片
│ └── PlannerCard.vue # 规划师卡片
└── layout/ # 布局组件
└── TabBar.vue # 底部导航
```
---
## 三、Vue 组件规范
### 3.1 组件结构
```vue
<script setup lang="ts">
/**
* 组件名称
* @description 组件描述
*/
// 1. 导入
import { ref, computed, onMounted } from 'vue'
import type { UserInfo } from '@/types/user'
import { useUserStore } from '@/stores/user'
import { getUserInfo } from '@/api/user'
// 2. Props 定义
interface Props {
/** 用户ID */
userId: number
/** 是否显示头像 */
showAvatar?: boolean
}
const props = withDefaults(defineProps<Props>(), {
showAvatar: true
})
// 3. Emits 定义
interface Emits {
(e: 'click', user: UserInfo): void
(e: 'update:value', value: string): void
}
const emit = defineEmits<Emits>()
// 4. 响应式状态
const loading = ref(false)
const userInfo = ref<UserInfo | null>(null)
// 5. 计算属性
const displayName = computed(() => {
return userInfo.value?.nickname || '未知用户'
})
// 6. 方法
async function fetchData() {
loading.value = true
try {
userInfo.value = await getUserInfo(props.userId)
} finally {
loading.value = false
}
}
function handleClick() {
if (userInfo.value) {
emit('click', userInfo.value)
}
}
// 7. 生命周期
onMounted(() => {
fetchData()
})
// 8. 暴露方法(可选)
defineExpose({
refresh: fetchData
})
</script>
<template>
<view class="user-card" @click="handleClick">
<image
v-if="showAvatar"
class="user-card__avatar"
:src="userInfo?.avatar"
/>
<text class="user-card__name">{{ displayName }}</text>
</view>
</template>
<style lang="scss" scoped>
.user-card {
display: flex;
align-items: center;
padding: 24rpx;
&__avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
&__name {
margin-left: 16rpx;
font-size: 28rpx;
color: $text-color;
}
}
</style>
```
### 3.2 Props 规范
```typescript
// ✅ 建议:使用 TypeScript 接口定义
interface Props {
/** 用户ID必填 */
userId: number
/** 标题 */
title?: string
/** 是否显示 */
visible?: boolean
/** 列表数据 */
list?: OrderItem[]
}
const props = withDefaults(defineProps<Props>(), {
title: '默认标题',
visible: false,
list: () => []
})
// ❌ 不建议:使用运行时声明
const props = defineProps({
userId: {
type: Number,
required: true
}
})
```
### 3.3 Emits 规范
```typescript
// ✅ 建议:使用 TypeScript 接口定义
interface Emits {
(e: 'submit', data: FormData): void
(e: 'cancel'): void
(e: 'update:visible', value: boolean): void
}
const emit = defineEmits<Emits>()
// 触发事件
emit('submit', formData)
emit('update:visible', false)
```
---
## 四、TypeScript 规范
### 4.1 类型定义
```typescript
// types/user.d.ts
/** 用户等级 */
export enum UserLevel {
/** 普通用户 */
Normal = 1,
/** 合伙人 */
Partner = 2,
/** 渠道合伙人 */
ChannelPartner = 3
}
/** 用户信息 */
export interface UserInfo {
/** 用户ID */
id: number
/** 用户UID */
uid: string
/** 昵称 */
nickname: string
/** 头像 */
avatar: string
/** 手机号 */
phone: string
/** 用户等级 */
userLevel: UserLevel
/** 创建时间 */
createTime: string
}
/** 登录请求参数 */
export interface LoginRequest {
/** 微信 code */
code: string
/** 加密手机号数据 */
encryptedData: string
/** 加密向量 */
iv: string
}
/** 登录响应 */
export interface LoginResponse {
/** 访问令牌 */
token: string
/** 刷新令牌 */
refreshToken: string
/** 用户信息 */
userInfo: UserInfo
}
```
### 4.2 API 类型
```typescript
// types/api.d.ts
/** 通用响应结构 */
export interface ApiResponse<T = any> {
/** 状态码 */
code: number
/** 提示信息 */
message: string
/** 业务数据 */
data: T
}
/** 分页请求参数 */
export interface PageRequest {
/** 页码 */
page?: number
/** 每页数量 */
pageSize?: number
}
/** 分页响应 */
export interface PageResponse<T> {
/** 列表数据 */
list: T[]
/** 总数 */
total: number
/** 当前页 */
page: number
/** 每页数量 */
pageSize: number
/** 总页数 */
totalPages: number
}
```
### 4.3 类型使用
```typescript
// ✅ 建议:明确类型
const userInfo = ref<UserInfo | null>(null)
const orderList = ref<OrderItem[]>([])
const loading = ref<boolean>(false)
// ✅ 函数参数和返回值类型
function formatPrice(price: number): string {
return ${price.toFixed(2)}`
}
async function fetchOrders(params: PageRequest): Promise<PageResponse<OrderItem>> {
const res = await request.get<PageResponse<OrderItem>>('/api/order/getList', { params })
return res.data
}
// ❌ 不建议:使用 any
const data: any = {}
function handleData(data: any) { }
```
---
## 五、API 请求规范
### 5.1 请求封装
```typescript
// api/request.ts
import type { ApiResponse } from '@/types/api'
const BASE_URL = import.meta.env.VITE_API_BASE_URL
interface RequestOptions {
url: string
method?: 'GET' | 'POST'
data?: any
params?: any
showLoading?: boolean
showError?: boolean
}
class Request {
private getToken(): string {
return uni.getStorageSync('token') || ''
}
async request<T = any>(options: RequestOptions): Promise<ApiResponse<T>> {
const { url, method = 'GET', data, params, showLoading = true, showError = true } = options
if (showLoading) {
uni.showLoading({ title: '加载中...' })
}
try {
const res = await uni.request({
url: BASE_URL + url,
method,
data: method === 'POST' ? data : undefined,
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getToken()}`
}
})
const result = res.data as ApiResponse<T>
if (result.code !== 0) {
if (showError) {
uni.showToast({ title: result.message, icon: 'none' })
}
throw new Error(result.message)
}
return result
} finally {
if (showLoading) {
uni.hideLoading()
}
}
}
get<T = any>(url: string, options?: Partial<RequestOptions>) {
return this.request<T>({ url, method: 'GET', ...options })
}
post<T = any>(url: string, data?: any, options?: Partial<RequestOptions>) {
return this.request<T>({ url, method: 'POST', data, ...options })
}
}
export const request = new Request()
```
### 5.2 API 模块
```typescript
// api/user.ts
import { request } from './request'
import type { LoginRequest, LoginResponse, UserInfo } from '@/types/user'
/**
* 微信登录
*/
export function login(data: LoginRequest) {
return request.post<LoginResponse>('/api/user/login', data)
}
/**
* 获取用户信息
*/
export function getUserProfile() {
return request.get<UserInfo>('/api/user/getProfile')
}
/**
* 更新用户信息
*/
export function updateProfile(data: Partial<UserInfo>) {
return request.post<boolean>('/api/user/updateProfile', data)
}
```
### 5.3 API 调用
```typescript
// 在组件中使用
import { getUserProfile } from '@/api/user'
async function fetchUserInfo() {
try {
const res = await getUserProfile()
userInfo.value = res.data
} catch (error) {
console.error('获取用户信息失败', error)
}
}
```
---
## 六、状态管理规范
### 6.1 Store 定义
```typescript
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { UserInfo } from '@/types/user'
import { getUserProfile, login } from '@/api/user'
export const useUserStore = defineStore('user', () => {
// 状态
const token = ref<string>('')
const userInfo = ref<UserInfo | null>(null)
// 计算属性
const isLoggedIn = computed(() => !!token.value)
const isPartner = computed(() => (userInfo.value?.userLevel ?? 0) >= 2)
// 方法
async function loginAction(code: string, encryptedData: string, iv: string) {
const res = await login({ code, encryptedData, iv })
token.value = res.data.token
userInfo.value = res.data.userInfo
uni.setStorageSync('token', res.data.token)
}
async function fetchUserInfo() {
if (!token.value) return
const res = await getUserProfile()
userInfo.value = res.data
}
function logout() {
token.value = ''
userInfo.value = null
uni.removeStorageSync('token')
}
// 初始化
function init() {
token.value = uni.getStorageSync('token') || ''
if (token.value) {
fetchUserInfo()
}
}
return {
token,
userInfo,
isLoggedIn,
isPartner,
loginAction,
fetchUserInfo,
logout,
init
}
})
```
### 6.2 Store 使用
```typescript
// 在组件中使用
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 访问状态
const { userInfo, isLoggedIn } = storeToRefs(userStore)
// 调用方法
await userStore.loginAction(code, encryptedData, iv)
userStore.logout()
```
---
## 七、样式规范
### 7.1 变量定义
```scss
// styles/variables.scss
// 主题色
$primary-color: #4A90E2;
$primary-color-light: #6BA3E8;
$primary-color-dark: #3A7BC8;
// 文字颜色
$text-color: #333333;
$text-color-secondary: #666666;
$text-color-placeholder: #999999;
$text-color-disabled: #CCCCCC;
// 背景色
$bg-color: #F5F5F5;
$bg-color-white: #FFFFFF;
$bg-color-gray: #F8F8F8;
// 边框
$border-color: #EEEEEE;
$border-radius: 8rpx;
$border-radius-lg: 16rpx;
// 间距
$spacing-xs: 8rpx;
$spacing-sm: 16rpx;
$spacing-md: 24rpx;
$spacing-lg: 32rpx;
$spacing-xl: 48rpx;
// 字体大小
$font-size-xs: 20rpx;
$font-size-sm: 24rpx;
$font-size-md: 28rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;
// 安全区域
$safe-area-bottom: env(safe-area-inset-bottom);
```
### 7.2 BEM 命名
```scss
// ✅ 建议:使用 BEM 命名
.order-card {
// Block
padding: $spacing-md;
background: $bg-color-white;
// Element
&__header {
display: flex;
justify-content: space-between;
}
&__title {
font-size: $font-size-lg;
color: $text-color;
}
&__status {
font-size: $font-size-sm;
color: $text-color-secondary;
}
&__content {
margin-top: $spacing-sm;
}
&__footer {
margin-top: $spacing-md;
text-align: right;
}
// Modifier
&--active {
border-color: $primary-color;
}
&--disabled {
opacity: 0.5;
}
}
```
### 7.3 响应式单位
```scss
// ✅ 使用 rpx 作为主要单位
.container {
padding: 24rpx;
font-size: 28rpx;
border-radius: 8rpx;
}
// ✅ 固定尺寸使用 px
.icon {
width: 24px;
height: 24px;
}
// ❌ 不建议混用单位
.box {
padding: 12px 24rpx; // 不要混用
}
```
---
## 八、页面开发流程
### 8.1 开发步骤
1. **查看设计图** - 确认页面布局和交互
2. **定义类型** - 在 `types/` 下定义相关类型
3. **编写 API** - 在 `api/` 下编写接口调用
4. **创建页面** - 在 `pages/` 下创建页面文件夹
5. **配置路由** - 在 `pages.json` 中添加页面配置
6. **编写组件** - 抽取可复用组件
7. **编写样式** - 使用 BEM 命名,引用变量
8. **测试验证** - 在模拟器和真机测试
### 8.2 页面配置
```json
// pages.json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
}
},
{
"path": "pages/assessment/info/index",
"style": {
"navigationBarTitleText": "填写信息"
}
}
],
"tabBar": {
"color": "#999999",
"selectedColor": "#4A90E2",
"backgroundColor": "#FFFFFF",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/icons/tab-home.png",
"selectedIconPath": "static/icons/tab-home-active.png"
},
{
"pagePath": "pages/team/index",
"text": "团队",
"iconPath": "static/icons/tab-team.png",
"selectedIconPath": "static/icons/tab-team-active.png"
},
{
"pagePath": "pages/mine/index",
"text": "我的",
"iconPath": "static/icons/tab-mine.png",
"selectedIconPath": "static/icons/tab-mine-active.png"
}
]
}
}
```
---
## 九、常用工具函数
### 9.1 格式化
```typescript
// utils/format.ts
/**
* 格式化价格
*/
export function formatPrice(price: number): string {
return ${price.toFixed(2)}`
}
/**
* 格式化日期
*/
export function formatDate(date: string | Date, format = 'YYYY-MM-DD'): string {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hour = String(d.getHours()).padStart(2, '0')
const minute = String(d.getMinutes()).padStart(2, '0')
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hour)
.replace('mm', minute)
}
/**
* 格式化手机号隐藏中间4位
*/
export function formatPhone(phone: string): string {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
```
### 9.2 验证
```typescript
// utils/validate.ts
/**
* 验证手机号
*/
export function isValidPhone(phone: string): boolean {
return /^1[3-9]\d{9}$/.test(phone)
}
/**
* 验证邀请码5位大写字母
*/
export function isValidInviteCode(code: string): boolean {
return /^[A-Z]{5}$/.test(code)
}
```
### 9.3 存储
```typescript
// utils/storage.ts
const TOKEN_KEY = 'token'
const USER_INFO_KEY = 'userInfo'
export const storage = {
getToken(): string {
return uni.getStorageSync(TOKEN_KEY) || ''
},
setToken(token: string): void {
uni.setStorageSync(TOKEN_KEY, token)
},
removeToken(): void {
uni.removeStorageSync(TOKEN_KEY)
},
getUserInfo<T>(): T | null {
const data = uni.getStorageSync(USER_INFO_KEY)
return data ? JSON.parse(data) : null
},
setUserInfo<T>(info: T): void {
uni.setStorageSync(USER_INFO_KEY, JSON.stringify(info))
},
clear(): void {
uni.clearStorageSync()
}
}
```
---
## 十、注意事项
### 10.1 性能优化
- 图片使用 CDN 地址,避免本地大图
- 列表使用虚拟滚动或分页加载
- 避免在 `onShow` 中频繁请求数据
- 使用 `v-if` 而非 `v-show` 控制大组件显示
### 10.2 兼容性
- 使用 `rpx` 作为主要单位
- 避免使用不支持的 CSS 属性
- 测试 iOS 和 Android 两端表现
- 注意安全区域适配
### 10.3 安全
- 敏感数据不存储在本地
- Token 过期自动刷新或跳转登录
- 支付相关操作需二次确认
- 用户输入需做 XSS 过滤
---
## 十一、Git 提交规范
### 11.1 提交信息格式
```
<type>(<scope>): <subject>
<body>
```
### 11.2 Type 类型
| Type | 说明 |
|------|------|
| feat | 新功能 |
| fix | 修复 bug |
| docs | 文档更新 |
| style | 代码格式(不影响功能) |
| refactor | 重构 |
| perf | 性能优化 |
| test | 测试 |
| chore | 构建/工具 |
### 11.3 示例
```
feat(assessment): 添加测评答题页面
- 实现题目列表展示
- 实现选项选择交互
- 添加提交答案功能
```