Files
huishou/现场回收uni-app设计文档_20260514.md
T
2026-07-27 14:07:26 +08:00

617 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 现场回收 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:回收种类选择
**回收种类(暂定)**
1. **纸类** - 按重量(kg)计价
2. **铁** - 按重量(kg)计价
3. **易拉罐** - 按个数计价
4. **塑料** - 按重量(kg)计价
5. **织物** - 按重量(kg)计价
**功能点**
- 展示回收种类列表(图标+名称)
- 显示当日回收价格
- 支持搜索/筛选(未来扩展)
- 选择后进入称重/计数界面
#### 模块2:当日回收价格管理
**数据来源**
- 云端API获取(管理员后台配置)
- 本地缓存(离线可用)
**价格展示**
- 列表页:每种回收物的当日价格
- 详情页:价格趋势(可选)
**数据格式示例**
```json
{
"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:蓝牙电子秤连接
**功能流程**
1. 搜索蓝牙设备
2. 连接指定电子秤(需知道设备名称/MAC地址)
3. 实时接收重量数据
4. 断线重连机制
5. 手动/自动归零
**蓝牙通信协议**(需根据实际电子秤调整):
- 服务UUID`0xFFE0`(示例)
- 特征UUID`0xFFE1`(示例)
- 数据格式:通常返回 ASCII 字符串,如 `"+00123kg\n"`
**关键代码逻辑**
```javascript
// 监听蓝牙数据
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) × 单价 |
**计算逻辑**
```javascript
function calculateAmount(category, weightOrCount, unitPrice) {
if (category === 'can') {
return weightOrCount * unitPrice // 按个数
} else {
return weightOrCount * unitPrice // 按重量
}
}
```
**实时计算**
- 重量变化时自动重新计算
- 显示预估金额(大字醒目)
- 支持手动修改重量/数量(特殊情况)
#### 模块5:订单管理
**订单数据模型**
```json
{
"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:初始化蓝牙**
```javascript
async initBluetooth() {
try {
await uni.openBluetoothAdapter()
console.log('蓝牙适配器初始化成功')
} catch (err) {
console.error('请打开蓝牙', err)
}
}
```
**步骤2:搜索设备**
```javascript
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:连接设备**
```javascript
async connectDevice(deviceId) {
try {
await uni.createBLEConnection({ deviceId })
this.deviceId = deviceId
this.getBLEDeviceServices(deviceId)
} catch (err) {
console.error('连接失败', err)
}
}
```
**步骤4:监听数据**
```javascript
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:解析重量数据**
```javascript
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接口设计**
```javascript
// 获取今日价格
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 }
]
}
}
```
**本地缓存策略**
```javascript
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 订单管理实现
**创建订单**
```javascript
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)
```sql
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)
```sql
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)
```sql
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 1MVP
1. ✅ 首页(回收种类选择)
2. ✅ 价格展示(写死本地数据)
3. ✅ 称重页面(手动输入重量)
4. ✅ 金额计算逻辑
5. ✅ 订单创建与本地存储
### Phase 2(核心功能)
1. ✅ 蓝牙连接电子秤
2. ✅ 实时重量获取
3. ✅ 价格API对接
4. ✅ 订单列表与详情
### Phase 3(优化体验)
1. 断线重连
2. 历史数据统计
3. 导出报表
4. 多语言支持
---
## 八、注意事项
### 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 订单创建测试
- [ ] 单品类订单
- [ ] 多品类订单
- [ ] 订单保存到本地
- [ ] 订单同步到云端
---
## 十、后续扩展
1. **用户系统**:登录、权限管理
2. **支付集成**:微信支付、支付宝
3. **数据可视化**:每日/每月回收统计图表
4. **打印功能**:连接便携式打印机打印小票
5. **多秤支持**:同时连接多个电子秤
6. **AI识别**:拍照识别回收物种类(可选)
---
**文档版本**: v1.0
**创建时间**: 2026-05-14
**作者**: QClaw AI Assistant