Files
niumalll/docs/小程序开发指南.md

21 KiB

小程序开发指南

版本历史

版本 日期 修改人 修改内容
1.0.0 2024-01-20 开发团队 初始版本

1. 小程序矩阵概述

1.1 小程序架构

本项目采用多小程序架构,包含四个独立的小程序:

  • 员工小程序 (staff-mp): 内部员工使用,管理订单、质检、运输等
  • 司机小程序 (driver-mp): 运输司机使用,管理运输任务和状态更新
  • 供应商小程序 (supplier-mp): 供应商使用,管理订单接收和牛只信息
  • 客户小程序 (client-mp): 客户使用,下单、查看订单状态等

1.2 技术栈

  • 框架: uni-app (Vue3 + TypeScript)
  • UI组件: uView Plus
  • 状态管理: Pinia
  • HTTP请求: uni.request 封装
  • 构建工具: Vite

2. 开发环境搭建

2.1 工具安装

# 安装 HBuilderX (推荐)
# 下载地址: https://www.dcloud.io/hbuilderx.html

# 或使用 VS Code + uni-app 插件
# 安装 uni-app 插件

2.2 微信开发者工具

# 下载微信开发者工具
# 下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html

# 配置小程序 AppID
# 在各小程序的 manifest.json 中配置对应的 AppID

2.3 项目初始化

# 进入小程序目录
cd mini_program/staff-mp

# 安装依赖
npm install

# 启动开发服务器
npm run dev:mp-weixin

3. 项目结构详解

3.1 员工小程序 (staff-mp)

staff-mp/
├── src/
│   ├── pages/              # 页面
│   │   ├── index/          # 首页
│   │   ├── order/          # 订单管理
│   │   │   ├── list.vue    # 订单列表
│   │   │   ├── detail.vue  # 订单详情
│   │   │   └── create.vue  # 创建订单
│   │   ├── quality/        # 质检管理
│   │   ├── transport/      # 运输管理
│   │   └── profile/        # 个人中心
│   ├── components/         # 组件
│   │   ├── OrderCard.vue   # 订单卡片
│   │   ├── QualityForm.vue # 质检表单
│   │   └── StatusBadge.vue # 状态标签
│   ├── api/               # API接口
│   │   ├── order.ts       # 订单接口
│   │   ├── quality.ts     # 质检接口
│   │   └── transport.ts   # 运输接口
│   ├── stores/            # 状态管理
│   │   ├── user.ts        # 用户状态
│   │   ├── order.ts       # 订单状态
│   │   └── app.ts         # 应用状态
│   ├── utils/             # 工具函数
│   │   ├── request.ts     # 请求封装
│   │   ├── auth.ts        # 认证工具
│   │   └── format.ts      # 格式化工具
│   ├── static/            # 静态资源
│   ├── App.vue            # 应用入口
│   ├── main.ts            # 主文件
│   └── pages.json         # 页面配置
├── package.json
└── tsconfig.json

3.2 司机小程序 (driver-mp)

driver-mp/
├── src/
│   ├── pages/
│   │   ├── index/          # 首页
│   │   ├── task/           # 运输任务
│   │   │   ├── list.vue    # 任务列表
│   │   │   ├── detail.vue  # 任务详情
│   │   │   └── track.vue   # 运输跟踪
│   │   ├── vehicle/        # 车辆管理
│   │   └── profile/        # 个人中心
│   └── ...

3.3 供应商小程序 (supplier-mp)

supplier-mp/
├── src/
│   ├── pages/
│   │   ├── index/          # 首页
│   │   ├── order/          # 订单管理
│   │   ├── cattle/         # 牛只管理
│   │   └── profile/        # 个人中心
│   └── ...

3.4 客户小程序 (client-mp)

client-mp/
├── src/
│   ├── pages/
│   │   ├── index/          # 首页
│   │   ├── order/          # 订单管理
│   │   ├── payment/        # 支付管理
│   │   └── profile/        # 个人中心
│   └── ...

4. 开发规范

4.1 页面开发规范

4.1.1 页面结构

<template>
  <view class="page-container">
    <!-- 页面内容 -->
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'

// 页面逻辑
const userStore = useUserStore()

onMounted(() => {
  // 页面初始化逻辑
})
</script>

<style lang="scss" scoped>
.page-container {
  padding: 20rpx;
}
</style>

4.1.2 组件开发规范

<template>
  <view class="order-card">
    <view class="order-header">
      <text class="order-id">{{ order.id }}</text>
      <StatusBadge :status="order.status" />
    </view>
    <view class="order-content">
      <!-- 订单内容 -->
    </view>
  </view>
</template>

<script setup lang="ts">
interface Props {
  order: OrderInfo
}

interface OrderInfo {
  id: string
  status: string
  // 其他字段...
}

defineProps<Props>()
</script>

4.2 API 调用规范

4.2.1 请求封装

// utils/request.ts
import { useUserStore } from '@/stores/user'

interface RequestOptions {
  url: string
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  data?: any
  header?: Record<string, string>
}

export function request<T = any>(options: RequestOptions): Promise<T> {
  const userStore = useUserStore()
  
  return new Promise((resolve, reject) => {
    uni.request({
      url: `${import.meta.env.VITE_API_BASE_URL}${options.url}`,
      method: options.method || 'GET',
      data: options.data,
      header: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${userStore.token}`,
        ...options.header
      },
      success: (res) => {
        if (res.statusCode === 200) {
          resolve(res.data as T)
        } else {
          reject(new Error(`请求失败: ${res.statusCode}`))
        }
      },
      fail: (err) => {
        reject(err)
      }
    })
  })
}

4.2.2 API 接口定义

// api/order.ts
import { request } from '@/utils/request'

export interface OrderListParams {
  page: number
  pageSize: number
  status?: string
}

export interface OrderInfo {
  id: string
  customerName: string
  status: string
  createTime: string
  // 其他字段...
}

// 获取订单列表
export function getOrderList(params: OrderListParams) {
  return request<{
    list: OrderInfo[]
    total: number
  }>({
    url: '/orders',
    method: 'GET',
    data: params
  })
}

// 获取订单详情
export function getOrderDetail(id: string) {
  return request<OrderInfo>({
    url: `/orders/${id}`,
    method: 'GET'
  })
}

4.3 状态管理规范

// stores/order.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { OrderInfo } from '@/api/order'

export const useOrderStore = defineStore('order', () => {
  const orderList = ref<OrderInfo[]>([])
  const currentOrder = ref<OrderInfo | null>(null)
  const loading = ref(false)

  // 获取订单列表
  async function fetchOrderList(params: any) {
    loading.value = true
    try {
      const { getOrderList } = await import('@/api/order')
      const result = await getOrderList(params)
      orderList.value = result.list
      return result
    } finally {
      loading.value = false
    }
  }

  // 设置当前订单
  function setCurrentOrder(order: OrderInfo) {
    currentOrder.value = order
  }

  return {
    orderList,
    currentOrder,
    loading,
    fetchOrderList,
    setCurrentOrder
  }
})

5. 页面开发指南

5.1 员工小程序页面

5.1.1 订单管理页面

<!-- pages/order/list.vue -->
<template>
  <view class="order-list-page">
    <!-- 搜索栏 -->
    <view class="search-bar">
      <u-search 
        v-model="searchKeyword" 
        placeholder="搜索订单号或客户名称"
        @search="handleSearch"
      />
    </view>

    <!-- 状态筛选 -->
    <view class="filter-tabs">
      <u-tabs 
        v-model="activeTab" 
        :list="statusTabs"
        @change="handleTabChange"
      />
    </view>

    <!-- 订单列表 -->
    <view class="order-list">
      <OrderCard 
        v-for="order in orderList" 
        :key="order.id"
        :order="order"
        @click="handleOrderClick"
      />
    </view>

    <!-- 加载更多 -->
    <u-loadmore 
      :status="loadStatus"
      @loadmore="loadMore"
    />
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useOrderStore } from '@/stores/order'
import OrderCard from '@/components/OrderCard.vue'

const orderStore = useOrderStore()
const searchKeyword = ref('')
const activeTab = ref(0)
const loadStatus = ref('loadmore')

const statusTabs = [
  { name: '全部' },
  { name: '待确认' },
  { name: '进行中' },
  { name: '已完成' }
]

// 搜索处理
function handleSearch() {
  // 搜索逻辑
}

// 标签切换
function handleTabChange(index: number) {
  activeTab.value = index
  // 重新加载数据
}

// 订单点击
function handleOrderClick(order: any) {
  uni.navigateTo({
    url: `/pages/order/detail?id=${order.id}`
  })
}

// 加载更多
function loadMore() {
  // 加载更多逻辑
}

onMounted(() => {
  // 初始化数据
  orderStore.fetchOrderList({ page: 1, pageSize: 20 })
})
</script>

5.1.2 质检管理页面

<!-- pages/quality/check.vue -->
<template>
  <view class="quality-check-page">
    <u-form 
      :model="formData" 
      ref="formRef"
      :rules="rules"
    >
      <!-- 基本信息 -->
      <u-form-item label="订单号" prop="orderId">
        <u-input v-model="formData.orderId" disabled />
      </u-form-item>

      <!-- 质检项目 -->
      <u-form-item label="体重检测" prop="weight">
        <u-input 
          v-model="formData.weight" 
          type="number"
          placeholder="请输入体重(kg)"
        />
      </u-form-item>

      <!-- 健康状况 -->
      <u-form-item label="健康状况" prop="healthStatus">
        <u-radio-group v-model="formData.healthStatus">
          <u-radio 
            v-for="item in healthOptions" 
            :key="item.value"
            :label="item.label"
            :name="item.value"
          />
        </u-radio-group>
      </u-form-item>

      <!-- 照片上传 -->
      <u-form-item label="质检照片">
        <u-upload 
          :fileList="fileList"
          @afterRead="afterRead"
          @delete="deleteFile"
          multiple
          :maxCount="5"
        />
      </u-form-item>

      <!-- 备注 -->
      <u-form-item label="备注">
        <u-textarea 
          v-model="formData.remark"
          placeholder="请输入质检备注"
        />
      </u-form-item>
    </u-form>

    <!-- 提交按钮 -->
    <view class="submit-section">
      <u-button 
        type="primary" 
        @click="handleSubmit"
        :loading="submitting"
      >
        提交质检报告
      </u-button>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'

const formRef = ref()
const submitting = ref(false)
const fileList = ref([])

const formData = reactive({
  orderId: '',
  weight: '',
  healthStatus: '',
  remark: ''
})

const rules = {
  weight: [
    { required: true, message: '请输入体重', trigger: 'blur' }
  ],
  healthStatus: [
    { required: true, message: '请选择健康状况', trigger: 'change' }
  ]
}

const healthOptions = [
  { label: '健康', value: 'healthy' },
  { label: '一般', value: 'normal' },
  { label: '异常', value: 'abnormal' }
]

// 文件上传
function afterRead(event: any) {
  // 处理文件上传
}

// 删除文件
function deleteFile(event: any) {
  // 处理文件删除
}

// 提交表单
async function handleSubmit() {
  try {
    await formRef.value.validate()
    submitting.value = true
    
    // 提交质检数据
    // await submitQualityCheck(formData)
    
    uni.showToast({
      title: '提交成功',
      icon: 'success'
    })
    
    // 返回上一页
    uni.navigateBack()
  } catch (error) {
    console.error('提交失败:', error)
  } finally {
    submitting.value = false
  }
}
</script>

5.2 司机小程序页面

5.2.1 运输任务页面

<!-- pages/task/list.vue -->
<template>
  <view class="task-list-page">
    <!-- 任务统计 -->
    <view class="task-stats">
      <view class="stat-item">
        <text class="stat-number">{{ taskStats.pending }}</text>
        <text class="stat-label">待接单</text>
      </view>
      <view class="stat-item">
        <text class="stat-number">{{ taskStats.inProgress }}</text>
        <text class="stat-label">进行中</text>
      </view>
      <view class="stat-item">
        <text class="stat-number">{{ taskStats.completed }}</text>
        <text class="stat-label">已完成</text>
      </view>
    </view>

    <!-- 任务列表 -->
    <view class="task-list">
      <TaskCard 
        v-for="task in taskList" 
        :key="task.id"
        :task="task"
        @accept="handleAcceptTask"
        @start="handleStartTask"
        @complete="handleCompleteTask"
      />
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import TaskCard from '@/components/TaskCard.vue'

const taskStats = ref({
  pending: 0,
  inProgress: 0,
  completed: 0
})

const taskList = ref([])

// 接受任务
function handleAcceptTask(task: any) {
  uni.showModal({
    title: '确认接单',
    content: '确定要接受这个运输任务吗?',
    success: (res) => {
      if (res.confirm) {
        // 接受任务逻辑
      }
    }
  })
}

// 开始运输
function handleStartTask(task: any) {
  uni.navigateTo({
    url: `/pages/task/transport?id=${task.id}`
  })
}

// 完成任务
function handleCompleteTask(task: any) {
  uni.navigateTo({
    url: `/pages/task/complete?id=${task.id}`
  })
}

onMounted(() => {
  // 加载任务数据
})
</script>

6. 组件开发

6.1 通用组件

6.1.1 状态标签组件

<!-- components/StatusBadge.vue -->
<template>
  <view :class="['status-badge', `status-${status}`]">
    <text class="status-text">{{ statusText }}</text>
  </view>
</template>

<script setup lang="ts">
import { computed } from 'vue'

interface Props {
  status: string
}

const props = defineProps<Props>()

const statusText = computed(() => {
  const statusMap: Record<string, string> = {
    'pending': '待处理',
    'processing': '处理中',
    'completed': '已完成',
    'cancelled': '已取消'
  }
  return statusMap[props.status] || '未知状态'
})
</script>

<style lang="scss" scoped>
.status-badge {
  display: inline-flex;
  align-items: center;
  padding: 4rpx 12rpx;
  border-radius: 12rpx;
  font-size: 24rpx;

  &.status-pending {
    background-color: #fff3cd;
    color: #856404;
  }

  &.status-processing {
    background-color: #d1ecf1;
    color: #0c5460;
  }

  &.status-completed {
    background-color: #d4edda;
    color: #155724;
  }

  &.status-cancelled {
    background-color: #f8d7da;
    color: #721c24;
  }
}
</style>

6.1.2 订单卡片组件

<!-- components/OrderCard.vue -->
<template>
  <view class="order-card" @click="handleClick">
    <view class="card-header">
      <text class="order-id">订单号: {{ order.id }}</text>
      <StatusBadge :status="order.status" />
    </view>
    
    <view class="card-content">
      <view class="info-row">
        <text class="label">客户名称:</text>
        <text class="value">{{ order.customerName }}</text>
      </view>
      <view class="info-row">
        <text class="label">数量:</text>
        <text class="value">{{ order.quantity }}</text>
      </view>
      <view class="info-row">
        <text class="label">创建时间:</text>
        <text class="value">{{ formatTime(order.createTime) }}</text>
      </view>
    </view>

    <view class="card-actions" v-if="showActions">
      <u-button 
        size="small" 
        type="primary" 
        @click.stop="handleAction('detail')"
      >
        查看详情
      </u-button>
      <u-button 
        size="small" 
        @click.stop="handleAction('edit')"
        v-if="order.status === 'pending'"
      >
        编辑
      </u-button>
    </view>
  </view>
</template>

<script setup lang="ts">
import { formatTime } from '@/utils/format'
import StatusBadge from './StatusBadge.vue'

interface Props {
  order: {
    id: string
    customerName: string
    quantity: number
    status: string
    createTime: string
  }
  showActions?: boolean
}

interface Emits {
  (e: 'click', order: any): void
  (e: 'action', action: string, order: any): void
}

const props = withDefaults(defineProps<Props>(), {
  showActions: true
})

const emit = defineEmits<Emits>()

function handleClick() {
  emit('click', props.order)
}

function handleAction(action: string) {
  emit('action', action, props.order)
}
</script>

<style lang="scss" scoped>
.order-card {
  background: #fff;
  border-radius: 12rpx;
  padding: 24rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);

  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20rpx;

    .order-id {
      font-size: 28rpx;
      font-weight: bold;
      color: #333;
    }
  }

  .card-content {
    .info-row {
      display: flex;
      margin-bottom: 12rpx;

      .label {
        width: 160rpx;
        font-size: 26rpx;
        color: #666;
      }

      .value {
        flex: 1;
        font-size: 26rpx;
        color: #333;
      }
    }
  }

  .card-actions {
    display: flex;
    gap: 20rpx;
    margin-top: 20rpx;
    padding-top: 20rpx;
    border-top: 1rpx solid #eee;
  }
}
</style>

7. 工具函数

7.1 认证工具

// utils/auth.ts
import { useUserStore } from '@/stores/user'

// 检查登录状态
export function checkAuth(): boolean {
  const userStore = useUserStore()
  return !!userStore.token
}

// 跳转到登录页
export function redirectToLogin() {
  uni.navigateTo({
    url: '/pages/auth/login'
  })
}

// 登出
export function logout() {
  const userStore = useUserStore()
  userStore.clearUserInfo()
  
  uni.reLaunch({
    url: '/pages/auth/login'
  })
}

// 微信登录
export function wxLogin(): Promise<string> {
  return new Promise((resolve, reject) => {
    uni.login({
      provider: 'weixin',
      success: (res) => {
        resolve(res.code)
      },
      fail: (err) => {
        reject(err)
      }
    })
  })
}

7.2 格式化工具

// utils/format.ts
import dayjs from 'dayjs'

// 格式化时间
export function formatTime(time: string | Date, format = 'YYYY-MM-DD HH:mm:ss'): string {
  return dayjs(time).format(format)
}

// 格式化金额
export function formatMoney(amount: number): string {
  return ${amount.toFixed(2)}`
}

// 格式化手机号
export function formatPhone(phone: string): string {
  return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}

// 格式化文件大小
export function formatFileSize(size: number): string {
  if (size < 1024) {
    return `${size}B`
  } else if (size < 1024 * 1024) {
    return `${(size / 1024).toFixed(1)}KB`
  } else {
    return `${(size / (1024 * 1024)).toFixed(1)}MB`
  }
}

8. 调试与测试

8.1 真机调试

# 生成预览二维码
npm run build:mp-weixin
# 在微信开发者工具中预览

8.2 性能监控

// utils/performance.ts
export function trackPagePerformance(pageName: string) {
  const startTime = Date.now()
  
  return {
    end: () => {
      const endTime = Date.now()
      const duration = endTime - startTime
      
      console.log(`页面 ${pageName} 加载耗时: ${duration}ms`)
      
      // 上报性能数据
      // reportPerformance(pageName, duration)
    }
  }
}

8.3 错误监控

// utils/error.ts
export function setupErrorHandler() {
  // 全局错误处理
  uni.onError((error) => {
    console.error('小程序错误:', error)
    
    // 上报错误信息
    // reportError(error)
  })
  
  // 未处理的 Promise 错误
  uni.onUnhandledRejection((event) => {
    console.error('未处理的 Promise 错误:', event.reason)
    
    // 上报错误信息
    // reportError(event.reason)
  })
}

9. 发布部署

9.1 构建配置

// package.json
{
  "scripts": {
    "dev:mp-weixin": "uni -p mp-weixin",
    "build:mp-weixin": "uni build -p mp-weixin",
    "dev:h5": "uni -p h5",
    "build:h5": "uni build -p h5"
  }
}

9.2 发布流程

  1. 代码检查
npm run lint
npm run type-check
  1. 构建生产版本
npm run build:mp-weixin
  1. 上传代码
  • 使用微信开发者工具上传代码
  • 填写版本号和更新说明
  1. 提交审核
  • 在微信公众平台提交审核
  • 等待审核通过后发布

10. 常见问题

10.1 开发问题

Q: 小程序白屏问题

  • 检查页面路径配置
  • 查看控制台错误信息
  • 确认组件引入是否正确

Q: 网络请求失败

  • 检查域名配置
  • 确认 HTTPS 证书
  • 查看请求参数格式

10.2 性能问题

Q: 页面加载慢

  • 优化图片大小
  • 减少组件层级
  • 使用分包加载

Q: 内存占用高

  • 及时清理定时器
  • 避免内存泄漏
  • 优化数据结构

11. 联系信息

相关文档