18 KiB
18 KiB
学业邑规划 - 小程序开发规范
本文档定义了小程序前端的开发规范和编码标准,所有开发工作必须遵循这些规范。
一、命名规范
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 命名示例
// ✅ 建议
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 组件结构
<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 接口定义
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 接口定义
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 类型定义
// 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 类型
// 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 类型使用
// ✅ 建议:明确类型
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 请求封装
// 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 模块
// 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 调用
// 在组件中使用
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 定义
// 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 使用
// 在组件中使用
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 访问状态
const { userInfo, isLoggedIn } = storeToRefs(userStore)
// 调用方法
await userStore.loginAction(code, encryptedData, iv)
userStore.logout()
七、样式规范
7.1 变量定义
// 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 命名
// ✅ 建议:使用 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 响应式单位
// ✅ 使用 rpx 作为主要单位
.container {
padding: 24rpx;
font-size: 28rpx;
border-radius: 8rpx;
}
// ✅ 固定尺寸使用 px
.icon {
width: 24px;
height: 24px;
}
// ❌ 不建议混用单位
.box {
padding: 12px 24rpx; // 不要混用
}
八、页面开发流程
8.1 开发步骤
- 查看设计图 - 确认页面布局和交互
- 定义类型 - 在
types/下定义相关类型 - 编写 API - 在
api/下编写接口调用 - 创建页面 - 在
pages/下创建页面文件夹 - 配置路由 - 在
pages.json中添加页面配置 - 编写组件 - 抽取可复用组件
- 编写样式 - 使用 BEM 命名,引用变量
- 测试验证 - 在模拟器和真机测试
8.2 页面配置
// 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 格式化
// 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 验证
// 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 存储
// 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): 添加测评答题页面
- 实现题目列表展示
- 实现选项选择交互
- 添加提交答案功能