# 现场回收 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 1(MVP) 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