# 学业邑规划 - 小程序开发规范 本文档定义了小程序前端的开发规范和编码标准,所有开发工作必须遵循这些规范。 --- ## 一、命名规范 ### 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(null) const isLoading = ref(false) const orderList = ref([]) 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 ``` ### 3.2 Props 规范 ```typescript // ✅ 建议:使用 TypeScript 接口定义 interface Props { /** 用户ID(必填) */ userId: number /** 标题 */ title?: string /** 是否显示 */ visible?: boolean /** 列表数据 */ list?: OrderItem[] } const props = withDefaults(defineProps(), { 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() // 触发事件 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 { /** 状态码 */ code: number /** 提示信息 */ message: string /** 业务数据 */ data: T } /** 分页请求参数 */ export interface PageRequest { /** 页码 */ page?: number /** 每页数量 */ pageSize?: number } /** 分页响应 */ export interface PageResponse { /** 列表数据 */ list: T[] /** 总数 */ total: number /** 当前页 */ page: number /** 每页数量 */ pageSize: number /** 总页数 */ totalPages: number } ``` ### 4.3 类型使用 ```typescript // ✅ 建议:明确类型 const userInfo = ref(null) const orderList = ref([]) const loading = ref(false) // ✅ 函数参数和返回值类型 function formatPrice(price: number): string { return `¥${price.toFixed(2)}` } async function fetchOrders(params: PageRequest): Promise> { const res = await request.get>('/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(options: RequestOptions): Promise> { 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 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(url: string, options?: Partial) { return this.request({ url, method: 'GET', ...options }) } post(url: string, data?: any, options?: Partial) { return this.request({ 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('/api/user/login', data) } /** * 获取用户信息 */ export function getUserProfile() { return request.get('/api/user/getProfile') } /** * 更新用户信息 */ export function updateProfile(data: Partial) { return request.post('/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('') const userInfo = ref(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 | null { const data = uni.getStorageSync(USER_INFO_KEY) return data ? JSON.parse(data) : null }, setUserInfo(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 提交信息格式 ``` (): ``` ### 11.2 Type 类型 | Type | 说明 | |------|------| | feat | 新功能 | | fix | 修复 bug | | docs | 文档更新 | | style | 代码格式(不影响功能) | | refactor | 重构 | | perf | 性能优化 | | test | 测试 | | chore | 构建/工具 | ### 11.3 示例 ``` feat(assessment): 添加测评答题页面 - 实现题目列表展示 - 实现选项选择交互 - 添加提交答案功能 ```