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

18 KiB
Raw Blame History

学业邑规划 - 小程序开发规范

本文档定义了小程序前端的开发规范和编码标准,所有开发工作必须遵循这些规范。


一、命名规范

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 开发步骤

  1. 查看设计图 - 确认页面布局和交互
  2. 定义类型 - 在 types/ 下定义相关类型
  3. 编写 API - 在 api/ 下编写接口调用
  4. 创建页面 - 在 pages/ 下创建页面文件夹
  5. 配置路由 - 在 pages.json 中添加页面配置
  6. 编写组件 - 抽取可复用组件
  7. 编写样式 - 使用 BEM 命名,引用变量
  8. 测试验证 - 在模拟器和真机测试

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): 添加测评答题页面

- 实现题目列表展示
- 实现选项选择交互
- 添加提交答案功能