Initial commit - full huishou project

This commit is contained in:
jiapengyu
2026-07-27 14:07:26 +08:00
commit 60790ad1b3
39127 changed files with 5989265 additions and 0 deletions
+616
View File
@@ -0,0 +1,616 @@
# 现场回收 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