17 KiB
17 KiB
现场回收 uni-app 程序设计文档
一、项目概述
1.1 项目背景
开发一套基于 uni-app 的跨平台现场回收程序,用于回收人员现场回收作业,支持实时称重、价格计算、蓝牙电子秤连接等功能。
1.2 技术栈
- 框架: uni-app (Vue 3 + Vite)
- UI组件: uni-ui / uView UI
- 状态管理: Pinia
- 蓝牙通信: uni-app 蓝牙 API
- 数据存储: 本地存储 (uni.getStorageSync) + 云端API
- 跨平台: 支持 iOS、Android、H5(调试用)
二、功能需求
2.1 核心功能模块
模块1:回收种类选择
回收种类(暂定):
- 纸类 - 按重量(kg)计价
- 铁 - 按重量(kg)计价
- 易拉罐 - 按个数计价
- 塑料 - 按重量(kg)计价
- 织物 - 按重量(kg)计价
功能点:
- 展示回收种类列表(图标+名称)
- 显示当日回收价格
- 支持搜索/筛选(未来扩展)
- 选择后进入称重/计数界面
模块2:当日回收价格管理
数据来源:
- 云端API获取(管理员后台配置)
- 本地缓存(离线可用)
价格展示:
- 列表页:每种回收物的当日价格
- 详情页:价格趋势(可选)
数据格式示例:
{
"date": "2026-05-14",
"prices": [
{"category": "paper", "name": "纸类", "unit": "kg", "price": 1.2},
{"category": "iron", "name": "铁", "unit": "kg", "price": 2.5},
{"category": "can", "name": "易拉罐", "unit": "piece", "price": 0.1},
{"category": "plastic", "name": "塑料", "unit": "kg", "price": 3.0},
{"category": "fabric", "name": "织物", "unit": "kg", "price": 0.8}
]
}
模块3:蓝牙电子秤连接
功能流程:
- 搜索蓝牙设备
- 连接指定电子秤(需知道设备名称/MAC地址)
- 实时接收重量数据
- 断线重连机制
- 手动/自动归零
蓝牙通信协议(需根据实际电子秤调整):
- 服务UUID:
0xFFE0(示例) - 特征UUID:
0xFFE1(示例) - 数据格式:通常返回 ASCII 字符串,如
"+00123kg\n"
关键代码逻辑:
// 监听蓝牙数据
uni.onBLECharacteristicValueChange((res) => {
const raw = this.ab2str(res.value)
const weight = this.parseWeight(raw) // 解析重量
this.currentWeight = weight
this.calculateAmount() // 触发金额计算
})
模块4:重量/数量获取与金额计算
业务逻辑:
| 回收种类 | 计价单位 | 计算方式 |
|---|---|---|
| 纸类 | kg | 重量(kg) × 单价 |
| 铁 | kg | 重量(kg) × 单价 |
| 易拉罐 | 个 | 数量(个) × 单价 |
| 塑料 | kg | 重量(kg) × 单价 |
| 织物 | kg | 重量(kg) × 单价 |
计算逻辑:
function calculateAmount(category, weightOrCount, unitPrice) {
if (category === 'can') {
return weightOrCount * unitPrice // 按个数
} else {
return weightOrCount * unitPrice // 按重量
}
}
实时计算:
- 重量变化时自动重新计算
- 显示预估金额(大字醒目)
- 支持手动修改重量/数量(特殊情况)
模块5:订单管理
订单数据模型:
{
"orderId": "ORD202605141430001",
"timestamp": 1715668200000,
"items": [
{
"category": "paper",
"name": "纸类",
"weight": 12.5,
"unit": "kg",
"unitPrice": 1.2,
"amount": 15.0
}
],
"totalAmount": 15.0,
"status": "completed",
"location": {
"latitude": 31.2304,
"longitude": 121.4737
}
}
功能点:
- 创建订单(一键生成)
- 订单列表(历史记录)
- 订单详情
- 导出/打印(可选)
三、页面设计
3.1 页面结构
pages/
├── index/
│ └── index.vue # 首页(回收种类选择)
├── price/
│ └── price.vue # 当日价格表
├── weigh/
│ └── weigh.vue # 称重/计数页面
├── bluetooth/
│ └── bluetooth.vue # 蓝牙连接管理
├── order/
│ ├── list.vue # 订单列表
│ └── detail.vue # 订单详情
└── user/
└── profile.vue # 个人中心
3.2 关键页面原型
首页(index.vue)
┌─────────────────────────┐
│ 🏠 现场回收系统 │
│ 📅 2026-05-14 周四 │
├─────────────────────────┤
│ 📊 今日价格 > │
├─────────────────────────┤
│ 🗂️ 选择回收种类 │
│ │
│ [📄 纸类] [🔩 铁] │
│ 1.2元/kg 2.5元/kg │
│ │
│ [🥫 易拉罐] [♻️ 塑料] │
│ 0.1元/个 3.0元/kg │
│ │
│ [👕 织物] │
│ 0.8元/kg │
└─────────────────────────┘
称重页面(weigh.vue)
┌─────────────────────────┐
│ ← 返回 🔵已连接 │
├─────────────────────────┤
│ 回收种类:纸类 │
│ 单价:1.2元/kg │
├─────────────────────────┤
│ │
│ 📏 实时重量 │
│ │
│ [ 12.5 ] kg │
│ │
│ 💰 预估金额 │
│ [ 15.00 ] 元 │
│ │
│ [📱 蓝牙连接] [⚖️ 归零]│
│ │
│ [ ✅ 确认回收 ] │
└─────────────────────────┘
易拉罐特殊页面(weigh.vue 变体)
┌─────────────────────────┐
│ ← 返回 │
├─────────────────────────┤
│ 回收种类:易拉罐 │
│ 单价:0.1元/个 │
├─────────────────────────┤
│ │
│ 🥫 个数统计 │
│ │
│ [ 25 ] 个 │
│ [ + ] [ - ] │
│ │
│ 💰 预估金额 │
│ [ 2.50 ] 元 │
│ │
│ [ ✅ 确认回收 ] │
└─────────────────────────┘
四、技术实现细节
4.1 蓝牙连接实现
步骤1:初始化蓝牙
async initBluetooth() {
try {
await uni.openBluetoothAdapter()
console.log('蓝牙适配器初始化成功')
} catch (err) {
console.error('请打开蓝牙', err)
}
}
步骤2:搜索设备
startDiscovery() {
uni.startBluetoothDevicesDiscovery({
services: [], // 不指定服务,搜索所有设备
success: (res) => {
console.log('开始搜索蓝牙设备')
this.listenDeviceFound()
}
})
}
listenDeviceFound() {
uni.onBluetoothDeviceFound((res) => {
const devices = res.devices.filter(d =>
d.name && d.name.includes('Scale') // 过滤电子秤设备
)
this.deviceList = [...this.deviceList, ...devices]
})
}
步骤3:连接设备
async connectDevice(deviceId) {
try {
await uni.createBLEConnection({ deviceId })
this.deviceId = deviceId
this.getBLEDeviceServices(deviceId)
} catch (err) {
console.error('连接失败', err)
}
}
步骤4:监听数据
async getBLEDeviceServices(deviceId) {
const res = await uni.getBLEDeviceServices({ deviceId })
const service = res.services.find(s => s.uuid === '0000FFE0-0000-1000-8000-00805F9B34FB')
if (service) {
this.getBLEDeviceCharacteristics(deviceId, service.uuid)
}
}
async getBLEDeviceCharacteristics(deviceId, serviceId) {
const res = await uni.getBLEDeviceCharacteristics({ deviceId, serviceId })
const characteristic = res.characteristics.find(c =>
c.uuid === '0000FFE1-0000-1000-8000-00805F9B34FB' && c.properties.notify
)
if (characteristic) {
this.notifyBLECharacteristicValueChange(deviceId, serviceId, characteristic.uuid)
}
}
notifyBLECharacteristicValueChange(deviceId, serviceId, characteristicId) {
uni.notifyBLECharacteristicValueChange({
deviceId,
serviceId,
characteristicId,
state: true,
success: () => {
console.log('启用notify成功')
this.listenBLECharacteristicValueChange()
}
})
}
listenBLECharacteristicValueChange() {
uni.onBLECharacteristicValueChange((res) => {
const rawData = this.arrayBufferToString(res.value)
const weight = this.parseWeightData(rawData)
this.currentWeight = weight
this.calculateTotal()
})
}
步骤5:解析重量数据
parseWeightData(rawData) {
// 示例:电子秤返回 "+00123kg\r\n"
// 需根据实际设备协议调整
const match = rawData.match(/[+-]?\d+\.\d+/)
if (match) {
return parseFloat(match[0])
}
return 0
}
arrayBufferToString(buffer) {
const decoder = new TextDecoder('ascii')
return decoder.decode(buffer)
}
4.2 价格管理实现
API接口设计:
// 获取今日价格
GET /api/prices/today
// 响应示例
{
"code": 0,
"data": {
"date": "2026-05-14",
"prices": [
{ "category": "paper", "name": "纸类", "unit": "kg", "price": 1.2 },
{ "category": "iron", "name": "铁", "unit": "kg", "price": 2.5 },
{ "category": "can", "name": "易拉罐", "unit": "piece", "price": 0.1 },
{ "category": "plastic", "name": "塑料", "unit": "kg", "price": 3.0 },
{ "category": "fabric", "name": "织物", "unit": "kg", "price": 0.8 }
]
}
}
本地缓存策略:
async getTodayPrices() {
// 1. 先读缓存
const cached = uni.getStorageSync('todayPrices')
const cacheTime = uni.getStorageSync('pricesCacheTime')
const now = Date.now()
// 缓存有效时间:1小时
if (cached && (now - cacheTime < 3600000)) {
return cached
}
// 2. 请求API
try {
const res = await uni.request({
url: 'https://api.example.com/prices/today',
method: 'GET'
})
if (res.statusCode === 200) {
uni.setStorageSync('todayPrices', res.data.data)
uni.setStorageSync('pricesCacheTime', now)
return res.data.data
}
} catch (err) {
console.error('API请求失败,使用缓存', err)
return cached || null
}
}
4.3 订单管理实现
创建订单:
createOrder(items) {
const order = {
orderId: this.generateOrderId(),
timestamp: Date.now(),
items: items.map(item => ({
category: item.category,
name: item.name,
weight: item.weight || item.count,
unit: item.unit,
unitPrice: item.unitPrice,
amount: item.weight ? item.weight * item.unitPrice : item.count * item.unitPrice
})),
totalAmount: items.reduce((sum, item) => {
const amount = item.weight ? item.weight * item.unitPrice : item.count * item.unitPrice
return sum + amount
}, 0),
status: 'completed'
}
// 保存到本地
this.saveOrderToLocal(order)
// 同步到云端(可选)
this.syncOrderToCloud(order)
return order
}
generateOrderId() {
const now = new Date()
const dateStr = `${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`
const timeStr = `${String(now.getHours()).padStart(2,'0')}${String(now.getMinutes()).padStart(2,'0')}`
const randomStr = String(Math.floor(Math.random() * 1000)).padStart(3,'0')
return `ORD${dateStr}${timeStr}${randomStr}`
}
五、数据库设计(后端)
5.1 价格表 (prices)
CREATE TABLE prices (
id INT PRIMARY KEY AUTO_INCREMENT,
category VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
unit ENUM('kg', 'piece') NOT NULL,
price DECIMAL(10,2) NOT NULL,
effective_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_date_category (effective_date, category)
);
5.2 订单表 (orders)
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(50) UNIQUE NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
status ENUM('pending', 'completed', 'cancelled') DEFAULT 'completed',
location_lat DECIMAL(10,8),
location_lng DECIMAL(11,8),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_order_id (order_id),
INDEX idx_created_at (created_at)
);
5.3 订单明细表 (order_items)
CREATE TABLE order_items (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(50) NOT NULL,
category VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
weight DECIMAL(10,2),
unit ENUM('kg', 'piece') NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(order_id),
INDEX idx_order_id (order_id)
);
六、项目目录结构
recycle-app/
├── pages/ # 页面
│ ├── index/ # 首页
│ ├── price/ # 价格页
│ ├── weigh/ # 称重页
│ ├── bluetooth/ # 蓝牙连接页
│ ├── order/ # 订单相关页
│ └── user/ # 用户中心
├── components/ # 组件
│ ├── BluetoothStatus.vue # 蓝牙状态组件
│ ├── PriceCard.vue # 价格卡片
│ ├── WeightDisplay.vue # 重量显示
│ └── OrderItem.vue # 订单条目
├── stores/ # Pinia状态管理
│ ├── bluetooth.js # 蓝牙状态
│ ├── price.js # 价格状态
│ └── order.js # 订单状态
├── utils/ # 工具函数
│ ├── bluetooth.js # 蓝牙工具
│ ├── price.js # 价格工具
│ ├── order.js # 订单工具
│ └── calculate.js # 计算工具
├── api/ # API接口
│ ├── price.js # 价格API
│ └── order.js # 订单API
├── static/ # 静态资源
│ ├── icons/ # 图标
│ └── images/ # 图片
├── App.vue
├── main.js
├── manifest.json # uni-app配置
├── pages.json # 页面路由
└── uni.scss # 全局样式
七、开发优先级
Phase 1(MVP)
- ✅ 首页(回收种类选择)
- ✅ 价格展示(写死本地数据)
- ✅ 称重页面(手动输入重量)
- ✅ 金额计算逻辑
- ✅ 订单创建与本地存储
Phase 2(核心功能)
- ✅ 蓝牙连接电子秤
- ✅ 实时重量获取
- ✅ 价格API对接
- ✅ 订单列表与详情
Phase 3(优化体验)
- 断线重连
- 历史数据统计
- 导出报表
- 多语言支持
八、注意事项
8.1 蓝牙兼容性
- iOS和Android的蓝牙API有差异,需充分测试
- 部分Android设备需要位置权限才能使用蓝牙
- 电子秤的蓝牙协议需提前确认(建议让硬件供应商提供文档)
8.2 重量精度
- 建议使用
Decimal类型处理金额计算,避免浮点数精度问题 - 重量显示保留2位小数
- 金额显示保留2位小数
8.3 离线支持
- 价格数据需缓存到本地
- 订单数据先存本地,网络恢复后同步
8.4 权限申请
- 蓝牙权限(Android)
- 位置权限(Android蓝牙扫描需要)
- 网络权限
九、测试用例
9.1 蓝牙连接测试
- 搜索设备
- 连接设备
- 接收数据
- 断线重连
- 断开连接
9.2 金额计算测试
- 纸类:12.5kg × 1.2元/kg = 15.0元
- 铁:5.0kg × 2.5元/kg = 12.5元
- 易拉罐:25个 × 0.1元/个 = 2.5元
- 塑料:3.2kg × 3.0元/kg = 9.6元
- 织物:8.0kg × 0.8元/kg = 6.4元
9.3 订单创建测试
- 单品类订单
- 多品类订单
- 订单保存到本地
- 订单同步到云端
十、后续扩展
- 用户系统:登录、权限管理
- 支付集成:微信支付、支付宝
- 数据可视化:每日/每月回收统计图表
- 打印功能:连接便携式打印机打印小票
- 多秤支持:同时连接多个电子秤
- AI识别:拍照识别回收物种类(可选)
文档版本: v1.0
创建时间: 2026-05-14
作者: QClaw AI Assistant