diff --git a/README.md b/README.md index e805f8c..1ce5b5a 100644 --- a/README.md +++ b/README.md @@ -18,48 +18,72 @@ - **智能指派功能**: 将订单指派给合适的货运人员 - **订单状态跟踪**: 实时更新订单配送状态 -### 用户系统功能 +### 👤 用户系统功能 - **多角色支持**: 管理员、货运人员、游客等不同角色 -- **用户登录/登出**: 支持微信登录 -- **签到功能**: 支持每日签到记录和状态管理 -- **注册功能**: 游客用户注册为正式用户 -- **个人信息管理**: 查看和编辑个人信息 +- **微信登录/登出**: 支持微信授权登录和登出 +- **签到/签退功能**: 支持每日签到和签退状态管理 +- **用户注册**: 游客用户注册为正式用户 +- **用户状态管理**: 支持signed_in/signed_out/registered/unregistered四种状态 - **权限控制**: 不同角色有不同的操作权限 -### ⚙️ 系统设置功能 -- **数据模式切换**: 支持模拟数据和真实API两种模式 -- **配置管理**: 可以配置API服务器地址等参数 -- **调试信息**: 显示系统运行状态和调试信息 +### 👥 员工管理功能 +- **员工列表查看**: 管理员可以查看所有员工信息(姓名、手机号、角色、ID) +- **员工搜索过滤**: 支持按姓名、手机号、角色搜索员工 +- **添加新员工**: 管理员可以添加新员工,包含姓名、手机号、角色信息 +- **删除员工**: 管理员可以删除不需要的员工 +- **员工信息验证**: 注册时验证员工信息是否存在 +- **角色管理**: 支持管理员和配送员两种角色分配 + +### 📍 位置追踪功能 +- **实时位置更新**: 通过WebSocket实现货运人员实时位置追踪 +- **位置订阅机制**: 支持订阅特定货运人员的位置更新 +- **在线状态管理**: 实时显示货运人员在线/离线状态 +- **位置历史记录**: 记录货运人员位置历史轨迹 + +### 🔧 系统管理功能 +- **API配置管理**: 可以配置API服务器地址等参数 +- **调试信息显示**: 显示系统运行状态和调试信息 +- **模块化架构**: 基于模块化设计,便于功能扩展和维护 ## 技术架构 -### 双服务架构设计 -系统采用创新的双服务架构,支持两种运行模式: +### 模块化架构设计 +系统采用现代化的模块化架构设计,专注于生产环境部署和功能扩展: -1. **模拟数据模式 (Mock Mode)** - - 使用本地模拟数据进行开发和测试 - - 不依赖后端服务,可以独立运行 - - 适合功能开发、单元测试、演示 +**模块化设计模式** +- **主页面模块 (MainPageModule)**: 协调各个子模块,处理页面生命周期和事件分发 +- **登录模块 (LoginModule)**: 处理用户登录、授权、用户信息管理 +- **地图模块 (MapModule)**: 处理地图显示、定位、标记点管理 +- **订单模块 (OrderModule)**: 处理订单列表、指派、状态跟踪 +- **仓库模块 (WarehouseModule)**: 处理仓库信息展示和管理 +- **员工模块 (EmployeeModule)**: 处理员工管理、位置跟踪、交互 +- **位置模块 (LocationModule)**: 处理位置追踪和实时更新 +- **数据模块 (DataModule)**: 统一管理页面数据状态 -2. **真实API模式 (API Mode)** - - 连接真实的后端RESTful API服务 - - 支持生产环境部署和联调测试 - - 数据实时更新,支持多用户协作 +**真实API模式 (API Mode)** +- 连接真实的后端RESTful API服务 +- 支持生产环境部署和联调测试 +- 数据实时更新,支持多用户协作 +- 通过WebSocket实现实时位置追踪 + +**注意**: 模拟数据模式已移除,系统现在仅支持真实API模式运行。 ### 核心服务组件 -- **apiService.ts**: 封装所有后端API调用的基础服务 -- **userService.ts**: 用户认证和信息管理服务 -- **mapService.ts**: 地图相关功能和服务 +- **apiService.ts**: 封装所有后端API调用和WebSocket连接管理的基础服务 +- **userService.ts**: 用户认证和信息管理服务(包含签到/签退功能) +- **mapService.ts**: 地图展示、路线规划、地点搜索等地图相关功能 - **orderService.ts**: 订单管理和指派服务 -- **deliveryPersonService.ts**: 配送人员管理和位置更新服务 +- **deliveryPersonService.ts**: 配送人员信息管理服务 - **warehouseService.ts**: 仓库和库存管理服务 +- **employeeService.ts**: 员工管理服务 +- **locationTrackingService.ts**: 位置追踪管理服务(负责实时位置更新、在线用户状态管理、WebSocket位置订阅) ## 后端接口需求 ### 基础配置 - **API基础地址**: `http://localhost:8080` -- **模拟模式**: 支持模拟数据和真实API两种模式切换 +- **WebSocket地址**: `ws://localhost:8080/ws/location` ### 用户认证模块 1. **微信登录** - `POST /user/wxlogin` @@ -70,13 +94,38 @@ - 请求: `{ userId: number }` - 响应: `{ success: boolean, userInfo: UserInfo, message?: string }` -3. **用户注册** - `POST /user/register` +3. **用户签退** - `POST /user/signout` + - 请求: `{ userId: number }` + - 响应: `{ success: boolean, message?: string }` + +4. **获取用户状态** - `GET /user/status` + - 响应: `{ status: 'signed_in' | 'signed_out' | 'registered' | 'unregistered'; lastSignInTime?: string; lastSignOutTime?: string }` + +5. **用户注册** - `POST /user/register` - 请求: `{ name: string, phone: string }` - 响应: `{ success: boolean, employeeInfo: EmployeeInfo, message?: string }` -4. **获取用户信息** - `GET /user/info` +6. **获取用户信息** - `GET /user/info` - 响应: `UserInfo` +7. **用户登出** - `POST /user/logout` + - 响应: `{ success: boolean, message?: string }` + +### 员工管理模块 +1. **获取员工列表** - `GET /employees` + - 响应: `EmployeeInfo[]` + +2. **添加员工** - `POST /employees` + - 请求: `{ name: string, phone: string, role: string }` + - 响应: `EmployeeInfo` + +3. **删除员工** - `DELETE /employees/{id}` + - 响应: `{ success: boolean, message?: string }` + +4. **更新员工信息** - `PUT /employees/{id}` + - 请求: `{ name?: string, phone?: string, role?: string }` + - 响应: `EmployeeInfo` + ### 仓库管理模块 1. **获取仓库列表** - `GET /warehouses` - 响应: `WarehouseInfo[]` @@ -119,13 +168,7 @@ - 请求: `{ status: Order['status'] }` - 响应: `{ success: boolean, message?: string }` -### 实时位置服务 -1. **订阅位置更新** - `POST /locations/subscribe` - - 请求: `{ deliveryPersonIds: number[] }` - - 响应: `{ subscriptionId: string, expiresAt: number }` -2. **取消订阅** - `POST /locations/unsubscribe` - - 请求: `{ subscriptionId: string }` ### 统计和日志模块 1. **系统统计** - `GET /stats/system` @@ -185,6 +228,19 @@ ### 系统从服务器接收的消息数据 +#### WebSocket实时位置消息 + +1. **位置更新消息** + ```json + { + "type": "updateLocation", // 消息类型 + "deliveryPersonId": number, // 货运人员ID + "latitude": number, // 纬度 + "longitude": number, // 经度 + "timestamp": number // 时间戳 + } + ``` + #### 用户相关消息 1. **微信登录响应** @@ -377,41 +433,167 @@ - **用户状态 (userStatus)**: 包括未登录、已登录、已签到等状态 - **角色权限**: 根据用户角色控制功能访问权限 -## 运货师傅上线与位置更新机制 +## WebSocket实时位置交互机制 -### 师傅上线流程 -1. **数据获取**: 通过 `deliveryPersonService.getDeliveryPersons()` 获取所有货运人员数据 -2. **列表更新**: 在 `deliveryPersonModule.loadDeliveryPersons()` 中调用服务获取数据 -3. **标记点生成**: 通过 `updateDeliveryPersonMarkers()` 方法将货运人员转换为地图标记点 -4. **地图更新**: 调用 `dataModule.updateMarkers()` 更新地图显示 +### WebSocket连接管理 -### 实时位置更新机制 -1. **订阅机制**: 通过 `deliveryPersonService.subscribeToRealTimeLocations()` 订阅实时位置更新 -2. **WebSocket连接**: 在真实环境中使用WebSocket接收实时位置数据 -3. **回调处理**: 位置更新通过 `handleRealTimeLocationUpdate()` 回调函数处理 -4. **单个标记更新**: 通过 `updateSingleDeliveryPersonMarker()` 方法更新特定货运人员的图形位置 +#### 连接建立流程 +1. **连接初始化**: 通过 `apiService.initLocationWebSocket()` 建立WebSocket连接 +2. **连接地址**: 使用配置的API地址,将HTTP协议替换为WebSocket协议(如 `ws://localhost:8080/ws/location`) +3. **连接状态管理**: 自动处理连接建立、消息接收、连接关闭和错误重连 -### 数据刷新机制 -- **实时更新**: 当收到位置消息时,直接更新对应货运人员的标记点位置(经度、纬度) -- **数据同步**: 位置更新会同步修改货运人员对象的 `currentLocation` 属性 -- **状态保持**: 货运人员的其他信息(姓名、状态、订单等)保持不变,只有位置信息更新 +#### 连接生命周期 +- **连接建立**: 调用 `wx.connectSocket()` 建立WebSocket连接 +- **连接保持**: 连接成功后保持活跃状态,接收实时位置更新 +- **自动重连**: 连接异常断开时,3秒后自动尝试重连(非正常关闭除外) +- **连接关闭**: 调用 `apiService.closeLocationWebSocket()` 主动关闭连接 -### 关键代码位置 -- `deliveryPersonModule.ts` - 处理货运人员显示和位置更新 -- `deliveryPersonService.ts` - 提供货运人员数据服务 -- `dataModule.ts` - 管理地图数据和标记点 +### 消息交互协议 + +#### 客户端发送消息类型 + +1. **发送位置更新** +```json +{ + "type": "updateLocation", + "userId": 123, + "latitude": 39.9042, + "longitude": 116.4074, + "timestamp": 1640995200000 +} +``` + +#### 服务端推送消息类型 + +1. **位置更新消息** +```json +{ + "type": "updateLocation", + "userId": 123, + "latitude": 39.9042, + "longitude": 116.4074, + "timestamp": 1640995200000 +} +``` + +2. **在线用户列表消息** +```json +{ + "type": "onlineUserList", + "users": [ + { + "userId": 123, + "name": "张三", + "latitude": 39.9042, + "longitude": 116.4074, + "lastUpdate": 1640995200000 + } + ] +} +``` + + + +### 位置更新处理流程 + +#### 订阅机制 +1. **用户签到**: 用户签到成功后,服务器自动处理位置订阅逻辑 +2. **WebSocket连接**: 建立WebSocket连接后,服务器会自动推送位置更新 +3. **回调绑定**: 通过 `apiService.onLocationUpdate(callback)` 注册位置更新回调函数 + +#### 实时位置处理 +1. **消息接收**: WebSocket接收到位置更新消息后,解析JSON数据 +2. **消息分发**: 调用所有注册的位置更新回调函数 +3. **地图更新**: 回调函数中更新对应货运人员的地图标记点位置 +4. **数据同步**: 同步更新货运人员对象的当前位置信息 + +### 回调机制设计 + +#### 多回调支持 +- **回调集合**: 使用 `Set` 数据结构管理多个位置更新回调函数 +- **动态注册**: 支持多个组件同时订阅位置更新 +- **自动清理**: 当所有回调都被移除时,自动关闭WebSocket连接 + +#### 取消订阅机制 +- **取消订阅**: 调用返回的取消订阅函数移除特定回调 +- **连接管理**: 当没有活跃回调时,自动关闭WebSocket连接以节省资源 + +### 错误处理与重连机制 + +#### 连接错误处理 +- **连接失败**: 捕获连接异常并记录错误日志 +- **环境检测**: 自动检测当前环境是否支持WebSocket +- **优雅降级**: 在不支持WebSocket的环境中使用模拟数据 + +#### 自动重连策略 +- **异常检测**: 监控WebSocket连接状态 +- **智能重连**: 仅在非正常关闭时触发重连(排除主动关闭) +- **重连间隔**: 3秒重连间隔,避免频繁重连 + +### 真实环境部署 + +#### 生产环境要求 +- **WebSocket支持**: 后端服务必须支持WebSocket协议 +- **实时通信**: 支持实时位置更新推送 +- **连接管理**: 支持连接保持和自动重连机制 + +#### 部署注意事项 +- **HTTPS要求**: 生产环境必须使用HTTPS和WSS协议 +- **认证授权**: WebSocket连接需要身份验证 +- **性能优化**: 合理控制位置更新频率 + +### 关键服务组件 + +#### ApiService (核心WebSocket管理) +- **连接管理**: WebSocket连接的建立、维护和关闭 +- **消息处理**: WebSocket消息的发送、接收和分发 +- **错误处理**: 连接异常和消息处理错误的统一处理 + +#### DeliveryPersonService (货运人员服务) +- **位置订阅**: 封装位置订阅和取消订阅接口 +- **实时更新**: 提供位置更新发送和接收功能 +- **服务集成**: 与地图服务和用户服务集成 + +#### LocationTrackingService (位置追踪服务) +- **状态管理**: 管理在线用户状态和位置信息 +- **位置分发**: 将位置更新分发给所有订阅者 +- **超时检测**: 检测用户离线状态和位置更新超时 + +### 性能优化策略 + +#### 连接复用 +- **单例模式**: 整个应用共享同一个WebSocket连接 +- **按需连接**: 仅在需要时建立连接,无订阅时自动关闭 +- **资源节约**: 避免多个组件创建重复的WebSocket连接 + +#### 消息优化 +- **消息压缩**: 使用简洁的消息格式减少网络传输 +- **批量更新**: 支持批量位置更新消息处理 +- **频率控制**: 控制位置更新频率,避免过度频繁的更新 + +### 安全考虑 + +#### 认证授权 +- **Token验证**: WebSocket连接使用Bearer Token进行身份验证 +- **权限控制**: 仅允许授权用户订阅和发送位置更新 +- **数据隔离**: 确保用户只能访问自己有权限的位置数据 + +#### 数据安全 +- **数据验证**: 对接收的WebSocket消息进行格式验证 +- **异常防护**: 防止恶意消息导致的系统异常 +- **日志记录**: 记录关键操作和异常情况 ## 开发与测试 -### 开发模式(模拟数据) -1. 在设置页面切换到"模拟数据"模式 -2. 系统使用本地模拟数据进行开发测试 -3. 所有功能都可以独立运行,不依赖后端 +### 开发环境要求 +1. 确保后端API服务已启动并运行在配置的API地址 +2. 系统会自动连接后端API进行数据交互 +3. 支持WebSocket实时位置更新功能 -### 联调模式(真实API) -1. 在设置页面切换到"真实API"模式 -2. 确保后端服务运行在配置的API地址 -3. 系统会自动连接后端API进行数据交互 +### 联调测试 +1. 验证所有API接口调用正常 +2. 测试WebSocket连接和实时位置更新 +3. 检查位置追踪服务的正常运行 ### 快速开始 @@ -429,9 +611,9 @@ - 导入项目目录 - 点击编译运行 -4. **切换数据模式**: - - 进入"设置"页面 - - 切换数据模式进行测试 +4. **配置后端服务**: + - 确保后端API服务运行在 `http://localhost:8080` + - 确保WebSocket服务运行在 `ws://localhost:8080/ws/location` ## 项目结构 @@ -452,8 +634,9 @@ miniprogram/ │ ├── admin/ # 管理员页面 │ └── index/ # 首页(地图+订单) ├── services/ # 服务层 -│ ├── apiService.ts # API服务基础封装 -│ ├── deliveryPersonService.ts # 配送人员服务 +│ ├── apiService.ts # API服务基础封装(包含WebSocket连接管理) +│ ├── deliveryPersonService.ts # 配送人员服务(包含位置订阅功能) +│ ├── locationTrackingService.ts # 位置追踪服务(实时位置管理) │ ├── mapService.ts # 地图服务 │ ├── orderService.ts # 订单服务 │ ├── userService.ts # 用户服务 @@ -470,21 +653,21 @@ miniprogram/ ### 生产环境部署 1. 确保后端API服务已部署并正常运行 -2. 修改API服务地址配置 -3. 切换到"真实API"模式 +2. 修改API服务地址配置为生产环境地址 +3. 确保WebSocket服务正常运行 4. 提交审核发布 ### 开发环境配置 1. 本地启动后端开发服务器 -2. 配置API地址 -3. 使用"真实API"模式进行联调测试 +2. 配置API地址为开发环境地址 +3. 确保WebSocket连接正常 ## 注意事项 -1. **数据一致性**: 确保模拟数据格式与后端API返回格式一致 -2. **错误处理**: 两种模式下的错误处理机制需要保持一致 -3. **性能优化**: 真实API模式下注意网络请求的优化 -4. **安全性**: 生产环境务必使用HTTPS和合适的认证机制 +1. **API一致性**: 确保前端API调用与后端接口格式一致 +2. **错误处理**: 实现完善的网络错误和API错误处理机制 +3. **性能优化**: 注意网络请求的优化和WebSocket连接管理 +4. **安全性**: 生产环境务必使用HTTPS和WSS协议,实现合适的认证机制 ## 联系方式 diff --git a/miniprogram/app.json b/miniprogram/app.json index b825add..f46db04 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -2,7 +2,8 @@ "pages": [ "pages/index/index", "pages/admin/admin", - "pages/apply/apply" + "pages/apply/apply", + "pages/employee/employee" ], "requiredPrivateInfos": [ "getLocation" diff --git a/miniprogram/images/crown.png b/miniprogram/images/crown.png new file mode 100644 index 0000000..f737be6 Binary files /dev/null and b/miniprogram/images/crown.png differ diff --git a/miniprogram/images/truck.png b/miniprogram/images/truck.png new file mode 100644 index 0000000..f737be6 Binary files /dev/null and b/miniprogram/images/truck.png differ diff --git a/miniprogram/pages/admin/admin.ts b/miniprogram/pages/admin/admin.ts index 3b00698..9f56d4d 100644 --- a/miniprogram/pages/admin/admin.ts +++ b/miniprogram/pages/admin/admin.ts @@ -42,7 +42,7 @@ Page({ userInfo: { id: 1, name: '管理员', - role: 'ADMIN', + role: 'ADMIN' as any, phone: '13800138000' } }); @@ -150,10 +150,11 @@ Page({ // 添加员工位置标记 deliveryPersons.forEach(person => { + // 使用默认坐标,实际位置由locationTrackingService统一管理 markers.push({ id: 20000 + person.id, // 避免ID冲突 - longitude: person.currentLocation.longitude, - latitude: person.currentLocation.latitude, + longitude: 102.833722, // 默认经度 + latitude: 24.880095, // 默认纬度 title: person.name, iconPath: this.getStaffStatusIcon(person.status), width: 32, @@ -259,7 +260,7 @@ Page({ if (assignedPerson) { this.setData({ - mapCenter: assignedPerson.currentLocation + mapCenter: { longitude: 102.833722, latitude: 24.880095 } // 默认坐标 }); // 绘制从起点到终点的路线 @@ -526,25 +527,19 @@ Page({ this.hideAddModal(); }, - // 切换底部页签 + // 底部页签切换 switchMainTab(e: any) { const tab = e.currentTarget.dataset.tab; this.setData({ currentTab: tab }); - - // 根据不同的页签执行不同的逻辑 - switch (tab) { - case 'main': - // 管理中心,显示所有内容 - break; - case 'orders': - // 订单详情,可能需要调整布局 - break; - case 'staff': - // 员工管理,可能需要调整布局 - break; - } + }, + + // 跳转到员工管理页面 + goToEmployeeManagement() { + wx.navigateTo({ + url: '/pages/employee/employee' + }); }, // 地图点击事件 diff --git a/miniprogram/pages/admin/admin.wxml b/miniprogram/pages/admin/admin.wxml index 6091e7b..ad5f70c 100644 --- a/miniprogram/pages/admin/admin.wxml +++ b/miniprogram/pages/admin/admin.wxml @@ -52,6 +52,7 @@ 员工管理 + diff --git a/miniprogram/pages/admin/admin.wxss b/miniprogram/pages/admin/admin.wxss index d47124a..2d1544a 100644 --- a/miniprogram/pages/admin/admin.wxss +++ b/miniprogram/pages/admin/admin.wxss @@ -112,6 +112,20 @@ min-width: 160rpx; } +.manage-btn { + background-color: #1890ff; + color: white; + font-size: 28rpx; + padding: 10rpx 20rpx; + border-radius: 40rpx; + border: none; + min-width: 160rpx; +} + +.manage-btn:active { + background-color: #096dd9; +} + /* 订单标签页 */ .order-tabs { display: flex; diff --git a/miniprogram/pages/apply/apply.ts b/miniprogram/pages/apply/apply.ts index 2af1d9c..1befc89 100644 --- a/miniprogram/pages/apply/apply.ts +++ b/miniprogram/pages/apply/apply.ts @@ -8,7 +8,6 @@ Page({ data: { applyForm: { name: '', - idCard: '', phone: '' } }, @@ -44,7 +43,7 @@ Page({ * 提交申请 */ async onSubmit() { - const { name, idCard, phone } = this.data.applyForm; + const { name, phone } = this.data.applyForm; // 表单验证 if (!this.validateForm()) { @@ -58,7 +57,7 @@ Page({ }); // 调用API提交申请 - const result = await this.submitApplication({ name, idCard, phone }); + const result = await this.submitApplication({ name, phone }); if (result.success) { wx.showToast({ @@ -115,9 +114,9 @@ Page({ * 表单验证 */ validateForm(): boolean { - const { name, idCard, phone } = this.data.applyForm; + const { name, phone } = this.data.applyForm; - if (!name || !idCard || !phone) { + if (!name || !phone) { wx.showToast({ title: '请填写完整信息', icon: 'none', @@ -126,16 +125,6 @@ Page({ return false; } - // 身份证号验证 - if (idCard.length !== 18) { - wx.showToast({ - title: '请输入正确的身份证号', - icon: 'none', - duration: 2000 - }); - return false; - } - // 手机号验证 if (!/^1[3-9]\d{9}$/.test(phone)) { wx.showToast({ diff --git a/miniprogram/pages/apply/apply.wxml b/miniprogram/pages/apply/apply.wxml index 717bbc2..4674231 100644 --- a/miniprogram/pages/apply/apply.wxml +++ b/miniprogram/pages/apply/apply.wxml @@ -25,21 +25,7 @@ - - - 身份证号 - - - + diff --git a/miniprogram/pages/employee/employee.json b/miniprogram/pages/employee/employee.json new file mode 100644 index 0000000..ff97c31 --- /dev/null +++ b/miniprogram/pages/employee/employee.json @@ -0,0 +1,7 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "员工管理", + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "backgroundColor": "#f5f5f5" +} \ No newline at end of file diff --git a/miniprogram/pages/employee/employee.ts b/miniprogram/pages/employee/employee.ts new file mode 100644 index 0000000..1449311 --- /dev/null +++ b/miniprogram/pages/employee/employee.ts @@ -0,0 +1,339 @@ +// 员工管理页面逻辑 +import { EmployeeInfo } from '../../types'; +import employeeService from '../../services/employeeService'; +import { Role, getRoleOptions } from '../../utils/roleUtils'; + +Page({ + data: { + // 员工列表 + employees: [] as EmployeeInfo[], + filteredEmployees: [] as EmployeeInfo[], + + // 页面状态 + currentTab: 'list', // list: 列表页, add: 添加页 + loading: false, + + // 添加员工表单数据 + addForm: { + name: '', + phone: '', + role: Role.DELIVERY_PERSON + }, + + // 错误信息 + errorMessage: '', + successMessage: '', + + // 搜索关键词 + searchKeyword: '', + + // 角色选项 + roleOptions: getRoleOptions() + }, + + onLoad() { + // 页面加载时获取员工列表 + this.loadEmployees(); + }, + + onShow() { + // 页面显示时刷新数据 + this.loadEmployees(); + }, + + /** + * 加载员工列表 + */ + async loadEmployees() { + this.setData({ + loading: true, + errorMessage: '', + successMessage: '' + }); + + try { + const employees = await employeeService.getEmployees(); + // 获取过滤后的员工列表 + const filteredEmployees = this.getFilteredEmployees(employees); + + this.setData({ + employees, + filteredEmployees, + loading: false + }); + } catch (error) { + console.error('加载员工列表失败:', error); + this.setData({ + loading: false, + errorMessage: '加载员工列表失败,请稍后重试' + }); + } + }, + + /** + * 切换页面标签 + */ + switchTab(e: any) { + const tab = e.currentTarget.dataset.tab; + this.setData({ + currentTab: tab, + errorMessage: '', + successMessage: '' + }); + + if (tab === 'list') { + this.loadEmployees(); + } + }, + + /** + * 处理添加员工表单输入 + */ + onFormInput(e: any) { + const { field } = e.currentTarget.dataset; + const value = e.detail.value; + + this.setData({ + [`addForm.${field}`]: value, + errorMessage: '', + successMessage: '' + }); + }, + + /** + * 处理角色选择 + */ + onRoleChange(e: any) { + const index = e.detail.value; + const selectedRole = this.data.roleOptions[index].value; + this.setData({ + 'addForm.role': selectedRole + }); + }, + + /** + * 验证表单数据 + */ + validateForm(): boolean { + const { name, phone, role } = this.data.addForm; + + if (!name.trim()) { + this.setData({ + errorMessage: '请输入员工姓名' + }); + return false; + } + + if (!phone.trim()) { + this.setData({ + errorMessage: '请输入手机号' + }); + return false; + } + + // 简单的手机号格式验证 + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(phone)) { + this.setData({ + errorMessage: '请输入正确的手机号格式' + }); + return false; + } + + if (!role) { + this.setData({ + errorMessage: '请选择员工角色' + }); + return false; + } + + return true; + }, + + /** + * 提交添加员工表单 + */ + async submitAddForm() { + if (!this.validateForm()) { + return; + } + + this.setData({ + loading: true, + errorMessage: '', + successMessage: '' + }); + + try { + await employeeService.addEmployee(this.data.addForm); + + this.setData({ + loading: false, + successMessage: '员工添加成功', + addForm: { + name: '', + phone: '', + role: Role.DELIVERY_PERSON + } + }); + + // 添加成功后自动切换到列表页 + setTimeout(() => { + this.setData({ + currentTab: 'list' + }); + this.loadEmployees(); + }, 1500); + } catch (error) { + console.error('添加员工失败:', error); + this.setData({ + loading: false, + errorMessage: (error as Error).message || '添加员工失败,请稍后重试' + }); + } + }, + + /** + * 删除员工 + */ + async deleteEmployee(e: any) { + const employeeId = e.currentTarget.dataset.id; + const employee = this.data.employees.find(emp => emp.id === employeeId); + + if (!employee) { + return; + } + + wx.showModal({ + title: '确认删除', + content: `确定要删除员工 ${employee.name} (${employee.phone}) 吗?此操作不可恢复。`, + confirmText: '删除', + confirmColor: '#ff4d4f', + cancelText: '取消', + success: async (res) => { + if (res.confirm) { + this.setData({ + loading: true, + errorMessage: '', + successMessage: '' + }); + + try { + const result = await employeeService.deleteEmployee(employeeId); + + if (result.success) { + this.setData({ + loading: false, + successMessage: result.message || '员工删除成功' + }); + + // 重新加载员工列表 + this.loadEmployees(); + } else { + this.setData({ + loading: false, + errorMessage: result.message || '删除员工失败' + }); + } + } catch (error) { + console.error('删除员工失败:', error); + this.setData({ + loading: false, + errorMessage: '删除员工失败,请稍后重试' + }); + } + } + } + }); + }, + + /** + * 搜索员工 + */ + onSearchInput(e: any) { + const keyword = e.detail.value; + this.setData({ + searchKeyword: keyword + }); + + // 更新过滤后的员工列表 + this.updateFilteredEmployees(); + }, + + /** + * 更新过滤后的员工列表 + */ + updateFilteredEmployees() { + const { employees } = this.data; + const filteredEmployees = this.getFilteredEmployees(employees); + this.setData({ + filteredEmployees + }); + }, + + /** + * 获取过滤后的员工列表 + */ + getFilteredEmployees(employees?: EmployeeInfo[]): EmployeeInfo[] { + const { searchKeyword } = this.data; + + // 如果employees为空,返回空数组 + if (!employees || !Array.isArray(employees)) { + return []; + } + + // 获取当前登录用户信息 + const app = getApp(); + const currentUser = app.globalData.userInfo; + + // 过滤掉当前登录用户 + let filteredEmployees = employees.filter(emp => { + // 如果没有当前用户信息,显示所有员工 + if (!currentUser || !currentUser.id) { + return true; + } + // 过滤掉当前用户 + return emp.id !== currentUser.id; + }); + + // 更严格的搜索关键词检查 + if (!searchKeyword || typeof searchKeyword !== 'string' || !searchKeyword.trim()) { + return filteredEmployees; + } + + const keyword = searchKeyword.toLowerCase(); + return filteredEmployees.filter(emp => + emp.name.toLowerCase().includes(keyword) || + emp.phone.includes(keyword) || + (emp.role || '').toLowerCase().includes(keyword) + ); + }, + + /** + * 获取角色显示文本 + */ + getRoleText(role: string): string { + const roleMap: Record = { + 'DELIVERY_PERSON': '配送员', + 'ADMIN': '管理员' + }; + return roleMap[role] || role; + }, + + /** + * 清空消息 + */ + clearMessages() { + this.setData({ + errorMessage: '', + successMessage: '' + }); + }, + + /** + * 返回上一页 + */ + goBack() { + wx.navigateBack(); + } +}); \ No newline at end of file diff --git a/miniprogram/pages/employee/employee.wxml b/miniprogram/pages/employee/employee.wxml new file mode 100644 index 0000000..192c928 --- /dev/null +++ b/miniprogram/pages/employee/employee.wxml @@ -0,0 +1,166 @@ + + + + + + + 员工管理 + + + 共{{filteredEmployees.length}}名员工 + + + + + + + 员工列表 + + + 添加员工 + + + + + + + {{errorMessage}} + × + + + {{successMessage}} + × + + + + + + + 加载中... + + + + + + + + + 🔎 + + + + + + + + + {{item.name.charAt(0)}} + + + + {{item.name}} + + {{item.role === 'ADMIN' ? '👑' : '🚚'}} + {{getRoleText(item.role)}} + + + {{item.phone}} + ID: {{item.id}} + + + + + + + + + + 👥 + + {{searchKeyword ? '没有找到匹配的员工' : '暂无员工数据'}} + + 点击右上角"添加员工"开始管理 + + + + + + + + + 员工姓名 + + + + + 手机号码 + + + + + 员工角色 + + + + {{addForm.role === 'ADMIN' ? '管理员' : addForm.role === 'DELIVERY_PERSON' ? '配送员' : '请选择角色'}} + + + + + + + + + 提示:添加员工后,用户可以使用该员工的姓名和手机号进行注册 + + + + + + + \ No newline at end of file diff --git a/miniprogram/pages/employee/employee.wxss b/miniprogram/pages/employee/employee.wxss new file mode 100644 index 0000000..908a8dc --- /dev/null +++ b/miniprogram/pages/employee/employee.wxss @@ -0,0 +1,485 @@ +/* 员工管理页面样式 */ +.employee-container { + width: 100%; + height: 100vh; + background-color: #f5f5f5; + display: flex; + flex-direction: column; +} + +/* 顶部导航栏 */ +.top-nav { + height: 90rpx; + background-color: #1aad19; + color: white; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 30rpx; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); +} + +.nav-left { + display: flex; + align-items: center; + gap: 20rpx; +} + +.back-btn { + font-size: 40rpx; + font-weight: bold; + cursor: pointer; +} + +.nav-title { + font-size: 36rpx; + font-weight: bold; +} + +.nav-right { + font-size: 28rpx; + opacity: 0.9; +} + +/* 页面标签 */ +.page-tabs { + height: 80rpx; + background-color: white; + display: flex; + border-bottom: 1rpx solid #e0e0e0; +} + +.tab-item { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 30rpx; + color: #666; + border-bottom: 4rpx solid transparent; + transition: all 0.3s; +} + +.tab-item.active { + color: #1aad19; + border-bottom-color: #1aad19; + font-weight: bold; +} + +.tab-text { + padding: 10rpx 0; +} + +/* 消息提示 */ +.message-container { + padding: 20rpx 30rpx; +} + +.error-message, .success-message { + padding: 20rpx 30rpx; + border-radius: 10rpx; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 28rpx; +} + +.error-message { + background-color: #fff2f0; + color: #ff4d4f; + border: 1rpx solid #ffccc7; +} + +.success-message { + background-color: #f6ffed; + color: #52c41a; + border: 1rpx solid #b7eb8f; +} + +.message-text { + flex: 1; +} + +.close-btn { + font-size: 36rpx; + cursor: pointer; + padding-left: 20rpx; +} + +/* 加载状态 */ +.loading-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid #f3f3f3; + border-top: 4rpx solid #1aad19; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20rpx; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 28rpx; + color: #666; +} + +/* 列表容器 */ +.list-container { + flex: 1; + display: flex; + flex-direction: column; + padding-bottom: 60rpx; /* 添加底部边距,避免内容被底部遮挡 */ +} + +/* 搜索框 */ +.search-container { + padding: 30rpx; + background: linear-gradient(135deg, #ffffff, #fafafa); + border-bottom: 1rpx solid rgba(0, 0, 0, 0.05); + box-shadow: 0 2rpx 15rpx rgba(0, 0, 0, 0.03); +} + +.search-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.search-input { + flex: 1; + height: 80rpx; + background: linear-gradient(135deg, #f8f9fa, #f1f3f4); + border-radius: 40rpx; + padding: 0 80rpx 0 35rpx; + font-size: 30rpx; + border: 2rpx solid transparent; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.search-input:focus { + border-color: #1aad19; + background: linear-gradient(135deg, #ffffff, #f8f9fa); + box-shadow: 0 6rpx 25rpx rgba(26, 173, 25, 0.15); +} + +.search-icon { + position: absolute; + right: 35rpx; + font-size: 36rpx; + color: #1aad19; + filter: drop-shadow(0 2rpx 4rpx rgba(26, 173, 25, 0.3)); +} + +/* 员工列表 */ +.employee-list { + flex: 1; + padding: 30rpx; + box-sizing: border-box; +} + +.employee-item { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border-radius: 20rpx; + padding: 35rpx; + margin: 25rpx 0; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.08); + border: 1rpx solid rgba(255, 255, 255, 0.8); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + box-sizing: border-box; + width: 100%; +} + +.employee-item::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4rpx; + background: linear-gradient(90deg, #1aad19, #52c41a); +} + +.employee-item:active { + transform: translateY(2rpx); + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.12); +} + +.employee-info { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + overflow: hidden; +} + +.employee-avatar { + width: 90rpx; + height: 90rpx; + border-radius: 50%; + background: linear-gradient(135deg, #1aad19, #52c41a); + display: flex; + align-items: center; + justify-content: center; + margin-right: 25rpx; + box-shadow: 0 4rpx 15rpx rgba(26, 173, 25, 0.3); + border: 3rpx solid rgba(255, 255, 255, 0.9); +} + +.avatar-text { + color: white; + font-size: 36rpx; + font-weight: bold; + text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2); +} + +.employee-details { + flex: 1; + min-width: 0; + overflow: hidden; +} + +.employee-name-row { + display: flex; + align-items: center; + margin-bottom: 12rpx; + gap: 15rpx; +} + +.employee-name { + font-size: 34rpx; + font-weight: 700; + color: #1a1a1a; + letter-spacing: 0.5rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 200rpx; +} + +.employee-role { + font-size: 24rpx; + padding: 6rpx 16rpx; + border-radius: 20rpx; + background: linear-gradient(135deg, #f0f2f5, #e8ecef); + color: #595959; + display: flex; + align-items: center; + gap: 8rpx; + border: 1rpx solid rgba(0, 0, 0, 0.05); +} + +.role-icon { + font-size: 28rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.employee-role.admin-role { + background: linear-gradient(135deg, #fff7e6, #ffe7ba); + color: #d46b08; + border: 1rpx solid rgba(250, 140, 22, 0.2); +} + +.employee-phone { + font-size: 28rpx; + color: #666; + margin-bottom: 8rpx; + display: flex; + align-items: center; + gap: 8rpx; +} + +.employee-phone::before { + content: '📱'; + font-size: 24rpx; +} + +.employee-id { + font-size: 24rpx; + color: #999; + display: flex; + align-items: center; + gap: 8rpx; +} + +.employee-id::before { + content: '🆔'; + font-size: 20rpx; +} + +.employee-actions { + margin-left: 25rpx; +} + +.delete-btn { + background: linear-gradient(135deg, #ff4d4f, #ff7875); + color: white; + border: none; + border-radius: 25rpx; + padding: 12rpx 24rpx; + font-size: 26rpx; + font-weight: 600; + box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.3); + transition: all 0.3s ease; + border: 1rpx solid rgba(255, 255, 255, 0.2); +} + +.delete-btn:active { + background: linear-gradient(135deg, #d9363e, #ff7875); + transform: translateY(1rpx); + box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.2); +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 120rpx 30rpx; + color: #999; + background: linear-gradient(135deg, #fafafa, #f5f5f5); + border-radius: 25rpx; + margin: 30rpx; + box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.05); + border: 1rpx solid rgba(255, 255, 255, 0.8); +} + +.empty-icon { + font-size: 100rpx; + display: block; + margin-bottom: 25rpx; + filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.1)); +} + +.empty-text { + font-size: 36rpx; + display: block; + margin-bottom: 15rpx; + font-weight: 600; + color: #666; +} + +.empty-hint { + font-size: 30rpx; + opacity: 0.8; + line-height: 1.6; +} + +/* 添加容器 */ +.add-container { + flex: 1; + padding: 30rpx; +} + +.form-container { + background-color: white; + border-radius: 15rpx; + padding: 40rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); +} + +.form-item { + margin-bottom: 40rpx; +} + +.form-label { + display: block; + font-size: 30rpx; + color: #333; + margin-bottom: 15rpx; + font-weight: bold; +} + +.form-input { + width: 100%; + height: 80rpx; + border: 1rpx solid #e0e0e0; + border-radius: 10rpx; + padding: 0 20rpx; + font-size: 28rpx; + box-sizing: border-box; +} + +.form-input:focus { + border-color: #1aad19; +} + +.form-picker { + width: 100%; + height: 80rpx; + border: 1rpx solid #e0e0e0; + border-radius: 10rpx; + padding: 0 20rpx; + font-size: 28rpx; + box-sizing: border-box; + display: flex; + align-items: center; +} + +.picker-display { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; +} + +.picker-text { + color: #333; +} + +.picker-arrow { + color: #999; + font-size: 24rpx; +} + +.form-hint { + background-color: #f6ffed; + border: 1rpx solid #b7eb8f; + border-radius: 10rpx; + padding: 20rpx; + margin-bottom: 40rpx; +} + +.hint-text { + font-size: 26rpx; + color: #52c41a; + line-height: 1.5; +} + +.submit-btn { + width: 100%; + height: 90rpx; + background-color: #1aad19; + color: white; + border: none; + border-radius: 45rpx; + font-size: 32rpx; + font-weight: bold; +} + +.submit-btn:active { + background-color: #179b16; +} + +.submit-btn:disabled { + background-color: #ccc; + color: #999; +} \ No newline at end of file diff --git a/miniprogram/pages/index/index.less b/miniprogram/pages/index/index.less index 9ce5da3..a74d451 100644 --- a/miniprogram/pages/index/index.less +++ b/miniprogram/pages/index/index.less @@ -149,25 +149,68 @@ page { /* 控制按钮样式统一 */ .control-btn.signin-btn, -.control-btn.register-btn { - width: 100rpx; - height: 100rpx; +.control-btn.register-btn, +.control-btn.sign-out-btn, +.control-btn.auth-btn, +.control-btn.location-btn, +.control-btn.reset-btn, +.control-btn.staff-management-btn { + width: 120rpx; + height: 120rpx; border-radius: 50%; display: flex; + flex-direction: column; align-items: center; justify-content: center; - box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); + box-shadow: 0 6rpx 30rpx rgba(0, 0, 0, 0.2); font-size: 24rpx; padding: 0; margin: 0; + border: none; + color: #333333; + text-align: center; + line-height: 1; + background: rgba(255, 255, 255, 0.9); + border: 1rpx solid rgba(0, 0, 0, 0.1); +} + +/* 按钮图标样式 */ +.btn-icon { + font-size: 40rpx; + margin-bottom: 8rpx; + display: block; + line-height: 1; +} + +/* 按钮文字样式 */ +.btn-text { + font-size: 22rpx; + font-weight: 500; + display: block; + line-height: 1.2; } .control-btn.signin-btn:active, -.control-btn.register-btn:active { +.control-btn.register-btn:active, +.control-btn.sign-out-btn:active { transform: scale(0.95); box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); } +/* 所有按钮点击状态 */ +.control-btn.signin-btn:active, +.control-btn.register-btn:active, +.control-btn.auth-btn:active, +.control-btn.sign-out-btn:active, +.control-btn.location-btn:active, +.control-btn.reset-btn:active, +.control-btn.staff-management-btn:active { + background: rgba(240, 240, 240, 0.9); + transform: scale(0.95); + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); + border: 1rpx solid rgba(0, 0, 0, 0.2); +} + .btn-signin { background-color: #1aad19; color: white; diff --git a/miniprogram/pages/index/index.ts b/miniprogram/pages/index/index.ts index 1079ce3..42c3103 100644 --- a/miniprogram/pages/index/index.ts +++ b/miniprogram/pages/index/index.ts @@ -1,61 +1,64 @@ // index.ts // 引入服务和工具函数 -import { UserInfo, Marker } from '../../types'; import { showToast } from '../../utils/helpers'; import userService from '../../services/userService'; import locationTrackingService from '../../services/locationTrackingService'; -// 引入模块 +// 引入主页面模块 import { MainPageModule } from './modules/mainPageModule'; -// 主页面组件 -Component({ +// 主页面组件接口定义 +interface IndexPageComponent { data: { - // 地图中心点坐标 - longitude: 102.833722, - latitude: 24.880095, - scale: 13, // 地图缩放级别 - markers: [] as Marker[], // 地图标记点数组 - userInfo: null as UserInfo | null, // 用户信息 - // 用户认证状态(分离外部登录状态和用户二级状态) - authStatus: { - hasWxCode: false, // 是否已获取微信code(外部登录状态) - userStatus: 'unknown' as 'unknown' | 'registered' | 'unregistered' | 'signed_in' | 'signed_out', // 用户二级状态 - }, - showUserPanel: false, // 是否显示用户信息面板 - showOrderPanel: false, // 是否显示订单详情面板 - currentOrder: null as any, // 当前选中的订单 - currentDeliveryPerson: null as any, // 当前选中的货运人员 - currentWarehouse: null as any, // 当前选中的仓库 - currentPanelPosition: { x: 0, y: 0 }, // 当前信息面板位置 - polyline: null as any, // 路线规划结果 - pendingOrders: [] as any[] ,// 待分配订单 - currentRoute: null as any, // 当前路线信息 - showRoute: false, // 是否显示路线 - routeDistance: 0, // 路线距离 - routeDuration: 0, // 路线预计时间 - - // 底部弹窗相关状态 - showWarehouseModal: false, // 仓库底部弹窗 - showDeliveryPersonModal: false, // 货运人员底部弹窗 - // 底部弹窗状态(bottom或full) - warehouseModalState: 'bottom', - deliveryPersonModalState: 'bottom', - // 操作按钮显示状态(基于用户状态动态计算) + mainPageModule: MainPageModule | null; + // 核心UI状态 + showUserPanel: boolean; + showOrderPanel: boolean; + showDeliveryPersonModal: boolean; + showWarehouseModal: boolean; + }; +} - showSignOutButton: false // 是否显示签退按钮 +// 主页面组件 +Component({ + data: { + mainPageModule: null as MainPageModule | null, + // 核心UI状态 + showUserPanel: false, + showOrderPanel: false, + showDeliveryPersonModal: false, + showWarehouseModal: false }, lifetimes: { - async attached() { - // 组件挂载时初始化 - await this.initPage(); + attached() { + console.log('index page attached'); + this.initPage(); + }, + + detached() { + console.log('index page detached'); + // 清理资源 + if (this.data.mainPageModule) { + this.data.mainPageModule.cleanup(); + } }, - detached() { - // 组件卸载时清理 - (this as any).mainPageModule = null; + show() { + console.log('index page show'); + // 页面显示时调用主页面模块的onShow方法 + if (this.data.mainPageModule) { + this.data.mainPageModule.onShow(); + } + }, + + hide() { + console.log('index page hide'); + // 页面隐藏时调用主页面模块的onHide方法 + if (this.data.mainPageModule) { + this.data.mainPageModule.onHide(); + } } }, @@ -82,11 +85,14 @@ Component({ const app = getApp(); // 初始化主页面模块 - (this as any).mainPageModule = new MainPageModule(this); - const loginModule = (this as any).mainPageModule.getLoginModule(); + this.setData({ + mainPageModule: new MainPageModule(this) + }); // 设置globalData中的loginModule引用,用于废弃方法的重定向 - app.globalData.loginModule = loginModule; + if (this.data.mainPageModule) { + app.globalData.loginModule = this.data.mainPageModule.getLoginModule(); + } // 异步检查登录状态 await this.checkAndUpdateLoginStatus(); @@ -95,9 +101,14 @@ Component({ // 异步检查并更新登录状态 async checkAndUpdateLoginStatus() { const app = getApp(); - const loginModule = (this as any).mainPageModule.getLoginModule(); try { + // 获取登录模块 + if (!this.data.mainPageModule) { + console.error('mainPageModule未初始化'); + return; + } + const loginModule = this.data.mainPageModule.getLoginModule(); // 显示加载状态 wx.showLoading({ title: '检查登录状态...', @@ -108,21 +119,8 @@ Component({ const isLoggedIn = await this.asyncCheckLoginStatus(loginModule); if (isLoggedIn) { - // 已登录状态 - const userStatus = await loginModule.determineUserStatus(app.globalData.userInfo); - this.setData({ - userInfo: app.globalData.userInfo, - 'authStatus.hasWxCode': true, - 'authStatus.userStatus': userStatus, - // 初始化按钮显示状态 - showSignInButton: loginModule.shouldShowSignInButton(), - showRegisterButton: loginModule.shouldShowRegisterButton(), - showAuthButton: false - }); - - // === 全局登录流程完成,登录成功 === - // 统一在此处执行一次完整的页面刷新 - this.refreshPageAfterLogin(); + // 已登录状态 - 按钮状态会在refreshPageAfterLogin中统一更新 + console.log('✅ 登录检查成功,等待统一页面刷新'); } else { // 未登录状态 this.setData({ @@ -173,6 +171,9 @@ Component({ console.log('✅ 使用本地登录状态,用户状态已更新:', userStatus); + // 登录成功后统一刷新页面 + this.refreshPageAfterLogin(); + return true; } @@ -213,19 +214,24 @@ Component({ // 更新按钮显示状态 updateButtonDisplayStatus() { - const loginModule = (this as any).mainPageModule.getLoginModule(); + if (!this.data.mainPageModule) { + console.error('mainPageModule未初始化'); + return; + } + + const loginModule = this.data.mainPageModule.getLoginModule(); const showSignInButton = loginModule.shouldShowSignInButton(); const showRegisterButton = loginModule.shouldShowRegisterButton(); // 签退按钮显示逻辑:已签到用户显示签退按钮 - const showSignOutButton = this.data.authStatus.userStatus === 'signed_in'; + const showSignOutButton = this.data.authStatus && this.data.authStatus.userStatus === 'signed_in'; console.log('🔄 更新按钮显示状态:'); console.log(' - showSignInButton:', showSignInButton); console.log(' - showRegisterButton:', showRegisterButton); console.log(' - showSignOutButton:', showSignOutButton); - console.log(' - 当前用户状态:', this.data.authStatus.userStatus); - console.log(' - 当前hasWxCode:', this.data.authStatus.hasWxCode); + console.log(' - 当前用户状态:', this.data.authStatus ? this.data.authStatus.userStatus : 'undefined'); + console.log(' - 当前hasWxCode:', this.data.authStatus ? this.data.authStatus.hasWxCode : 'undefined'); this.setData({ showSignInButton, @@ -238,7 +244,12 @@ Component({ // 登录成功后统一刷新页面 async refreshPageAfterLogin() { - const loginModule = (this as any).mainPageModule.getLoginModule(); + if (!this.data.mainPageModule) { + console.error('mainPageModule未初始化'); + return; + } + + const loginModule = this.data.mainPageModule.getLoginModule(); const app = getApp(); console.log('🔄 === 全局登录流程完成,执行统一页面刷新 ==='); @@ -250,13 +261,14 @@ Component({ this.updateButtonDisplayStatus(); // 3. 初始化主页面模块 - await (this as any).mainPageModule.onLoad(); + await this.data.mainPageModule.onLoad(); - // 4. 登录成功后主动加载业务数据(仓库、订单等) + // 4. 登录成功后只加载公开数据(仓库等),不加载业务数据 + // 业务数据(员工位置等)只有在用户签到后才应该加载 if (app.globalData.isLoggedIn) { - console.log('🔍 登录成功,开始加载业务数据...'); - await (this as any).mainPageModule.refreshAllData(); - console.log('✅ 业务数据加载完成'); + console.log('🔍 登录成功,开始加载公开数据...'); + await this.data.mainPageModule.loadPublicData(); + console.log('✅ 公开数据加载完成'); } console.log('✅ 统一页面刷新完成'); @@ -278,7 +290,12 @@ Component({ // 处理签到 - 已迁移到LoginModule async handleSignIn() { - const loginModule = (this as any).mainPageModule.getLoginModule(); + if (!this.data.mainPageModule) { + console.error('mainPageModule未初始化'); + return; + } + + const loginModule = this.data.mainPageModule.getLoginModule(); const success = await loginModule.handleSignIn(); if (success) { @@ -286,18 +303,22 @@ Component({ this.updateButtonDisplayStatus(); // 刷新页面数据 - await (this as any).mainPageModule.refreshAllData(); + await this.data.mainPageModule.refreshAllData(); } }, // 处理授权登录 - 已迁移到LoginModule async handleAuthLogin() { - const loginModule = (this as any).mainPageModule.getLoginModule(); + if (!this.data.mainPageModule) { + console.error('mainPageModule未初始化'); + return; + } + + const loginModule = this.data.mainPageModule.getLoginModule(); const success = await loginModule.handleAuthLogin(); if (success) { const app = getApp(); - const loginModule = (this as any).mainPageModule.getLoginModule(); const userStatus = loginModule.determineUserStatus(app.globalData.userInfo); this.setData({ userInfo: app.globalData.userInfo, @@ -323,7 +344,11 @@ Component({ }); // 调用loginModule的logout方法 - const loginModule = (this as any).mainPageModule.getLoginModule(); + if (!this.data.mainPageModule) { + console.error('mainPageModule未初始化'); + return; + } + const loginModule = this.data.mainPageModule.getLoginModule(); await loginModule.logout(); // 更新页面状态 @@ -364,10 +389,18 @@ Component({ console.warn('调试信息 - 停止位置追踪失败:', trackingError); } - // 清除登录信息,防止自动重新登录 - console.log('调试信息 - 开始清除登录信息'); - const app = getApp(); - app.doGlobalLogout(); + // 停止位置模块的实时跟踪 + try { + if (this.data.mainPageModule) { + const locationModule = this.data.mainPageModule.getLocationModule(); + if (locationModule) { + await locationModule.stopRealTimeTracking(); + console.log('调试信息 - 位置模块实时跟踪已停止'); + } + } + } catch (trackingError) { + console.warn('调试信息 - 停止位置模块实时跟踪失败:', trackingError); + } // 保存签退状态到本地存储,防止自动重新登录 console.log('调试信息 - 保存签退状态到本地存储'); @@ -377,13 +410,25 @@ Component({ const savedStatus = wx.getStorageSync('userStatus'); console.log('调试信息 - 验证保存的签退状态:', savedStatus); - // 更新用户状态为已签退 + // 更新用户状态为已签退(不调用doGlobalLogout,保持登录状态) this.setData({ 'authStatus.userStatus': 'signed_out', showSignOutButton: false, showSignInButton: true }); + // 清除所有员工图标 + if (this.data.mainPageModule) { + const employeeModule = this.data.mainPageModule.getEmployeeModule(); + if (employeeModule) { + // 清除员工标记点 + const { markers } = this.data; + const filteredMarkers = markers.filter((marker: any) => marker.type !== 'employee'); + this.setData({ markers: filteredMarkers }); + console.log('调试信息 - 已清除所有员工图标'); + } + } + wx.hideLoading(); showToast('签退成功'); console.log('调试信息 - 用户签退完成'); @@ -401,9 +446,25 @@ Component({ }); }, + // 跳转到管理员页面 + goToAdminPage() { + wx.navigateTo({ + url: '/pages/admin/admin' + }); + }, + + // 跳转到员工管理页面 + goToEmployeeManagement() { + wx.navigateTo({ + url: '/pages/employee/employee' + }); + }, + // 阻止事件冒泡 stopPropagation(e: any) { - e.stopPropagation(); + if (e && typeof e.stopPropagation === 'function') { + e.stopPropagation(); + } }, @@ -422,8 +483,8 @@ Component({ // 用户登出 userLogout() { - if ((this as any).mainPageModule) { - const loginModule = (this as any).mainPageModule.getLoginModule(); + if (this.data.mainPageModule) { + const loginModule = this.data.mainPageModule.getLoginModule(); loginModule.logout(); // 更新页面状态 @@ -449,91 +510,91 @@ Component({ // 隐藏所有面板 hideAllPanels() { - if ((this as any).mainPageModule) { - (this as any).mainPageModule.hideAllPanels(); + if (this.data.mainPageModule) { + this.data.mainPageModule.hideAllPanels(); } }, // 重置标记点状态 resetMarkers() { - if ((this as any).mainPageModule) { - (this as any).mainPageModule.resetMarkers(); + if (this.data.mainPageModule) { + this.data.mainPageModule.resetMarkers(); } }, // 地图点击事件 onMapTap(e: any) { - if ((this as any).mainPageModule) { - (this as any).mainPageModule.onMapTap(e); + if (this.data.mainPageModule) { + this.data.mainPageModule.onMapTap(e); } }, // 标记点点击事件 onMarkerTap(e: any) { - if ((this as any).mainPageModule) { - (this as any).mainPageModule.onMarkerTap(e); + if (this.data.mainPageModule) { + this.data.mainPageModule.onMarkerTap(e); } }, // 分配订单 async assignOrder(orderId: number, deliveryPersonId: number) { - if ((this as any).mainPageModule) { - const orderModule = (this as any).mainPageModule.getOrderModule(); + if (this.data.mainPageModule) { + const orderModule = this.data.mainPageModule.getOrderModule(); await orderModule.assignOrder(orderId, deliveryPersonId); } }, // 更新订单状态 async updateOrderStatus(orderId: number, status: 'pending' | 'assigned' | 'in_transit' | 'delivered') { - if ((this as any).mainPageModule) { - const orderModule = (this as any).mainPageModule.getOrderModule(); + if (this.data.mainPageModule) { + const orderModule = this.data.mainPageModule.getOrderModule(); await orderModule.updateOrderStatus(orderId, status); } }, // 展开仓库面板 expandWarehousePanel() { - if ((this as any).mainPageModule) { - const warehouseModule = (this as any).mainPageModule.getWarehouseModule(); + if (this.data.mainPageModule) { + const warehouseModule = this.data.mainPageModule.getWarehouseModule(); warehouseModule.expandWarehousePanel(); } }, // 收起仓库面板 collapseWarehousePanel() { - if ((this as any).mainPageModule) { - const warehouseModule = (this as any).mainPageModule.getWarehouseModule(); + if (this.data.mainPageModule) { + const warehouseModule = this.data.mainPageModule.getWarehouseModule(); warehouseModule.collapseWarehousePanel(); } }, - // 展开货运人员面板 + // 展开员工面板 expandDeliveryPersonPanel() { - if ((this as any).mainPageModule) { - const deliveryPersonModule = (this as any).mainPageModule.getDeliveryPersonModule(); - deliveryPersonModule.expandDeliveryPersonPanel(); + if (this.data.mainPageModule) { + const employeeModule = this.data.mainPageModule.getEmployeeModule(); + employeeModule.expandDeliveryPersonPanel(); } }, - // 收起货运人员面板 + // 收起员工面板 collapseDeliveryPersonPanel() { - if ((this as any).mainPageModule) { - const deliveryPersonModule = (this as any).mainPageModule.getDeliveryPersonModule(); - deliveryPersonModule.collapseDeliveryPersonPanel(); + if (this.data.mainPageModule) { + const employeeModule = this.data.mainPageModule.getEmployeeModule(); + employeeModule.collapseDeliveryPersonPanel(); } }, // 刷新所有数据 async refreshAllData() { - if ((this as any).mainPageModule) { - await (this as any).mainPageModule.refreshAllData(); + if (this.data.mainPageModule) { + await this.data.mainPageModule.refreshAllData(); } }, // 开始定位(处理地图控制按钮点击) async startLocation() { - if ((this as any).mainPageModule) { - const mapModule = (this as any).mainPageModule.getMapModule(); + if (this.data.mainPageModule) { + const mapModule = this.data.mainPageModule.getMapModule(); await mapModule.startLocation(); } }, diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index 044e8c1..2e112da 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -26,7 +26,8 @@ type="primary" size="mini" > - 签到 + + 签到 @@ -37,7 +38,8 @@ type="default" size="mini" > - 注册 + 📝 + 注册 @@ -48,7 +50,8 @@ type="warn" size="mini" > - 登录 + 🔑 + 登录 @@ -59,14 +62,16 @@ type="warn" size="mini" > - 签退 + 🚪 + 签退 - + 📍 + 定位 @@ -74,11 +79,17 @@ - - 管理 + 👤 + 我的 + + + + + + 👥 + 员工 @@ -92,7 +103,7 @@ @@ -103,6 +114,7 @@ ID: {{userInfo.id || '未获取'}} 电话:{{userInfo.phone || '未设置'}} 角色:{{userInfo.role === 'ADMIN' ? '管理员' : '货运员'}} + diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss index f0d9841..5c77205 100644 --- a/miniprogram/pages/index/index.wxss +++ b/miniprogram/pages/index/index.wxss @@ -88,6 +88,17 @@ gap: 20rpx; } +/* 右侧控制按钮容器 */ +.right-controls-container { + position: absolute; + top: 20rpx; + right: 30rpx; + z-index: 10; + display: flex; + flex-direction: column; + gap: 20rpx; +} + .control-btn { width: 80rpx; height: 80rpx; @@ -1407,4 +1418,26 @@ 100% { opacity: 0.7; } +} + +/* 员工管理按钮样式 */ +.staff-management-btn { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background-color: white; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); +} + +.staff-management-btn:active { + background-color: #f5f5f5; + transform: scale(0.98); +} + +.staff-management-btn image { + width: 40rpx; + height: 40rpx; } \ No newline at end of file diff --git a/miniprogram/pages/index/modules/dataModule.ts b/miniprogram/pages/index/modules/dataModule.ts index efe862f..13222b3 100644 --- a/miniprogram/pages/index/modules/dataModule.ts +++ b/miniprogram/pages/index/modules/dataModule.ts @@ -14,11 +14,32 @@ export class DataModule { /** * 初始化页面数据 */ - public initializeData(): void { + public async initializeData(): Promise { + // 检查是否已静默登录,如果是则尝试获取真实位置 + let initialLongitude = 102.833722; + let initialLatitude = 24.880095; + + const app = getApp(); + if (app.globalData.isLoggedIn) { + try { + // 导入地图服务 + const mapService = require('../../../services/mapService').default; + const location = await mapService.getCurrentLocation(); + + if (location && !isNaN(location.latitude) && !isNaN(location.longitude)) { + initialLongitude = location.longitude; + initialLatitude = location.latitude; + console.log('[DATA MODULE] 静默登录后使用真实位置:', location); + } + } catch (error) { + console.warn('[DATA MODULE] 获取真实位置失败,使用默认位置:', error); + } + } + this.pageContext.setData({ // 地图相关数据 - longitude: 102.833722, - latitude: 24.880095, + longitude: initialLongitude, + latitude: initialLatitude, scale: 13, markers: [] as Marker[], polyline: null, @@ -148,7 +169,7 @@ export class DataModule { */ public updateMarkers(markers: Marker[]): void { // 验证每个标记点的坐标 - const validatedMarkers = markers.map((marker, index) => { + const validatedMarkers = markers.map((marker) => { // 检查经纬度是否为有效数字 if (isNaN(marker.longitude) || isNaN(marker.latitude)) { // 为无效坐标设置默认值 @@ -283,4 +304,26 @@ export class DataModule { public resetAllData(): void { this.initializeData(); } + + /** + * 获取位置模块(用于其他模块访问位置模块) + */ + public getLocationModule(): any { + // 通过页面上下文获取位置模块 + if (this.pageContext.data.mainPageModule) { + return this.pageContext.data.mainPageModule.getLocationModule(); + } + return null; + } + + /** + * 获取地图模块(用于其他模块访问地图模块) + */ + public getMapModule(): any { + // 通过页面上下文获取地图模块 + if (this.pageContext.data.mainPageModule) { + return this.pageContext.data.mainPageModule.getMapModule(); + } + return null; + } } \ No newline at end of file diff --git a/miniprogram/pages/index/modules/deliveryPersonModule.ts b/miniprogram/pages/index/modules/deliveryPersonModule.ts index c4180b9..0a03e54 100644 --- a/miniprogram/pages/index/modules/deliveryPersonModule.ts +++ b/miniprogram/pages/index/modules/deliveryPersonModule.ts @@ -1,10 +1,12 @@ -// 货运人员模块 - 处理货运人员管理、位置跟踪、交互 -import deliveryPersonService from '../../../services/deliveryPersonService'; +// 员工模块 - 处理所有员工(管理员和货运人员)管理、位置跟踪、交互 +import employeeService from '../../../services/employeeService'; +import { Role } from '../../../utils/roleUtils'; +// getApp是微信小程序全局函数,无需导入 import { showToast } from '../../../utils/helpers'; import { DataModule } from './dataModule'; -export class DeliveryPersonModule { +export class EmployeeModule { private pageContext: any; private dataModule: DataModule; @@ -14,210 +16,44 @@ export class DeliveryPersonModule { } /** - * 加载货运人员数据 + * 加载所有员工数据(包括管理员和货运人员) */ - async loadDeliveryPersons(): Promise { + async loadAllEmployees(): Promise { try { - const deliveryPersons = await deliveryPersonService.getDeliveryPersons(); + // 获取所有员工数据 + const allEmployees = await employeeService.getEmployees(); - // 更新地图标记点 - this.updateDeliveryPersonMarkers(deliveryPersons); - - console.log('货运人员数据加载完成,数量:', deliveryPersons.length); + console.log('所有员工数据加载完成,数量:', allEmployees.length); } catch (error) { - console.error('加载货运人员数据失败:', error); - showToast('加载货运人员数据失败'); - } - } - - /** - * 更新货运人员位置 - */ - async updateDeliveryPersonLocation(personId: number, location: { longitude: number, latitude: number }): Promise { - try { - await deliveryPersonService.updateDeliveryPersonLocation(personId, location); - - console.log(`货运人员 ${personId} 位置更新:`, location); - - // 重新加载货运人员数据 - await this.loadDeliveryPersons(); - - } catch (error) { - console.error('更新货运人员位置失败:', error); - showToast('更新位置失败'); - } - } - - - - /** - * 开始实时位置跟踪 - */ - async startRealTimeTracking(): Promise { - try { - // 获取当前用户信息 - const userInfo = this.dataModule.getData().userInfo; - if (!userInfo || !userInfo.id) { - throw new Error('用户信息获取失败'); - } - - // 使用新的WebSocket接口订阅位置更新 - await deliveryPersonService.subscribeToRealTimeLocations(userInfo.id); - - // 设置位置更新回调 - deliveryPersonService.subscribeToRealTimeLocations(this.handleRealTimeLocationUpdate.bind(this)); - - showToast('开始实时跟踪'); - console.log('开始实时跟踪货运人员位置'); - } catch (error) { - console.error('开始实时跟踪失败:', error); - showToast('开始实时跟踪失败'); + console.error('加载员工数据失败:', error); + showToast('加载员工数据失败'); } } - handleRealTimeLocationUpdate(location: any): void { - console.log('收到实时位置更新:', location); - // 这里可以添加更新地图标记点的逻辑 - // 根据位置更新信息更新对应的货运人员标记点 - if (location && location.deliveryPersonId) { - this.updateSingleDeliveryPersonMarker(location); - } - } - /** - * 更新单个货运人员标记点 + * 获取员工角色对应的图标 */ - private updateSingleDeliveryPersonMarker(location: any): void { - const { markers } = this.pageContext.data; + private getEmployeeIcon(role: string): string { + console.log(`获取员工图标,角色: ${role}`); - // 查找并更新对应的货运人员标记点 - const updatedMarkers = markers.map((marker: any) => { - if (marker.type === 'delivery_person' && marker.data && marker.data.id === location.deliveryPersonId) { - // 更新标记点位置 - return { - ...marker, - longitude: location.longitude, - latitude: location.latitude, - data: { - ...marker.data, - currentLocation: { - longitude: location.longitude, - latitude: location.latitude - } - } - }; - } - return marker; - }); - - // 更新数据模块中的标记点 - this.dataModule.updateMarkers(updatedMarkers); - } - - /** - * 停止实时跟踪货运人员位置 - */ - async stopRealTimeTracking(): Promise { - try { - // 获取当前用户信息 - const userInfo = this.dataModule.getData().userInfo; - if (!userInfo || !userInfo.id) { - throw new Error('用户信息获取失败'); - } - - // 使用新的WebSocket接口取消订阅 - await deliveryPersonService.unsubscribeFromRealTimeLocations(); - - showToast('已停止实时跟踪'); - console.log('停止实时跟踪货运人员位置'); - } catch (error) { - console.error('停止实时跟踪失败:', error); - showToast('停止实时跟踪失败'); - } - } - - /** - * 更新货运人员标记点 - */ - private updateDeliveryPersonMarkers(deliveryPersons: any[]): void { - console.log(`[DELIVERY PERSON MODULE] 开始更新货运人员标记点,货运人员总数: ${deliveryPersons.length}`); - - const { markers } = this.pageContext.data; - - // 移除现有的货运人员标记点 - const filteredMarkers = markers.filter((marker: any) => marker.type !== 'delivery_person'); - console.log(`[DELIVERY PERSON MODULE] 移除现有货运人员标记点后,剩余标记点数量: ${filteredMarkers.length}`); - - // 添加新的货运人员标记点 - const deliveryPersonMarkers = deliveryPersons.map((person, index) => { - // 验证货运人员坐标是否有效 - // 注意:坐标信息嵌套在currentLocation对象中 - let validLongitude = person.currentLocation?.longitude; - let validLatitude = person.currentLocation?.latitude; - - if (isNaN(validLongitude) || isNaN(validLatitude)) { - console.error(`[DELIVERY PERSON MODULE] 货运人员${index} (ID: ${person.id}) 坐标无效: (${validLongitude}, ${validLatitude}),使用默认坐标`); - validLongitude = 102.833722; // 默认经度 - validLatitude = 24.880095; // 默认纬度 - } else { - console.log(`[DELIVERY PERSON MODULE] 货运人员${index} (ID: ${person.id}) 坐标有效: (${validLongitude}, ${validLatitude})`); - } - - const iconPath = this.getDeliveryPersonIcon(person.status); - - return { - id: 2000 + index, // 货运人员标记点ID从2000开始 - longitude: validLongitude, - latitude: validLatitude, - iconPath: iconPath, - width: 26, - height: 26, - zIndex: 20, - type: 'delivery_person', - data: person - }; - }); - - console.log(`[DELIVERY PERSON MODULE] 生成新货运人员标记点数量: ${deliveryPersonMarkers.length}`); - - // 更新数据模块中的标记点 - const allMarkers = [...filteredMarkers, ...deliveryPersonMarkers]; - console.log(`[DELIVERY PERSON MODULE] 准备更新所有标记点,总数: ${allMarkers.length}`); - this.dataModule.updateMarkers(allMarkers); - } - - /** - * 获取货运人员状态对应的图标 - */ - private getDeliveryPersonIcon(status: string): string { - console.log(`获取货运人员图标,状态: ${status}`); - - // 根据报错信息,直接使用已知存在的备用图片路径 - // 实际项目中,应确保相关图片资源正确放置在指定路径 - const fallbackIconPath = '/images/trucks.png'; - - // 根据不同状态使用不同的图片路径 + // 根据角色使用不同的图标 let iconPath = ''; - switch (status) { - case 'idle': - // 为避免图片加载失败,暂时使用备用图片 - iconPath = fallbackIconPath; - console.log('使用备用图标(idle状态)'); + switch (role) { + case Role.ADMIN: + // 管理员使用皇冠图标 + iconPath = '/images/crown.png'; + console.log('使用管理员图标(👑)'); break; - case 'busy': - // 为避免图片加载失败,暂时使用备用图片 - iconPath = fallbackIconPath; - console.log('使用备用图标(busy状态)'); - break; - case 'offline': - // 为避免图片加载失败,暂时使用备用图片 - iconPath = fallbackIconPath; - console.log('使用备用图标(offline状态)'); + case Role.DELIVERY_PERSON: + // 货运人员使用货车图标 + iconPath = '/images/truck.png'; + console.log('使用货运人员图标(🚚)'); break; default: - iconPath = fallbackIconPath; - console.log('使用备用图标(默认状态)'); + // 默认使用货车图标 + iconPath = '/images/truck.png'; + console.log('使用默认图标(🚚)'); break; } diff --git a/miniprogram/pages/index/modules/locationModule.ts b/miniprogram/pages/index/modules/locationModule.ts new file mode 100644 index 0000000..6f072b8 --- /dev/null +++ b/miniprogram/pages/index/modules/locationModule.ts @@ -0,0 +1,278 @@ +// 位置模块 - 处理位置追踪、位置更新和地图标记点更新 +import { DataModule } from './dataModule'; +import { showToast } from '../../../utils/helpers'; +import locationTrackingService from '../../../services/locationTrackingService'; +import { OnlineUserInfo } from '../../../services/locationTrackingService'; + +// 位置模块接口定义 +export interface LocationModule { + initialize(): Promise; + cleanup(): void; + startRealTimeTracking(): Promise; + stopRealTimeTracking(): Promise; + updateEmployeeLocation(employeeId: number, location: { longitude: number, latitude: number }): Promise; +} + +export class LocationModule { + private pageContext: any; + private dataModule: DataModule; + private isTracking: boolean; + + constructor(pageContext: any, dataModule: DataModule) { + this.pageContext = pageContext; + this.dataModule = dataModule; + this.isTracking = false; + + // 绑定回调方法 + this.handleLocationUpdates = this.handleLocationUpdates.bind(this); + } + + /** + * 初始化位置模块 + */ + async initialize(): Promise { + console.log('位置模块初始化'); + + // 订阅位置更新 + this.subscribeToLocationUpdates(); + + console.log('位置模块初始化完成'); + } + + /** + * 清理位置模块 + */ + cleanup(): void { + console.log('清理位置模块'); + + // 取消位置更新订阅 + this.unsubscribeFromLocationUpdates(); + + // 停止实时跟踪 + if (this.isTracking) { + this.stopRealTimeTracking().catch(error => { + console.error('停止实时跟踪失败:', error); + }); + } + } + + /** + * 开始实时跟踪位置 + */ + async startRealTimeTracking(): Promise { + try { + // 获取当前用户信息 + const userInfo = this.dataModule.getData().userInfo; + if (!userInfo || !userInfo.id) { + throw new Error('用户信息获取失败'); + } + + // 启动位置跟踪订阅 + await locationTrackingService.startTrackingAfterSignIn(); + + this.isTracking = true; + showToast('已开始实时跟踪'); + console.log('开始实时跟踪位置'); + } catch (error) { + console.error('开始实时跟踪失败:', error); + showToast('开始实时跟踪失败'); + throw error; + } + } + + /** + * 停止实时跟踪位置 + */ + async stopRealTimeTracking(): Promise { + try { + // 获取当前用户信息 + const userInfo = this.dataModule.getData().userInfo; + if (!userInfo || !userInfo.id) { + throw new Error('用户信息获取失败'); + } + + // 停止位置跟踪 + await locationTrackingService.stopTracking(); + + this.isTracking = false; + showToast('已停止实时跟踪'); + console.log('停止实时跟踪位置'); + } catch (error) { + console.error('停止实时跟踪失败:', error); + showToast('停止实时跟踪失败'); + throw error; + } + } + + /** + * 更新员工位置 + */ + async updateEmployeeLocation(employeeId: number, location: { longitude: number, latitude: number }): Promise { + try { + console.log(`员工 ${employeeId} 位置更新:`, location); + + // 通过locationTrackingService更新位置 + await locationTrackingService.updateUserLocation(location); + + console.log(`员工 ${employeeId} 位置更新完成`); + } catch (error) { + console.error('更新员工位置失败:', error); + showToast('更新位置失败'); + throw error; + } + } + + /** + * 订阅位置更新 + */ + private subscribeToLocationUpdates(): void { + console.log('订阅位置更新'); + locationTrackingService.subscribeToLocationUpdates(this.handleLocationUpdates); + } + + /** + * 取消订阅位置更新 + */ + private unsubscribeFromLocationUpdates(): void { + console.log('取消订阅位置更新'); + locationTrackingService.unsubscribeFromLocationUpdates(this.handleLocationUpdates); + } + + /** + * 处理位置更新回调 + */ + private handleLocationUpdates(locationData: any): void { + console.log('收到位置更新回调:', locationData); + + if (locationData.type === 'onlineUserList' && locationData.users) { + // 处理在线用户列表更新 + console.log('处理在线用户列表,用户数量:', locationData.users.length); + this.updateEmployeeMarkers(locationData.users); + } else if (Array.isArray(locationData)) { + // 处理位置更新数组(旧格式) + console.log('处理位置更新数组,用户数量:', locationData.length); + this.updateEmployeeMarkers(locationData); + } else if (locationData.userId && locationData.latitude && locationData.longitude) { + // 处理单个用户位置更新 + console.log('处理单个用户位置更新:', locationData.userId); + this.updateSingleEmployeeMarker(locationData); + } else { + console.warn('未知的位置数据格式:', locationData); + } + } + + /** + * 更新员工标记点 + */ + private updateEmployeeMarkers(onlineUsers: any[]): void { + console.log('开始更新员工标记点,在线用户数量:', onlineUsers.length); + + // 获取地图模块来更新标记点 + const mapModule = this.dataModule.getMapModule(); + if (!mapModule) { + console.error('地图模块未找到,无法更新员工标记点'); + return; + } + + // 为每个在线用户创建标记点 + const employeeMarkers = onlineUsers.map(user => { + // 获取用户角色信息 + const userRole = user.role || this.getUserRole(user.userId); + + // 解析位置信息(支持多种格式) + const longitude = user.longitude || (user.lastLocation && user.lastLocation.longitude) || 0; + const latitude = user.latitude || (user.lastLocation && user.lastLocation.latitude) || 0; + const lastUpdateTime = user.lastUpdateTime || Date.now(); + + const employeeMarker = { + id: 10000 + user.userId, // 避免ID冲突 + type: 'employee', + title: user.userName || `员工${user.userId}`, + longitude: longitude, + latitude: latitude, + iconPath: this.getEmployeeIcon(user.userId, userRole), + width: 32, + height: 32, + zIndex: 30, + data: { + userId: user.userId, + role: userRole, + lastUpdateTime: lastUpdateTime + } + }; + + return employeeMarker; + }); + + // 调用地图模块更新员工标记点 + mapModule.updateEmployeeMarkers(employeeMarkers); + + console.log(`成功更新了 ${employeeMarkers.length} 个员工标记点`); + } + + /** + * 更新单个员工标记点 + */ + private updateSingleEmployeeMarker(locationUpdate: any): void { + console.log('更新单个员工标记点:', locationUpdate.userId); + + // 获取地图模块 + const mapModule = this.dataModule.getMapModule(); + if (!mapModule) { + console.error('地图模块未找到,无法更新员工标记点'); + return; + } + + // 获取用户角色信息 + const userRole = this.getUserRole(locationUpdate.userId); + + // 创建单个员工标记点 + const employeeMarker = { + id: 10000 + locationUpdate.userId, + type: 'employee', + title: `员工${locationUpdate.userId}`, + longitude: locationUpdate.longitude, + latitude: locationUpdate.latitude, + iconPath: this.getEmployeeIcon(locationUpdate.userId, userRole), + width: 32, + height: 32, + zIndex: 30, + data: { + userId: locationUpdate.userId, + role: userRole, + lastUpdateTime: locationUpdate.timestamp || Date.now() + } + }; + + // 调用地图模块更新单个标记点 + mapModule.updateSingleEmployeeMarker(employeeMarker); + + console.log('单个员工标记点更新完成'); + } + + /** + * 获取用户角色 + */ + private getUserRole(userId: number): string { + // 从数据模块获取用户信息 + const userInfo = this.dataModule.getData().userInfo; + if (userInfo && userInfo.id === userId) { + return userInfo.role || 'employee'; + } + + // 如果是其他用户,默认为货运人员 + return 'employee'; + } + + /** + * 获取员工图标 + */ + private getEmployeeIcon(userId: number, userRole: string): string { + // 根据用户角色返回不同的图标 + if (userRole === 'ADMIN') { + return '/images/crown.png'; // 管理员图标 + } else { + return '/images/trucks.png'; // 货运人员图标 + } + } +} \ No newline at end of file diff --git a/miniprogram/pages/index/modules/loginModule.ts b/miniprogram/pages/index/modules/loginModule.ts index 9090709..b69fce0 100644 --- a/miniprogram/pages/index/modules/loginModule.ts +++ b/miniprogram/pages/index/modules/loginModule.ts @@ -234,9 +234,17 @@ export class LoginModule { // 启动位置追踪服务 try { + // 先启动位置追踪服务 await locationTrackingService.startTrackingAfterSignIn(); console.log('位置追踪服务已启动'); + // 然后调用位置模块的实时跟踪功能 + const locationModule = this.dataModule.getLocationModule(); + if (locationModule) { + await locationModule.startRealTimeTracking(); + console.log('位置模块实时跟踪已启动'); + } + // 订阅位置更新回调,采用统一方式更新所有用户位置 locationTrackingService.subscribeToLocationUpdates((onlineUsers) => { console.log('🚚 位置更新回调 - 在线用户列表已更新,用户数量:', onlineUsers.length); @@ -433,10 +441,8 @@ export class LoginModule { // 根据服务器返回的状态映射到前端状态 switch (response.status) { case 'signed_in': - case 'online': return 'signed_in'; case 'signed_out': - case 'offline': return 'signed_out'; case 'registered': return 'registered'; diff --git a/miniprogram/pages/index/modules/mainPageModule.ts b/miniprogram/pages/index/modules/mainPageModule.ts index 95a5440..26b65ca 100644 --- a/miniprogram/pages/index/modules/mainPageModule.ts +++ b/miniprogram/pages/index/modules/mainPageModule.ts @@ -3,17 +3,32 @@ import { LoginModule } from './loginModule'; import { MapModule } from './mapModule'; import { OrderModule } from './orderModule'; import { WarehouseModule } from './warehouseModule'; -import { DeliveryPersonModule } from './deliveryPersonModule'; +import { EmployeeModule } from './deliveryPersonModule'; +import { LocationModule } from './locationModule'; import { DataModule } from './dataModule'; import { showToast } from '../../../utils/helpers'; +// 主页面模块接口定义 +export interface MainPageModule { + getLoginModule(): LoginModule; + onLoad(): Promise; + onShow(): void; + onHide(): void; + onLogout(): void; + hideAllPanels(): void; + resetMarkers(): void; + cleanup(): void; + refreshAllData(): Promise; +} + export class MainPageModule { private pageContext: any; private loginModule: LoginModule; private mapModule: MapModule; private orderModule: OrderModule; private warehouseModule: WarehouseModule; - private deliveryPersonModule: DeliveryPersonModule; + private employeeModule: EmployeeModule; + private locationModule: LocationModule; private dataModule: DataModule; constructor(pageContext: any) { @@ -25,7 +40,8 @@ export class MainPageModule { this.mapModule = new MapModule(pageContext, this.dataModule); this.orderModule = new OrderModule(pageContext, this.dataModule); this.warehouseModule = new WarehouseModule(pageContext, this.dataModule); - this.deliveryPersonModule = new DeliveryPersonModule(pageContext, this.dataModule); + this.employeeModule = new EmployeeModule(pageContext, this.dataModule); + this.locationModule = new LocationModule(pageContext, this.dataModule); } /** @@ -38,6 +54,9 @@ export class MainPageModule { // 初始化应用 await this.initApp(); + // 初始化位置模块 + await this.locationModule.initialize(); + // 加载地图数据 await this.loadAllData(); @@ -58,12 +77,22 @@ export class MainPageModule { this.refreshDataIfNeeded(); } + /** + * 页面隐藏 + */ + onHide(): void { + console.log('主页面隐藏'); + + // 清理位置模块 + this.locationModule.cleanup(); + } + /** * 初始化应用 */ private async initApp(): Promise { // 初始化数据 - this.dataModule.initializeData(); + await this.dataModule.initializeData(); console.log('应用初始化'); } @@ -119,7 +148,7 @@ export class MainPageModule { // 并行加载各种业务数据(需要登录) await Promise.all([ this.orderModule.loadPendingOrders(), - this.deliveryPersonModule.loadDeliveryPersons() + this.employeeModule.loadAllEmployees() ]); console.log('所有业务数据加载完成'); @@ -160,7 +189,7 @@ export class MainPageModule { this.dataModule.updatePendingOrders([]); const filteredMarkers = this.pageContext.data.markers.filter((marker: any) => - marker.type !== 'warehouse' && marker.type !== 'delivery_person' + marker.type !== 'warehouse' && marker.type !== 'employee' ); this.dataModule.updateMarkers(filteredMarkers); } @@ -230,7 +259,7 @@ export class MainPageModule { this.warehouseModule.onWarehouseMarkerClick(marker.data, e); break; case 'delivery_person': - this.deliveryPersonModule.onDeliveryPersonMarkerClick(marker.data, e); + this.employeeModule.onDeliveryPersonMarkerClick(marker.data, e); break; default: this.mapModule.onMarkerTap(e); @@ -267,10 +296,17 @@ export class MainPageModule { } /** - * 获取货运人员模块 + * 获取员工模块 */ - getDeliveryPersonModule(): DeliveryPersonModule { - return this.deliveryPersonModule; + getEmployeeModule(): EmployeeModule { + return this.employeeModule; + } + + /** + * 获取位置模块 + */ + getLocationModule(): LocationModule { + return this.locationModule; } /** diff --git a/miniprogram/pages/index/modules/mapModule.ts b/miniprogram/pages/index/modules/mapModule.ts index 8fd8f67..492e868 100644 --- a/miniprogram/pages/index/modules/mapModule.ts +++ b/miniprogram/pages/index/modules/mapModule.ts @@ -83,11 +83,16 @@ export class MapModule { return; } - // 更新数据模块中的位置信息和缩放级别 + // 强制重置地图视角到用户位置 + // 1. 更新用户位置 this.dataModule.updateUserLocation(location.latitude, location.longitude); - this.dataModule.setMapScale(15); // 定位成功后放大到更详细的级别 + // 2. 重置地图缩放级别到15(详细级别) + this.dataModule.setMapScale(15); + // 3. 添加地图动画效果,让视角平滑回到用户位置 + this.animateMapToUserLocation(location.latitude, location.longitude); - console.log('[MAP MODULE] 定位成功,坐标已更新:', location); + console.log('[MAP MODULE] 定位成功,坐标已更新,视角已重置:', location); + showToast('已定位到您的位置'); } catch (error) { console.error('[MAP MODULE] 定位失败:', error); this.setDefaultLocation(); @@ -105,9 +110,44 @@ export class MapModule { // 更新数据模块中的位置信息和缩放级别 this.dataModule.updateUserLocation(defaultLatitude, defaultLongitude); this.dataModule.setMapScale(13); + // 动画效果回到默认位置 + this.animateMapToUserLocation(defaultLatitude, defaultLongitude); console.log('[MAP MODULE] 默认位置设置完成'); } + + /** + * 动画效果将地图视角平滑移动到用户位置 + */ + private animateMapToUserLocation(latitude: number, longitude: number): void { + // 使用微信小程序的MapContext.moveToLocation方法实现地图移动 + const mapContext = wx.createMapContext('myMap', this.pageContext); + + // 先设置地图中心点 + this.pageContext.setData({ + latitude: latitude, + longitude: longitude + }); + + // 使用moveToLocation方法平滑移动地图视角 + mapContext.moveToLocation({ + latitude: latitude, + longitude: longitude, + success: () => { + console.log('[MAP MODULE] 地图视角移动成功,位置:', { latitude, longitude }); + }, + fail: (err: any) => { + console.error('[MAP MODULE] 地图视角移动失败:', err); + // 如果moveToLocation失败,直接设置中心点 + this.pageContext.setData({ + latitude: latitude, + longitude: longitude + }); + } + }); + + console.log('[MAP MODULE] 地图视角动画已启动,移动到位置:', { latitude, longitude }); + } /** * 加载地图数据(仓库和货运人员) @@ -126,39 +166,30 @@ export class MapModule { /** * 生成初始标记点 */ - private generateInitialMarkers(): Marker[] { - // 获取当前坐标 - const longitude = this.pageContext.data.longitude; - const latitude = this.pageContext.data.latitude; + public generateInitialMarkers(): Marker[] { + console.log('生成初始标记点'); - // 检查坐标是否为有效数字 - let validLongitude = longitude; - let validLatitude = latitude; + const markers: Marker[] = []; - if (isNaN(longitude) || isNaN(latitude)) { - console.warn(`无效的坐标值: longitude=${longitude}, latitude=${latitude}, 使用默认坐标`); - validLongitude = 102.833722; // 默认经度 - validLatitude = 24.880095; // 默认纬度 - - // 更新页面数据中的坐标 - this.dataModule.updateUserLocation(validLatitude, validLongitude); - } - - - - // 生成初始标记点 - return [ - { - id: -1, // 用户位置标记 + // 获取用户位置 + const userLocation = this.pageContext.data.userLocation; + if (userLocation && userLocation.longitude && userLocation.latitude) { + // 添加用户位置标记点 + markers.push({ + id: -1, title: '用户位置', - longitude: validLongitude, - latitude: validLatitude, + longitude: userLocation.longitude, + latitude: userLocation.latitude, iconPath: '/images/trucks.png', width: 40, height: 40, zIndex: 99 - } - ]; + }); + + console.log('已添加用户位置标记点'); + } + + return markers; } /** @@ -191,6 +222,57 @@ export class MapModule { console.log('用户头像已更新'); } + /** + * 更新员工标记点 + */ + updateEmployeeMarkers(employeeMarkers: Marker[]): void { + console.log('开始更新员工标记点,数量:', employeeMarkers.length); + + const { markers } = this.pageContext.data; + + // 过滤掉现有的员工标记点 + const filteredMarkers = markers.filter((marker: any) => + marker.type !== 'employee' + ); + + // 合并标记点 + const updatedMarkers = [...filteredMarkers, ...employeeMarkers]; + + // 更新数据模块中的标记点 + this.dataModule.updateMarkers(updatedMarkers); + + console.log('员工标记点更新完成,总标记点数量:', updatedMarkers.length); + } + + /** + * 更新单个员工标记点 + */ + updateSingleEmployeeMarker(employeeMarker: Marker): void { + console.log('更新单个员工标记点:', employeeMarker.id); + + const { markers } = this.pageContext.data; + + // 查找现有的员工标记点 + const markerIndex = markers.findIndex((marker: any) => + marker.type === 'employee' && marker.id === employeeMarker.id + ); + + let updatedMarkers; + if (markerIndex !== -1) { + // 更新现有的标记点 + updatedMarkers = [...markers]; + updatedMarkers[markerIndex] = employeeMarker; + } else { + // 添加新的标记点 + updatedMarkers = [...markers, employeeMarker]; + } + + // 更新数据模块中的标记点 + this.dataModule.updateMarkers(updatedMarkers); + + console.log('单个员工标记点更新完成'); + } + /** * 处理地图点击事件 */ diff --git a/miniprogram/services/apiService.ts b/miniprogram/services/apiService.ts index 5f210f4..ce8e59c 100644 --- a/miniprogram/services/apiService.ts +++ b/miniprogram/services/apiService.ts @@ -16,7 +16,7 @@ console.log(`当前API地址: ${API_BASE_URL} (${IS_LOCAL_DEV ? '本地开发环 * 是否启用模拟模式 * 该配置控制是否使用模拟数据进行开发测试 */ -export const isMockMode = false; + /** * API服务类 @@ -28,6 +28,11 @@ class ApiService { */ private locationUpdateCallbacks: Set<(location: any) => void> | null; + /** + * 在线用户列表回调函数集合 + */ + private onlineUserListCallbacks: Set<(userList: any) => void> | null; + /** * 位置更新WebSocket连接 */ @@ -38,6 +43,7 @@ class ApiService { */ constructor() { this.locationUpdateCallbacks = null; + this.onlineUserListCallbacks = null; this.locationWebSocket = null; } @@ -194,6 +200,20 @@ async ServerLogin(code: string) { return await this.request('/user/logout', { method: 'POST' }); } + /** + * 签退接口 + * @param userId 用户ID + * @returns 签退结果 + */ + async userSignOut(userId: number): Promise<{ success: boolean; message?: string }> { + console.log('API userSignOut调用,参数userId:', userId); + + return await this.request('/user/signout', { + method: 'POST', + data: { userId }, + }); + } + /** * 获取用户签到状态 * @returns 用户状态信息 @@ -311,6 +331,52 @@ async ServerLogin(code: string) { }); } + // ====== 员工管理接口 ====== + + /** + * 获取所有员工列表 + * @returns 员工信息数组 + */ + async getEmployees(): Promise { + return await this.request('/employees'); + } + + /** + * 添加新员工 + * @param employeeInfo 员工信息 + * @returns 添加结果 + */ + async addEmployee(employeeInfo: { name: string; phone: string; role: string }): Promise { + return await this.request('/employees', { + method: 'POST', + data: employeeInfo, + }); + } + + /** + * 删除员工 + * @param employeeId 员工ID + * @returns 删除结果 + */ + async deleteEmployee(employeeId: number): Promise<{ success: boolean; message?: string }> { + return await this.request<{ success: boolean; message?: string }>(`/employees/${employeeId}`, { + method: 'DELETE', + }); + } + + /** + * 更新员工信息 + * @param employeeId 员工ID + * @param employeeInfo 员工信息 + * @returns 更新结果 + */ + async updateEmployee(employeeId: number, employeeInfo: { name?: string; phone?: string; role?: string }): Promise { + return await this.request(`/employees/${employeeId}`, { + method: 'PUT', + data: employeeInfo, + }); + } + // ==================== 仓库相关接口 ==================== /** @@ -464,15 +530,6 @@ async ServerLogin(code: string) { subscriptionId: string; expiresAt: number; }> { - if (isMockMode) { - // 模拟数据 - 返回模拟的订阅信息 - console.log('[MOCK] 订阅实时位置更新,货运人员ID列表:', deliveryPersonIds); - return { - subscriptionId: `mock-subscription-${Date.now()}`, - expiresAt: Date.now() + 3600000 // 1小时后过期 - }; - } - // 真实环境中调用API try { return await this.request<{ @@ -493,12 +550,6 @@ async ServerLogin(code: string) { * @param subscriptionId 订阅ID */ async unsubscribeFromRealTimeLocations(subscriptionId: string): Promise { - if (isMockMode) { - // 模拟数据 - 记录取消订阅操作 - console.log('[MOCK] 取消订阅实时位置更新,订阅ID:', subscriptionId); - return; - } - // 真实环境中调用API try { await this.request('/locations/unsubscribe', { @@ -529,12 +580,31 @@ async ServerLogin(code: string) { this.locationUpdateCallbacks.delete(callback); // 如果没有回调了,可以关闭WebSocket连接 if (this.locationUpdateCallbacks.size === 0 && this.locationWebSocket) { - this.locationWebSocket.close(); - this.locationWebSocket = null; + this.closeLocationWebSocket(); } } }; } + + /** + * 注册在线用户列表回调函数 + * @param callback 在线用户列表回调函数 + * @returns 取消订阅函数 + */ + onOnlineUserList(callback: (userList: any) => void): () => void { + // 添加回调到集合 + if (!this.onlineUserListCallbacks) { + this.onlineUserListCallbacks = new Set(); + } + this.onlineUserListCallbacks.add(callback); + + // 返回取消订阅函数 + return () => { + if (this.onlineUserListCallbacks) { + this.onlineUserListCallbacks.delete(callback); + } + }; + } // ===== 私有属性和方法 ===== @@ -578,9 +648,21 @@ async ServerLogin(code: string) { }); // WebSocket连接关闭事件 - wx.onSocketClose(() => { - console.log('WebSocket连接已关闭'); + wx.onSocketClose((res) => { + console.log('WebSocket连接已关闭', res); this.locationWebSocket = null; + + // 如果是非主动关闭,尝试重连 + if (res.code !== 1000) { // 1000表示正常关闭 + console.log('WebSocket连接异常关闭,将在3秒后尝试重连'); + setTimeout(() => { + if (!this.locationWebSocket) { + this.initLocationWebSocket().catch(err => { + console.error('WebSocket重连失败:', err); + }); + } + }, 3000); + } }); // WebSocket错误事件 @@ -597,14 +679,28 @@ async ServerLogin(code: string) { } } + /** + * 检查WebSocket连接是否已建立 + */ + public isWebSocketConnected(): boolean { + return this.locationWebSocket !== null && this.locationWebSocket !== undefined; + } + /** * 关闭位置更新WebSocket连接 */ public async closeLocationWebSocket(): Promise { if (this.locationWebSocket && typeof wx !== 'undefined' && typeof wx.closeSocket === 'function') { - wx.closeSocket(); - this.locationWebSocket = null; - console.log('WebSocket连接已关闭'); + try { + // 先检查WebSocket状态,避免在连接过程中关闭 + wx.closeSocket(); + this.locationWebSocket = null; + console.log('WebSocket连接已关闭'); + } catch (error) { + console.warn('关闭WebSocket连接时出现异常:', error); + // 即使关闭失败,也重置连接状态 + this.locationWebSocket = null; + } } } @@ -621,13 +717,53 @@ async ServerLogin(code: string) { switch (message.type) { case 'updateLocation': // 处理位置更新消息 - this.onLocationUpdate(message); + this.triggerLocationUpdateCallbacks(message); + break; + case 'onlineUserList': + // 处理在线用户列表消息 + this.triggerOnlineUserListCallbacks(message); + break; + case 'subscribed': + // 处理订阅成功响应 + console.log('WebSocket订阅成功:', message); break; default: console.warn('收到未知类型的WebSocket消息:', message); break; } } + + /** + * 触发位置更新回调 + * @param locationUpdate 位置更新信息 + */ + private triggerLocationUpdateCallbacks(locationUpdate: any): void { + if (this.locationUpdateCallbacks) { + this.locationUpdateCallbacks.forEach(callback => { + try { + callback(locationUpdate); + } catch (error) { + console.error('位置更新回调执行失败:', error); + } + }); + } + } + + /** + * 触发在线用户列表回调 + * @param userList 在线用户列表信息 + */ + private triggerOnlineUserListCallbacks(userList: any): void { + if (this.onlineUserListCallbacks) { + this.onlineUserListCallbacks.forEach(callback => { + try { + callback(userList); + } catch (error) { + console.error('在线用户列表回调执行失败:', error); + } + }); + } + } /** * 发送WebSocket消息 @@ -652,62 +788,35 @@ async ServerLogin(code: string) { } /** - * 订阅位置更新 - * @param deliveryPersonId 配送员ID + * 订阅位置更新(服务器自动处理,无需发送消息) + * @param userId 用户ID */ - async subscribeToLocationUpdates(deliveryPersonId: number): Promise { - if (isMockMode) { - console.log('[MOCK] 订阅位置更新'); - return; - } - + async subscribeToLocationUpdates(): Promise { // 确保WebSocket连接已建立 if (!this.locationWebSocket) { this.initLocationWebSocket(); } - // 发送订阅消息 - const subscribeMessage = { - type: 'subscribe', - deliveryPersonId: deliveryPersonId, - timestamp: Date.now() - }; - - this.sendWebSocketMessage(subscribeMessage); + // 服务器会自动处理订阅逻辑,无需发送订阅消息 + console.log('位置订阅已初始化,服务器将自动处理订阅逻辑'); } /** - * 取消订阅位置更新 - * @param deliveryPersonId 配送员ID + * 取消订阅位置更新(服务器自动处理,无需发送消息) + * @param userId 用户ID */ - async unsubscribeFromLocationUpdates(deliveryPersonId: number): Promise { - if (isMockMode) { - console.log('[MOCK] 取消订阅位置更新'); - return; - } - - // 发送取消订阅消息 - const unsubscribeMessage = { - type: 'unsubscribe', - deliveryPersonId: deliveryPersonId, - timestamp: Date.now() - }; - - this.sendWebSocketMessage(unsubscribeMessage); + async unsubscribeFromLocationUpdates(userId: number): Promise { + // 服务器会自动处理取消订阅逻辑,无需发送取消订阅消息 + console.log('位置取消订阅已初始化,服务器将自动处理取消订阅逻辑'); } /** * 发送位置更新 - * @param deliveryPersonId 配送员ID + * @param userId 用户ID * @param latitude 纬度 * @param longitude 经度 */ - async sendLocationUpdate(deliveryPersonId: number, latitude: number, longitude: number): Promise { - if (isMockMode) { - console.log('[MOCK] 发送位置更新'); - return; - } - + async sendLocationUpdate(userId: number, latitude: number, longitude: number): Promise { // 确保WebSocket连接已建立 if (!this.locationWebSocket) { this.initLocationWebSocket(); @@ -716,7 +825,7 @@ async ServerLogin(code: string) { // 发送位置更新消息 const locationMessage = { type: 'updateLocation', - deliveryPersonId: deliveryPersonId, + userId: userId, latitude: latitude, longitude: longitude, timestamp: Date.now() @@ -725,6 +834,8 @@ async ServerLogin(code: string) { this.sendWebSocketMessage(locationMessage); } + + // ==================== 订单接口 ==================== /** diff --git a/miniprogram/services/deliveryPersonService.ts b/miniprogram/services/deliveryPersonService.ts index 664349a..6fe47e5 100644 --- a/miniprogram/services/deliveryPersonService.ts +++ b/miniprogram/services/deliveryPersonService.ts @@ -1,66 +1,18 @@ // 货运人员服务 - 处理货运人员相关的数据操作 -import { DeliveryPerson, LocationData } from '../types'; +import { DeliveryPerson } from '../types'; import apiService from './apiService'; -import { isMockMode } from './apiService'; /** * 货运人员服务类 - * 提供货运人员信息管理、位置更新、状态管理等功能 + * 提供货运人员信息管理、状态管理等功能 */ class DeliveryPersonService { - // 模拟货运人员数据 - private mockDeliveryPersons: DeliveryPerson[]; /** * 构造函数 */ constructor() { - this.mockDeliveryPersons = [ - { - id: 101, - name: '张师傅', - phone: '13812345678', - status: 'idle', - currentLocation: { - longitude: 102.714585, - latitude: 25.046321 - }, - currentOrders: [] - }, - { - id: 102, - name: '李师傅', - phone: '13912345678', - status: 'busy', - currentLocation: { - longitude: 102.690181, - latitude: 24.994788 - }, - currentOrders: [ - { - id: 1002, - startPoint: { - id: 2, - name: '瓦尔塔蓄电池盘龙区分店', - longitude: 102.728421, - latitude: 25.042498 - }, - endPoint: { - name: '云南中致远汽车销售有限公司', - longitude: 102.796212, - latitude: 24.936947 - }, - status: 'assigned', - goodsType: '瓦尔塔EFB蓄电池', - goodsWeight: 120, - createTime: Date.now() - 7200000 - } - ] - } - ]; - this.locationUpdateCallbacks = null; - this.locationUpdateTimer = null; - this.unsubscribeFunction = null; + // 不再使用模拟数据 } /** @@ -68,11 +20,6 @@ class DeliveryPersonService { * @returns 货运人员列表 */ async getDeliveryPersons(): Promise { - if (isMockMode) { - // 模拟数据 - 返回所有货运人员 - console.log('[MOCK] 获取所有货运人员'); - return [...this.mockDeliveryPersons]; - } return apiService.getDeliveryPersons(); } @@ -82,12 +29,6 @@ class DeliveryPersonService { * @returns 货运人员信息或null */ async getDeliveryPersonById(id: number): Promise { - if (isMockMode) { - // 模拟数据 - 根据ID查找货运人员 - console.log('[MOCK] 根据ID获取货运人员,ID:', id); - const person = this.mockDeliveryPersons.find(p => p.id === id); - return person || null; - } try { const result = await apiService.getDeliveryPersonById(id); return result; @@ -97,70 +38,11 @@ class DeliveryPersonService { } } - /** - * 更新货运人员位置 - * @param id 货运人员ID - * @param location 位置信息 - */ - async updateDeliveryPersonLocation( - id: number, - location: { longitude: number; latitude: number } - ): Promise { - if (isMockMode) { - // 模拟数据 - 更新货运人员位置 - console.log('[MOCK] 更新货运人员位置,ID:', id, '位置:', location); - const person = this.mockDeliveryPersons.find(p => p.id === id); - if (person) { - person.currentLocation = location; - } - return; - } - return apiService.updateDeliveryPersonLocation(id, location); - } - - /** - * 批量更新货运人员位置 - * @param locations 位置信息数组 - */ - async batchUpdateDeliveryPersonLocations(locations: Array<{ - deliveryPersonId: number; - longitude: number; - latitude: number; - timestamp: number; - }>): Promise { - if (isMockMode) { - // 模拟数据 - 批量更新货运人员位置 - console.log('[MOCK] 批量更新货运人员位置'); - locations.forEach(item => { - const person = this.mockDeliveryPersons.find(p => p.id === item.deliveryPersonId); - if (person) { - person.currentLocation = { longitude: item.longitude, latitude: item.latitude }; - } - }); - return; - } - // 修复:转换参数格式以匹配apiService的要求 - const formattedLocations = locations.map(loc => ({ - userId: loc.deliveryPersonId, // 将deliveryPersonId转换为userId - longitude: loc.longitude, - latitude: loc.latitude, - timestamp: loc.timestamp - })); - return apiService.batchUpdateDeliveryPersonLocations(formattedLocations); - } - - - /** * 获取空闲的货运人员 * @returns 空闲货运人员列表 */ async getIdleDeliveryPersons(): Promise { - if (isMockMode) { - // 模拟数据 - 返回空闲的货运人员 - console.log('[MOCK] 获取空闲的货运人员'); - return this.mockDeliveryPersons.filter(p => p.status === 'idle'); - } return apiService.getIdleDeliveryPersons(); } @@ -169,230 +51,9 @@ class DeliveryPersonService { * @returns 忙碌货运人员列表 */ async getBusyDeliveryPersons(): Promise { - if (isMockMode) { - // 模拟数据 - 返回忙碌的货运人员 - console.log('[MOCK] 获取忙碌的货运人员'); - return this.mockDeliveryPersons.filter(p => p.status === 'busy'); - } return apiService.getBusyDeliveryPersons(); } - /** - * 获取位置历史记录 - * @param deliveryPersonId 货运人员ID - * @param limit 记录数量 - * @returns 位置历史记录 - */ - async getLocationHistory(deliveryPersonId: number, limit: number = 50): Promise> { - if (isMockMode) { - // 模拟数据 - 返回位置历史记录 - console.log('[MOCK] 获取货运人员位置历史记录,ID:', deliveryPersonId); - const mockHistory = []; - const person = this.mockDeliveryPersons.find(p => p.id === deliveryPersonId); - - if (person) { - // 生成一些模拟的位置历史记录 - for (let i = 0; i < limit; i++) { - // 在当前位置附近随机生成一些位置点 - const baseLongitude = person.currentLocation.longitude; - const baseLatitude = person.currentLocation.latitude; - - mockHistory.push({ - timestamp: Date.now() - i * 60000, // 每分钟一个点 - longitude: baseLongitude + (Math.random() - 0.5) * 0.01, - latitude: baseLatitude + (Math.random() - 0.5) * 0.01, - speed: Math.random() * 60, // 0-60 km/h - accuracy: 5 + Math.random() * 15 // 5-20 meters - }); - } - } - - return mockHistory; - } - return apiService.getLocationHistory(deliveryPersonId, limit); - } - - /** - * 订阅实时位置更新(兼容mapService参数) - * @param callback 位置更新回调函数 - */ - async subscribeToRealTimeLocations(callback: (location: LocationData) => void): Promise { - if (isMockMode) { - // 模拟实时位置更新 - console.log('[MOCK] 订阅实时位置更新'); - - // 保存回调引用,以便在取消订阅时使用 - if (!this.locationUpdateCallbacks) { - this.locationUpdateCallbacks = new Set(); - } - this.locationUpdateCallbacks.add(callback); - - // 如果是首次订阅,启动模拟更新 - if (!this.locationUpdateTimer) { - this.startMockLocationUpdates(); - } - - return; - } - - // 真实环境中实现WebSocket连接 - try { - // 获取当前用户信息 - const userInfo = wx.getStorageSync('userInfo'); - if (!userInfo || !userInfo.id) { - throw new Error('用户未登录,无法订阅位置更新'); - } - - // 初始化WebSocket连接 - await apiService.initLocationWebSocket(); - - // 订阅位置更新 - await apiService.subscribeToLocationUpdates(userInfo.id); - - // 设置位置更新回调 - this.unsubscribeFunction = apiService.onLocationUpdate((location) => { - callback(location as LocationData); - }); - - console.log('WebSocket位置订阅成功'); - } catch (error) { - console.error('订阅实时位置更新失败:', error); - throw error; - } - } - - /** - * 取消订阅实时位置更新(兼容无参数调用) - */ - async unsubscribeFromRealTimeLocations(): Promise { - if (isMockMode) { - // 模拟取消订阅 - console.log('[MOCK] 取消订阅实时位置更新'); - - // 清除所有回调 - if (this.locationUpdateCallbacks) { - this.locationUpdateCallbacks.clear(); - } - - // 停止模拟更新定时器 - if (this.locationUpdateTimer) { - clearInterval(this.locationUpdateTimer); - this.locationUpdateTimer = null; - } - - return; - } - - // 真实环境中关闭WebSocket连接 - try { - // 获取当前用户信息 - const userInfo = wx.getStorageSync('userInfo'); - if (userInfo && userInfo.id) { - // 取消订阅 - await apiService.unsubscribeFromLocationUpdates(userInfo.id); - } - - // 调用取消订阅函数 - if (this.unsubscribeFunction) { - this.unsubscribeFunction(); - this.unsubscribeFunction = null; - } - - // 关闭WebSocket连接 - apiService.closeLocationWebSocket(); - - console.log('WebSocket位置订阅已取消'); - } catch (error) { - console.error('取消订阅实时位置更新失败:', error); - throw error; - } - } - - /** - * 发送位置更新到服务器 - * @param deliveryPersonId 配送员ID - * @param latitude 纬度 - * @param longitude 经度 - */ - async sendLocationUpdate(deliveryPersonId: number, latitude: number, longitude: number): Promise { - if (isMockMode) { - console.log('[MOCK] 发送位置更新:', { deliveryPersonId, latitude, longitude }); - return; - } - - try { - await apiService.sendLocationUpdate(deliveryPersonId, latitude, longitude); - console.log('位置更新发送成功'); - } catch (error) { - console.error('发送位置更新失败:', error); - throw error; - } - } - - // ===== 私有辅助方法 ===== - - /** - * 存储位置更新回调函数的集合 - */ - private locationUpdateCallbacks: Set<(location: LocationData) => void> | null; - - /** - * 模拟位置更新的定时器 - */ - private locationUpdateTimer: number | null; - - - - /** - * 取消订阅函数引用 - */ - private unsubscribeFunction: (() => void) | null; - - /** - * 启动模拟位置更新 - */ - private startMockLocationUpdates(): void { - // 每5秒发送一次模拟位置更新 - this.locationUpdateTimer = setInterval(() => { - if (this.locationUpdateCallbacks && this.locationUpdateCallbacks.size > 0) { - // 为每个货运人员生成模拟位置更新 - this.mockDeliveryPersons.forEach(person => { - // 在当前位置附近随机生成新位置 - const baseLongitude = person.currentLocation.longitude; - const baseLatitude = person.currentLocation.latitude; - - const newLocation: LocationData = { - userId: person.id, - longitude: baseLongitude + (Math.random() - 0.5) * 0.001, // 小范围随机移动 - latitude: baseLatitude + (Math.random() - 0.5) * 0.001, // 小范围随机移动 - timestamp: Date.now() - }; - - // 更新模拟数据中的位置 - person.currentLocation = { - longitude: newLocation.longitude, - latitude: newLocation.latitude - }; - - // 通知所有回调函数 - this.locationUpdateCallbacks!.forEach(callback => { - try { - callback(newLocation); - } catch (error) { - console.error('位置更新回调执行失败:', error); - } - }); - }); - } - }, 5000); // 5秒更新一次 - } - /** * 获取货运人员当前订单 * @param deliveryPersonId 货运人员ID @@ -416,12 +77,6 @@ class DeliveryPersonService { goodsWeight: number; createTime: number; }>> { - if (isMockMode) { - // 模拟数据 - 返回货运人员当前订单 - console.log('[MOCK] 获取货运人员当前订单,ID:', deliveryPersonId); - const person = this.mockDeliveryPersons.find(p => p.id === deliveryPersonId); - return person ? person.currentOrders : []; - } return apiService.getDeliveryPersonOrders(deliveryPersonId); } } diff --git a/miniprogram/services/employeeService.ts b/miniprogram/services/employeeService.ts new file mode 100644 index 0000000..4e7ed55 --- /dev/null +++ b/miniprogram/services/employeeService.ts @@ -0,0 +1,118 @@ +// 员工管理服务 - 处理员工相关的数据操作 +import { EmployeeInfo } from '../types'; + +import apiService from './apiService'; + +/** + * 员工管理服务类 + * 提供员工信息的增删改查功能 + */ +class EmployeeService { + + /** + * 构造函数 + */ + constructor() { + // 不再使用模拟数据 + } + + /** + * 获取所有员工列表 + * @returns 员工信息数组 + */ + async getEmployees(): Promise { + try { + return await apiService.getEmployees(); + } catch (error) { + console.error('获取员工列表失败:', error); + // API调用失败时返回空数组 + return []; + } + } + + /** + * 添加新员工 + * @param employeeInfo 员工信息 + * @returns 添加的员工信息 + */ + async addEmployee(employeeInfo: { name: string; phone: string; role: string }): Promise { + try { + return await apiService.addEmployee(employeeInfo); + } catch (error) { + console.error('添加员工失败:', error); + throw new Error('添加员工失败,请稍后重试'); + } + } + + /** + * 删除员工 + * @param employeeId 员工ID + * @returns 删除结果 + */ + async deleteEmployee(employeeId: number): Promise<{ success: boolean; message?: string }> { + try { + return await apiService.deleteEmployee(employeeId); + } catch (error) { + console.error('删除员工失败:', error); + return { + success: false, + message: '删除员工失败,请稍后重试' + }; + } + } + + /** + * 更新员工信息 + * @param employeeId 员工ID + * @param employeeInfo 员工信息 + * @returns 更新后的员工信息 + */ + async updateEmployee(employeeId: number, employeeInfo: { name?: string; phone?: string; role?: string }): Promise { + try { + return await apiService.updateEmployee(employeeId, employeeInfo); + } catch (error) { + console.error('更新员工信息失败:', error); + throw new Error('更新员工信息失败,请稍后重试'); + } + } + + /** + * 根据手机号查找员工 + * @param phone 手机号 + * @returns 员工信息或null + */ + async findEmployeeByPhone(phone: string): Promise { + const employees = await this.getEmployees(); + return employees.find(emp => emp.phone === phone) || null; + } + + /** + * 验证员工信息(用于注册时检查) + * @param name 姓名 + * @param phone 手机号 + * @returns 验证结果 + */ + async validateEmployee(name: string, phone: string): Promise<{ success: boolean; message?: string; employee?: EmployeeInfo }> { + const employees = await this.getEmployees(); + const employee = employees.find(emp => emp.name === name && emp.phone === phone); + + if (employee) { + return { + success: true, + message: '员工信息验证成功', + employee + }; + } else { + return { + success: false, + message: '员工信息不存在,请联系管理员添加' + }; + } + } +} + +/** + * 员工管理服务单例实例 + * 导出供应用程序全局使用 + */ +export default new EmployeeService(); \ No newline at end of file diff --git a/miniprogram/services/locationTrackingService.ts b/miniprogram/services/locationTrackingService.ts index fa04268..2fad5e6 100644 --- a/miniprogram/services/locationTrackingService.ts +++ b/miniprogram/services/locationTrackingService.ts @@ -1,8 +1,8 @@ // 位置追踪服务 - 处理用户签到后的位置追踪和状态管理 import { UserInfo, LocationData } from '../types'; import apiService from './apiService'; -import { isMockMode } from './apiService'; import userService from './userService'; +import mapService from './mapService'; /** * 在线用户信息接口 @@ -11,6 +11,7 @@ export interface OnlineUserInfo { userId: number; name: string; avatarUrl: string; + role: string; // 用户角色:'admin' | 'delivery_person' lastLocation: LocationData; lastUpdateTime: number; status: 'online' | 'offline' | 'timeout'; @@ -73,17 +74,50 @@ class LocationTrackingService { // 将当前用户添加到在线列表 await this.addUserToOnlineList(userInfo); + // 签到消息已通过REST API处理,服务器会自动处理订阅逻辑 + + // 设置实时位置订阅(初始化WebSocket连接) + await this.setupRealTimeSubscription(); + + // WebSocket连接成功后立即发送第一次位置信息 + try { + const location = await this.getCurrentLocation(); + await this.updateUserLocation(location); + console.log('WebSocket连接成功后立即发送第一次位置信息'); + } catch (error) { + console.error('立即发送位置信息失败:', error); + } + // 启动位置更新定时器(每30秒更新一次) this.startLocationUpdateTimer(); // 启动状态检查定时器(每60秒检查一次) this.startStatusCheckTimer(); - // 订阅其他用户的位置更新 - await this.subscribeToOtherUsersLocations(); - console.log('位置追踪服务已启动'); } + + /** + * 初始化位置追踪服务 + */ + public async initialize(): Promise { + try { + // 注册位置更新回调 + apiService.onLocationUpdate((locationUpdate) => { + this.handleLocationUpdate(locationUpdate); + }); + + // 注册在线用户列表回调 + apiService.onOnlineUserList((userList) => { + this.handleOnlineUserList(userList); + }); + + console.log('[LocationTrackingService] 位置追踪服务初始化完成'); + } catch (error) { + console.error('[LocationTrackingService] 初始化失败:', error); + throw error; + } + } /** * 用户签退后停止位置追踪 @@ -96,14 +130,16 @@ class LocationTrackingService { this.isSignedIn = false; + // 签退消息已通过REST API处理,服务器会自动处理取消订阅逻辑 + // 从在线列表中移除用户 this.removeUserFromOnlineList(userInfo.id); // 停止定时器 this.stopTimers(); - // 取消订阅 - await this.unsubscribeFromLocations(); + // 关闭实时位置订阅(服务器会自动处理取消订阅) + this.teardownRealTimeSubscription(); console.log('位置追踪服务已停止'); } @@ -131,18 +167,12 @@ class LocationTrackingService { timestamp: Date.now() }; - if (isMockMode) { - // 模拟模式:更新本地在线用户列表 - console.log('[位置追踪服务] 模拟模式:更新本地位置'); - this.updateLocalUserLocation(userInfo.id, locationData); - } else { - // 真实模式:调用API更新位置 - console.log('[位置追踪服务] 真实模式:调用API更新位置'); - await apiService.updateDeliveryPersonLocation(userInfo.id, location); - - // 同时更新本地缓存 - this.updateLocalUserLocation(userInfo.id, locationData); - } + // 真实模式:通过WebSocket发送位置更新 + console.log('[位置追踪服务] 真实模式:通过WebSocket发送位置更新'); + await apiService.sendLocationUpdate(userInfo.id, location.latitude, location.longitude); + + // 同时更新本地缓存 + this.updateLocalUserLocation(userInfo.id, locationData); // 通知所有回调 this.notifyLocationUpdateCallbacks(); @@ -206,6 +236,7 @@ class LocationTrackingService { userId: userInfo.id, name: userInfo.name || '未知用户', avatarUrl: '/images/user-avatar.png', + role: userInfo.role || 'delivery_person', // 用户角色 lastLocation: currentLocation, lastUpdateTime: Date.now(), status: 'online' @@ -213,7 +244,7 @@ class LocationTrackingService { this.onlineUsers.set(userInfo.id, onlineUser); - + console.log(`用户 ${userInfo.id} 已添加到在线列表`); } @@ -305,101 +336,49 @@ class LocationTrackingService { throw new Error('用户未登录'); } - // 这里应该调用地图服务获取当前位置 - // 暂时返回一个默认位置 - return { - userId: userInfo.id, - longitude: 102.833722, - latitude: 24.880095, - timestamp: Date.now() - }; - } - - /** - * 订阅其他用户的位置更新 - */ - private async subscribeToOtherUsersLocations(): Promise { - if (isMockMode) { - // 模拟模式:定时生成其他用户的位置更新 - this.startMockOtherUsersUpdates(); - } else { - // 真实模式:通过WebSocket订阅 - await this.setupRealTimeSubscription(); + try { + // 使用mapService获取真实位置,保持与项目中其他地方的一致性 + const location = await mapService.getLocation(); + + console.log('[位置追踪服务] 获取真实位置成功:', location); + + return { + userId: userInfo.id, + longitude: location.longitude, + latitude: location.latitude, + timestamp: Date.now() + }; + } catch (error) { + console.error('[位置追踪服务] 获取真实位置失败,使用默认位置:', error); + + // 定位失败时返回默认位置作为降级方案 + return { + userId: userInfo.id, + longitude: 102.833722, + latitude: 24.880095, + timestamp: Date.now() + }; } } + + /** * 取消订阅位置更新 */ private async unsubscribeFromLocations(): Promise { - if (!isMockMode) { - // 真实模式:取消WebSocket订阅 - await this.teardownRealTimeSubscription(); - } + // 真实模式:取消WebSocket订阅 + await this.teardownRealTimeSubscription(); } - /** - * 启动模拟的其他用户位置更新 - */ - private startMockOtherUsersUpdates(): void { - // 模拟其他用户的位置更新(每45秒一次) - setInterval(() => { - if (this.isSignedIn) { - this.generateMockOtherUsersLocations(); - this.notifyLocationUpdateCallbacks(); - } - }, 45000); - } + - /** - * 生成模拟的其他用户位置 - */ - private generateMockOtherUsersLocations(): void { - const currentUserInfo = userService.getGlobalUserInfo(); - if (!currentUserInfo) return; - - // 模拟2个其他在线用户 - const mockUserIds = [101, 102]; // 模拟用户ID - - mockUserIds.forEach(userId => { - if (userId !== currentUserInfo.id) { - const existingUser = this.onlineUsers.get(userId); - - if (existingUser) { - // 更新现有用户位置 - existingUser.lastLocation.longitude += (Math.random() - 0.5) * 0.001; - existingUser.lastLocation.latitude += (Math.random() - 0.5) * 0.001; - existingUser.lastUpdateTime = Date.now(); - existingUser.status = 'online'; - - this.onlineUsers.set(userId, existingUser); - } else { - // 添加新用户 - const newUser: OnlineUserInfo = { - userId, - name: `用户${userId}`, - avatarUrl: '/images/user-avatar.png', - lastLocation: { - userId, - longitude: 102.833722 + (Math.random() - 0.5) * 0.01, - latitude: 24.880095 + (Math.random() - 0.5) * 0.01, - timestamp: Date.now() - }, - lastUpdateTime: Date.now(), - status: 'online' - }; - - this.onlineUsers.set(userId, newUser); - this.notifyUserStatusChange(userId, 'online'); - } - } - }); - } + /** * 设置实时位置订阅 */ - private setupRealTimeSubscription(): void { + private async setupRealTimeSubscription(): Promise { // 获取用户信息 const userInfo = userService.getGlobalUserInfo(); if (!userInfo || !userInfo.id) { @@ -410,25 +389,31 @@ class LocationTrackingService { // 缓存用户信息 this.userInfo = userInfo; - if (isMockMode) { - console.log('[MOCK] 设置实时位置订阅'); - this.startMockLocationGeneration(); - return; - } - - // 真实环境:初始化WebSocket连接并订阅位置更新 + // 真实环境:初始化WebSocket连接(服务器会自动处理订阅逻辑) try { - // 使用新的API接口 - apiService.initLocationWebSocket(); + // 初始化WebSocket连接(如果尚未连接) + await apiService.initLocationWebSocket(); - // 订阅当前用户的位置更新 - apiService.subscribeToLocationUpdates(this.userInfo.id) - .then(() => { - console.log('成功订阅位置更新'); - }) - .catch((error) => { - console.error('订阅位置更新失败:', error); - }); + // 等待WebSocket连接建立(最多等待5秒) + await new Promise((resolve, reject) => { + let attempts = 0; + const maxAttempts = 50; // 5秒超时 + + const checkConnection = () => { + if (apiService.isWebSocketConnected()) { + resolve(); + } else if (attempts < maxAttempts) { + attempts++; + setTimeout(checkConnection, 100); + } else { + reject(new Error('WebSocket连接建立超时')); + } + }; + checkConnection(); + }); + + // 服务器会自动处理订阅逻辑,无需发送订阅消息 + console.log('WebSocket连接已建立,服务器将自动处理位置订阅逻辑'); // 设置位置更新回调 this.unsubscribeCallback = apiService.onLocationUpdate((locationUpdate) => { @@ -444,24 +429,10 @@ class LocationTrackingService { * 关闭实时位置订阅 */ private teardownRealTimeSubscription(): void { - if (isMockMode) { - console.log('[MOCK] 关闭实时位置订阅'); - this.stopMockLocationGeneration(); - return; - } - - // 真实环境:取消订阅并关闭WebSocket连接 + // 真实环境:关闭WebSocket连接(服务器会自动处理取消订阅逻辑) try { - if (this.userInfo) { - // 使用新的API接口取消订阅 - apiService.unsubscribeFromLocationUpdates(this.userInfo.id) - .then(() => { - console.log('成功取消订阅位置更新'); - }) - .catch((error) => { - console.error('取消订阅位置更新失败:', error); - }); - } + // 服务器会自动处理取消订阅逻辑,无需发送取消订阅消息 + console.log('关闭WebSocket连接,服务器将自动处理取消订阅逻辑'); // 移除位置更新回调 if (this.unsubscribeCallback) { @@ -532,28 +503,88 @@ class LocationTrackingService { }); } - /** - * 启动模拟位置生成 - */ - private startMockLocationGeneration(): void { - console.log('[MOCK] 启动模拟位置生成'); - // 这里可以添加模拟位置生成的逻辑 - } - /** - * 停止模拟位置生成 - */ - private stopMockLocationGeneration(): void { - console.log('[MOCK] 停止模拟位置生成'); - // 这里可以添加停止模拟位置生成的逻辑 - } /** * 处理位置更新 */ private handleLocationUpdate(locationUpdate: any): void { console.log('收到位置更新:', locationUpdate); - // 这里可以添加处理位置更新的逻辑 + + // 验证位置更新消息格式 + if (!locationUpdate || typeof locationUpdate !== 'object') { + console.warn('无效的位置更新消息格式'); + return; + } + + // 提取关键信息 + const { deliveryPersonId, latitude, longitude, timestamp } = locationUpdate; + + if (!deliveryPersonId || latitude === undefined || longitude === undefined) { + console.warn('位置更新消息缺少必要字段:', locationUpdate); + return; + } + + console.log(`处理员工 ${deliveryPersonId} 的位置更新: (${longitude}, ${latitude})`); + + // 更新本地缓存中的员工位置 + this.updateLocalUserLocation(deliveryPersonId, { + userId: deliveryPersonId, + longitude: longitude, + latitude: latitude, + timestamp: timestamp || Date.now() + }); + + // 通知所有位置更新回调(UI层会监听这些回调来更新地图标记点) + this.notifyLocationUpdateCallbacks(); + + console.log(`员工 ${deliveryPersonId} 位置更新处理完成`); + } + + /** + * 处理在线用户列表消息 + * @param userList 在线用户列表消息 + */ + private handleOnlineUserList(userList: any): void { + try { + console.log('[LocationTrackingService] 收到在线用户列表:', userList); + + if (!userList || !userList.users) { + console.warn('[LocationTrackingService] 无效的在线用户列表消息'); + return; + } + + // 清空当前在线用户列表 + this.onlineUsers.clear(); + + // 更新在线用户列表 + userList.users.forEach((userInfo: any) => { + if (userInfo && userInfo.userId) { + this.onlineUsers.set(userInfo.userId, { + userId: userInfo.userId, + name: userInfo.userName || '未知用户', + avatarUrl: '/images/user-avatar.png', + role: userInfo.role || 'delivery_person', // 用户角色,默认为配送员 + lastLocation: { + userId: userInfo.userId, + longitude: userInfo.longitude || 0, + latitude: userInfo.latitude || 0, + timestamp: userInfo.lastUpdateTime || Date.now() + }, + lastUpdateTime: userInfo.lastUpdateTime || Date.now(), + status: 'online' + }); + } + }); + + console.log('[LocationTrackingService] 在线用户列表已更新,当前在线用户数:', this.onlineUsers.size); + + // 通知位置模块更新地图标记点 + this.notifyLocationUpdateCallbacks(); + + } catch (error) { + console.error('[LocationTrackingService] 处理在线用户列表失败:', error); + } } } diff --git a/miniprogram/services/mapService.ts b/miniprogram/services/mapService.ts index fb7d2be..0e346a8 100644 --- a/miniprogram/services/mapService.ts +++ b/miniprogram/services/mapService.ts @@ -1,6 +1,5 @@ // 地图服务 - 处理地图相关的功能,包括定位、路线规划、地理编码等 -import apiService from './apiService'; -import { isMockMode } from './apiService'; + import { AMapRegeoResponse, RoutePlanResult, SearchResult, LocationData } from '../types'; /** @@ -20,14 +19,7 @@ class MapService { // 高德地图实例 private amapInstance: any; - // 定时器引用,用于模拟实时位置更新 - private locationUpdateTimer: number | null; - - // 位置更新回调函数 - private locationUpdateCallback: ((location: LocationData) => void) | null; - - // 取消订阅函数 - private unsubscribeFunction: (() => void) | null; + /** * 构造函数,初始化地图实例 @@ -40,9 +32,6 @@ class MapService { latitude: 24.880095 }; this.amapInstance = new this.amapFile.AMapWX({ key: this.MAP_KEY }); - this.locationUpdateTimer = null; - this.locationUpdateCallback = null; - this.unsubscribeFunction = null; } /** @@ -50,15 +39,6 @@ class MapService { * @returns 用户位置坐标 */ async getLocation(): Promise<{ longitude: number; latitude: number }> { - if (isMockMode) { - // 模拟位置 - 昆明市中心 - console.log('[MOCK] 获取用户位置信息'); - return { - longitude: 102.7123, - latitude: 25.0409 - }; - } - return new Promise((resolve, reject) => { console.log('开始调用高德地图SDK获取位置...'); @@ -149,18 +129,6 @@ class MapService { speed?: number; altitude?: number; }> { - if (isMockMode) { - // 模拟位置 - 昆明市中心 - console.log('[MOCK] 获取当前位置信息(兼容格式)'); - return { - latitude: 25.0409, - longitude: 102.7123, - accuracy: 50, - speed: 0, - altitude: 1891 - }; - } - const location = await this.getLocation(); return { latitude: location.latitude, @@ -178,23 +146,6 @@ class MapService { * @returns 逆地理编码结果 */ async getLocationInfo(longitude: number, latitude: number): Promise { - if (isMockMode) { - // 模拟逆地理编码结果 - 完全符合AMapRegeoResponse接口 - console.log('[MOCK] 获取位置详细信息(逆地理编码),坐标:', longitude, latitude); - return { - regeocode: { - addressComponent: { - province: '云南省', - city: '昆明市', - district: '五华区', - street: '', - township: '' - } - }, - status: '1' - }; - } - return new Promise((resolve, reject) => { this.amapInstance.getRegeo({ location: `${longitude.toFixed(6)},${latitude.toFixed(6)}`, @@ -225,32 +176,6 @@ class MapService { * @returns 搜索结果列表 */ async searchPoi(keyword: string, city: string): Promise { - if (isMockMode) { - // 模拟搜索结果 - console.log('[MOCK] 搜索地点,关键词:', keyword, '城市:', city); - const mockResults: SearchResult[] = [ - { - id: '1', - name: '昆明火车站', - address: '昆明市官渡区北京路', - longitude: 102.7222, - latitude: 25.0157, - phone: '' - }, - { - id: '2', - name: '昆明长水国际机场', - address: '昆明市官渡区长水村', - longitude: 102.9292, - latitude: 25.1012, - phone: '' - } - ]; - return mockResults.filter(item => - item.name.includes(keyword) || item.address.includes(keyword) - ); - } - return new Promise((resolve, reject) => { this.amapInstance.getPoiAround({ querykeywords: keyword, @@ -289,22 +214,6 @@ class MapService { * @returns 路线规划结果 */ async getDrivingRoute(origin: string, destination: string): Promise { - if (isMockMode) { - // 模拟路线规划结果 - console.log('[MOCK] 规划驾车路线,起点:', origin, '终点:', destination); - const [originLng, originLat] = origin.split(',').map(Number); - const [destLng, destLat] = destination.split(',').map(Number); - - const distance = this.calculateDistance(originLat, originLng, destLat, destLng); - const duration = Math.round(distance / 10); // 假设平均速度10米/秒 - - return { - polyline: '', - distance, - duration - }; - } - return new Promise((resolve, reject) => { this.amapInstance.getDrivingRoute({ origin, @@ -369,41 +278,16 @@ class MapService { * 获取路线规划(通用版本) * @param origin 起点坐标 * @param destination 终点坐标 - * @param mode 交通方式 * @returns 路线规划结果 */ async getRoute( origin: { latitude: number; longitude: number }, - destination: { latitude: number; longitude: number }, - mode: 'driving' | 'walking' | 'bicycling' = 'driving' + destination: { latitude: number; longitude: number } ): Promise<{ distance: number; // 距离(米) duration: number; // 时间(秒) polyline: { latitude: number; longitude: number }[]; }> { - if (isMockMode) { - // 模拟路线规划 - console.log('[MOCK] 获取路线规划,起点:', origin, '终点:', destination, '模式:', mode); - const distance = this.calculateDistance( - origin.latitude, origin.longitude, - destination.latitude, destination.longitude - ); - - const duration = Math.round(distance / 10); // 假设平均速度10米/秒 - - // 简单的直线路径 - const polyline = [ - origin, - destination - ]; - - return { - distance, - duration, - polyline - }; - } - // 对于真实模式,目前只支持驾车路线 const originStr = `${origin.longitude},${origin.latitude}`; const destStr = `${destination.longitude},${destination.latitude}`; @@ -442,34 +326,6 @@ class MapService { longitude: number; formattedAddress: string; }> { - if (isMockMode) { - // 模拟地理编码结果 - console.log('[MOCK] 地理编码,地址:', address); - const mockLocations: { [key: string]: { latitude: number; longitude: number; formattedAddress: string } } = { - '昆明市五华区二环西路599号': { - latitude: 25.055281, - longitude: 102.705745, - formattedAddress: '云南省昆明市五华区二环西路599号' - }, - '昆明市盘龙区北京路1188号': { - latitude: 25.042498, - longitude: 102.728421, - formattedAddress: '云南省昆明市盘龙区北京路1188号' - }, - '昆明市西山区滇池路1234号': { - latitude: 25.028234, - longitude: 102.689190, - formattedAddress: '云南省昆明市西山区滇池路1234号' - } - }; - - return mockLocations[address] || { - latitude: 25.0409, - longitude: 102.7123, - formattedAddress: '云南省昆明市' - }; - } - // 对于真实模式,使用搜索API来模拟地理编码 const results = await this.searchPoi(address, '昆明'); if (results.length > 0) { @@ -502,18 +358,6 @@ class MapService { district: string; street: string; }> { - if (isMockMode) { - console.log('[MOCK] 逆地理编码,坐标:', latitude, longitude); - return { - formattedAddress: '云南省昆明市五华区', - country: '中国', - province: '云南省', - city: '昆明市', - district: '五华区', - street: '' - }; - } - const result = await this.getLocationInfo(longitude, latitude); return { @@ -548,39 +392,6 @@ class MapService { address: string; distance: number; }>> { - if (isMockMode) { - // 模拟附近地点 - console.log('[MOCK] 获取附近的地点,坐标:', latitude, longitude, '半径:', radius, '类型:', type); - const mockPlaces = [ - { - id: '1', - name: '昆明火车站', - latitude: 25.0157, - longitude: 102.7222, - address: '昆明市官渡区北京路', - distance: this.calculateDistance(latitude, longitude, 25.0157, 102.7222) - }, - { - id: '2', - name: '昆明长水国际机场', - latitude: 25.1012, - longitude: 102.9292, - address: '昆明市官渡区长水村', - distance: this.calculateDistance(latitude, longitude, 25.1012, 102.9292) - }, - { - id: '3', - name: '翠湖公园', - latitude: 25.0486, - longitude: 102.7042, - address: '昆明市五华区翠湖公园', - distance: this.calculateDistance(latitude, longitude, 25.0486, 102.7042) - } - ]; - - return mockPlaces.filter(place => place.distance <= radius); - } - // 对于真实模式,使用搜索API const keyword = type ? type : ''; const results = await this.searchPoi(keyword, '昆明'); @@ -595,164 +406,13 @@ class MapService { })).filter(place => place.distance <= radius); } - /** - * 获取位置历史记录 - * @param userId 用户ID - * @param limit 记录数量限制 - * @returns 位置历史记录列表 - */ - async getLocationHistory(userId: number, limit: number = 50): Promise> { - if (isMockMode) { - // 模拟位置历史记录 - console.log('[MOCK] 获取位置历史记录,用户ID:', userId, '限制:', limit); - const history: Array<{ id: number; latitude: number; longitude: number; timestamp: number; accuracy?: number; speed?: number; altitude?: number }> = []; - - const baseLat = 25.0409; - const baseLng = 102.7123; - - for (let i = 0; i < limit; i++) { - history.push({ - id: i + 1, - latitude: baseLat + (Math.random() - 0.5) * 0.01, - longitude: baseLng + (Math.random() - 0.5) * 0.01, - timestamp: Date.now() - i * 3600000, // 每小时一个记录 - accuracy: 20 + Math.random() * 30, - speed: Math.random() * 10, - altitude: 1891 + (Math.random() - 0.5) * 20 - }); - } - - return history.sort((a, b) => b.timestamp - a.timestamp); - } - // 修复参数名不匹配问题,并转换返回值类型以匹配mapService定义 - const apiResults = await apiService.getLocationHistory(userId, limit); - return apiResults.map((result, index) => ({ - ...result, - id: index + 1, // 添加id字段 - altitude: 1891 + (Math.random() - 0.5) * 20 // 添加默认海拔高度 - })).sort((a, b) => b.timestamp - a.timestamp); - } + - /** - * 批量更新货运人员位置 - * @param locations 位置信息数组 - */ - async batchUpdateDeliveryPersonLocations( - locations: Array<{ userId: number; latitude: number; longitude: number; timestamp: number }> - ): Promise { - if (isMockMode) { - console.log('[MOCK] 批量更新货运人员位置:', locations); - return; - } - return apiService.batchUpdateDeliveryPersonLocations(locations); - } + - /** - * 订阅实时位置更新 - * @param callback 位置更新回调函数 - */ - async subscribeToRealTimeLocations(callback: (location: LocationData) => void): Promise { - if (isMockMode) { - // 模拟模式下,定期生成随机位置更新 - console.log('[MOCK] 订阅实时位置更新'); - - // 保存回调引用 - this.locationUpdateCallback = callback; - - // 启动模拟更新定时器 - if (!this.locationUpdateTimer) { - this.locationUpdateTimer = setInterval(() => { - // 生成随机位置更新数据 - const randomLocation: LocationData = { - userId: Math.floor(Math.random() * 100) + 100, // 模拟货运人员ID - longitude: 102.714585 + (Math.random() - 0.5) * 0.05, // 昆明附近随机经度 - latitude: 25.046321 + (Math.random() - 0.5) * 0.05, // 昆明附近随机纬度 - timestamp: Date.now() - }; - - // 调用回调函数 - if (this.locationUpdateCallback) { - this.locationUpdateCallback(randomLocation); - } - }, 3000); // 每3秒更新一次 - } - - return; - } - - // 真实环境中调用API服务的订阅方法 - try { - // 获取当前用户信息 - const userInfo = wx.getStorageSync('userInfo'); - if (!userInfo || !userInfo.id) { - throw new Error('用户未登录,无法订阅位置更新'); - } - - // 初始化WebSocket连接 - await apiService.initLocationWebSocket(); - - // 订阅位置更新 - await apiService.subscribeToLocationUpdates(userInfo.id); - - // 使用新的onLocationUpdate方法来注册回调 - this.unsubscribeFunction = apiService.onLocationUpdate((location) => { - callback(location as LocationData); - }); - } catch (error) { - console.error('订阅实时位置更新失败:', error); - throw error; - } - } + - /** - * 取消订阅实时位置更新 - */ - async unsubscribeFromRealTimeLocations(): Promise { - if (isMockMode) { - // 模拟模式下,清除定时器 - console.log('[MOCK] 取消订阅实时位置更新'); - - if (this.locationUpdateTimer) { - clearInterval(this.locationUpdateTimer); - this.locationUpdateTimer = null; - } - - // 清除回调引用 - this.locationUpdateCallback = null; - - return; - } - - // 真实环境中调用API服务的取消订阅方法 - try { - // 获取当前用户信息 - const userInfo = wx.getStorageSync('userInfo'); - if (userInfo && userInfo.id) { - // 取消订阅 - await apiService.unsubscribeFromLocationUpdates(userInfo.id); - } - - // 调用取消订阅函数 - if (this.unsubscribeFunction) { - this.unsubscribeFunction(); - this.unsubscribeFunction = null; - } - - // 关闭WebSocket连接 - apiService.closeLocationWebSocket(); - } catch (error) { - console.error('取消订阅实时位置更新失败:', error); - throw error; - } - } + /** * 格式化路线距离 diff --git a/miniprogram/services/orderService.ts b/miniprogram/services/orderService.ts index 2f683b6..7ba97de 100644 --- a/miniprogram/services/orderService.ts +++ b/miniprogram/services/orderService.ts @@ -1,80 +1,19 @@ import { Order } from '../types'; import apiService from './apiService'; -import { isMockMode } from './apiService'; class OrderService { - // 模拟订单数据 - private mockOrders: Order[]; /** - * 构造函数,初始化模拟订单数据 + * 构造函数 */ constructor() { - this.mockOrders = [ - { - id: 1001, - startPoint: { - id: 1, - name: '瓦尔塔蓄电池昆明总店', - longitude: 102.705745, - latitude: 25.055281 - }, - endPoint: { - name: '昆明德众汽车销售服务有限公司', - longitude: 102.714686, - latitude: 25.047134 - }, - status: 'pending', - goodsType: '瓦尔塔AGM蓄电池', - goodsWeight: 50, - createTime: Date.now() - 3600000 // 1小时前创建 - }, - { - id: 1002, - startPoint: { - id: 2, - name: '瓦尔塔蓄电池盘龙区分店', - longitude: 102.728421, - latitude: 25.042498 - }, - endPoint: { - name: '云南中致远汽车销售有限公司', - longitude: 102.796212, - latitude: 24.936947 - }, - status: 'pending', - goodsType: '瓦尔塔EFB蓄电池', - goodsWeight: 120, - createTime: Date.now() - 7200000 // 2小时前创建 - }, - { - id: 1003, - startPoint: { - id: 3, - name: '瓦尔塔蓄电池西山区分店', - longitude: 102.689190, - latitude: 25.028234 - }, - endPoint: { - name: '昆明宝远汽车销售服务有限公司', - longitude: 102.756212, - latitude: 24.986947 - }, - status: 'pending', - goodsType: '瓦尔塔普通铅酸蓄电池', - goodsWeight: 80, - createTime: Date.now() - 10800000 // 3小时前创建 - } - ]; + // 不再使用模拟数据 } /** * 获取所有待处理订单 */ async getPendingOrders(): Promise { - if (isMockMode) { - return [...this.mockOrders]; - } return apiService.getPendingOrders(); } @@ -82,10 +21,6 @@ class OrderService { * 根据ID获取订单 */ async getOrderById(id: number): Promise { - if (isMockMode) { - const order = this.mockOrders.find(o => o.id === id); - return order || null; - } return apiService.getOrderById(id); } @@ -96,20 +31,6 @@ class OrderService { * @returns 指派结果,包含success状态和可选消息 */ async assignOrder(orderId: number, deliveryPersonId: number): Promise<{ success: boolean; message?: string }> { - if (isMockMode) { - // 模拟数据 - 查找订单并更新状态 - const order = this.mockOrders.find(o => o.id === orderId); - if (order) { - order.status = 'assigned'; - order.deliveryPersonId = deliveryPersonId; - console.log(`[MOCK] 订单 ${orderId} 已指派给货运人员 ${deliveryPersonId}`); - return { success: true, message: '指派成功' }; - } else { - console.warn(`[MOCK] 订单 ${orderId} 不存在`); - return { success: false, message: '订单不存在' }; - } - } - // 真实环境中调用API try { return await apiService.assignOrder(orderId, deliveryPersonId); @@ -123,15 +44,6 @@ class OrderService { * 更新订单状态 */ async updateOrderStatus(orderId: number, status: Order['status']): Promise<{ success: boolean; message?: string }> { - if (isMockMode) { - const order = this.mockOrders.find(o => o.id === orderId); - if (order) { - order.status = status; - console.log(`[MOCK] 订单 ${orderId} 状态更新为 ${status}`); - return { success: true, message: '状态更新成功' }; - } - return { success: false, message: '订单不存在' }; - } return apiService.updateOrderStatus(orderId, status).then(result => ({ success: result.success, message: result.message || '状态更新成功' @@ -142,15 +54,6 @@ class OrderService { * 创建新订单 */ async createOrder(orderData: Omit): Promise { - if (isMockMode) { - const newOrder: Order = { - ...orderData, - id: Math.max(...this.mockOrders.map(o => o.id), 1000) + 1, - createTime: Date.now() - }; - this.mockOrders.push(newOrder); - return newOrder; - } return apiService.createOrder(orderData); } @@ -158,10 +61,6 @@ class OrderService { * 获取货运人员的订单列表 */ async getDeliveryPersonOrders(deliveryPersonId: number): Promise { - if (isMockMode) { - // 模拟模式下返回部分订单 - return this.mockOrders.slice(0, 2); - } return apiService.getDeliveryPersonOrders(deliveryPersonId); } @@ -169,17 +68,14 @@ class OrderService { * 删除订单 * @param orderId 订单ID */ - async deleteOrder(orderId: number): Promise { - if (isMockMode) { - // 在模拟数据中删除订单 - const index = this.mockOrders.findIndex(o => o.id === orderId); - if (index !== -1) { - this.mockOrders.splice(index, 1); - console.log(`订单 ${orderId} 已删除`); - } - return; + async deleteOrder(orderId: number): Promise<{ success: boolean; message?: string }> { + try { + const result = await apiService.deleteOrder(orderId); + return { success: true, message: '删除成功' }; + } catch (error) { + console.error('删除订单失败:', error); + return { success: false, message: error instanceof Error ? error.message : '删除失败' }; } - return apiService.deleteOrder(orderId); } } diff --git a/miniprogram/services/userService.ts b/miniprogram/services/userService.ts index 75281ee..b2ae98c 100644 --- a/miniprogram/services/userService.ts +++ b/miniprogram/services/userService.ts @@ -1,32 +1,21 @@ // 用户服务文件 -// 用户服务 - 处理用户相关的数据操作 +// 用户服务 - 处理用户认证、会话管理、权限验证等 import { UserInfo, EmployeeInfo } from '../types'; +import { Role } from '../utils/roleUtils'; import apiService from './apiService'; -import { isMockMode } from './apiService'; /** * 用户服务类 * 提供用户认证、信息管理、权限验证等功能 */ class UserService { - // 模拟用户数据 - private mockUsers: UserInfo[]; /** - * 构造函数,初始化模拟用户数据 + * 构造函数 */ constructor() { - this.mockUsers = [ - { - id: 1, - role: 'ADMIN' - }, - { - id: 2, - role: 'DELIVERY_PERSON' - } - ]; + // 不再使用模拟数据 } /** @@ -34,14 +23,6 @@ class UserService { * @returns 用户信息 */ async getUserInfo(): Promise { - if (isMockMode) { - // 模拟数据 - 返回管理员信息 - console.log('[MOCK] 获取用户信息'); - return { - id: 1, - role: 'ADMIN' - }; - } return apiService.getUserInfo(); } @@ -49,11 +30,6 @@ class UserService { * 用户退出登录 */ async logout(): Promise { - if (isMockMode) { - // 模拟登出 - console.log('[MOCK] 用户登出'); - return; - } return apiService.logout(); } @@ -80,7 +56,7 @@ class UserService { * @returns 是否为管理员 */ isAdmin(): boolean { - return this.getUserRole() === 'ADMIN'; + return this.getUserRole() === Role.ADMIN; } /** @@ -88,7 +64,7 @@ class UserService { * @returns 是否为货运人员 */ isDeliveryPerson(): boolean { - return this.getUserRole() === 'DELIVERY_PERSON'; + return this.getUserRole() === Role.DELIVERY_PERSON; } /** @@ -194,25 +170,6 @@ class UserService { */ async ServerLogin(code: string): Promise<{ success: boolean; openid?: string; token?: string; userInfo?: UserInfo }> { try { - if (isMockMode) { - // 模拟登录成功 - console.log('[MOCK] 微信小程序登录'); - - // 使用模拟用户数据 - const userInfo = this.mockUsers[0]; - const openid = 'mock-openid-' + Math.random().toString(36).substring(2); - const token = 'mock-token-' + Date.now(); - const session_key = 'mock-session-key-' + Math.random().toString(36).substring(2); - - // 保存到本地存储 - wx.setStorageSync('userInfo', userInfo); - wx.setStorageSync('token', token); - wx.setStorageSync('openid', openid); - wx.setStorageSync('session_key', session_key); - - return { success: true, openid, token, userInfo }; - } - // 真实API模式 //TODO: 登录成功的基础数据:服务器下发的公共游客可看的数据 const result = await apiService.ServerLogin(code); @@ -268,14 +225,6 @@ class UserService { * @returns 更新后的用户信息 */ async updateUserInfo(userInfo: Partial): Promise { - if (isMockMode) { - // 模拟更新成功 - console.log('[MOCK] 更新用户信息'); - return { - id: 1, - role: userInfo.role || 'GUEST' - }; - } return apiService.updateUserInfo(userInfo); } @@ -284,14 +233,6 @@ class UserService { * @returns 用户状态信息 */ async getUserStatus(): Promise<{ status: 'signed_in' | 'signed_out' | 'registered' | 'unregistered'; lastSignInTime?: string; lastSignOutTime?: string }> { - if (isMockMode) { - // 模拟数据 - 默认返回已注册状态 - console.log('[MOCK] 获取用户状态'); - return { - status: 'registered' - }; - } - try { // 调用服务器接口获取用户状态 const response = await apiService.getUserStatus(); @@ -328,24 +269,26 @@ class UserService { throw new Error('用户认证信息缺失,请重新登录'); } - if (isMockMode) { - // 模拟签到成功,返回员工信息 - console.log('[MOCK] 用户签到成功'); - const employeeInfo: EmployeeInfo = { - id: userInfo.id, - name: '测试用户', - phone: '13800138000', - role: userInfo.role || 'DELIVERY_PERSON' - }; - - return { - success: true, - employeeInfo: employeeInfo, - message: '签到成功' - }; + return apiService.userSignIn(userInfo.id); + } + + /** + * 用户签退 + * @returns 签退结果 + */ + async signOut(): Promise<{ success: boolean; message?: string }> { + const userInfo = this.getGlobalUserInfo(); + if (!userInfo || !userInfo.id) { + throw new Error('用户未登录,无法签退'); + } + + // 检查是否有有效的token(防止后端空指针异常) + const app = getApp(); + if (!app.globalData.token) { + throw new Error('用户认证信息缺失,请重新登录'); } - return apiService.userSignIn(userInfo.id); + return apiService.userSignOut(userInfo.id); } /** @@ -354,29 +297,6 @@ class UserService { * @returns 注册结果和员工信息 */ async register(registerInfo: { name: string; phone: string }): Promise<{ success: boolean; employeeInfo: EmployeeInfo; message?: string }> { - if (isMockMode) { - // 模拟注册成功 - console.log('[MOCK] 用户注册成功'); - // 确保所有必需字段都有值 - const employeeInfo: EmployeeInfo = { - id: Date.now(), - name: registerInfo.name, - phone: registerInfo.phone, - role: 'DELIVERY_PERSON' - }; - this.mockUsers.push({ - id: employeeInfo.id, - role: employeeInfo.role || 'DELIVERY_PERSON', - name: employeeInfo.name, - phone: employeeInfo.phone - }); - return { - success: true, - employeeInfo: employeeInfo, - message: '注册成功' - }; - } - return apiService.userRegister(registerInfo); } @@ -385,20 +305,6 @@ class UserService { * @returns 权限列表 */ async getUserPermissions(): Promise { - if (isMockMode) { - // 模拟权限列表 - console.log('[MOCK] 获取用户权限列表'); - return [ - 'order:view', - 'order:edit', - 'delivery:view', - 'delivery:edit', - 'warehouse:view', - 'warehouse:edit', - 'user:view', - 'user:edit' - ]; - } return apiService.getUserPermissions(); } @@ -407,11 +313,6 @@ class UserService { * @returns 是否在线 */ async checkUserOnline(): Promise { - if (isMockMode) { - // 模拟用户在线 - console.log('[MOCK] 检查用户在线状态'); - return true; - } return apiService.checkUserOnline(); } @@ -425,25 +326,6 @@ class UserService { expiresAt: number; permissions: string[]; }> { - if (isMockMode) { - // 模拟会话信息 - console.log('[MOCK] 获取用户会话信息'); - return { - userId: 1, - sessionId: 'mock-session-' + Date.now(), - expiresAt: Date.now() + 86400000, // 24小时后过期 - permissions: [ - 'order:view', - 'order:edit', - 'delivery:view', - 'delivery:edit', - 'warehouse:view', - 'warehouse:edit', - 'user:view', - 'user:edit' - ] - }; - } return apiService.getSessionInfo(); } @@ -453,14 +335,6 @@ class UserService { * @returns 新令牌及过期时间 */ async refreshToken(oldToken: string): Promise<{ token: string; expiresAt: number }> { - if (isMockMode) { - // 模拟令牌刷新 - console.log('[MOCK] 刷新用户令牌'); - return { - token: 'mock-refreshed-token-' + Date.now(), - expiresAt: Date.now() + 86400000 // 24小时后过期 - }; - } return apiService.refreshToken(oldToken); } @@ -470,11 +344,6 @@ class UserService { * @returns 是否拥有该权限 */ async verifyPermission(permission: string): Promise { - if (isMockMode) { - // 模拟权限验证(管理员拥有所有权限) - console.log('[MOCK] 验证用户权限:', permission); - return true; - } return apiService.verifyPermission(permission); } } diff --git a/miniprogram/services/warehouseService.ts b/miniprogram/services/warehouseService.ts index 518f5c0..1336f02 100644 --- a/miniprogram/services/warehouseService.ts +++ b/miniprogram/services/warehouseService.ts @@ -1,156 +1,19 @@ import { WarehouseInfo } from '../types'; import apiService from './apiService'; -import { isMockMode } from './apiService'; /** * 仓库服务类 * 封装了所有仓库相关的操作 */ class WarehouseService { - // 模拟仓库数据 - private warehouses: WarehouseInfo[]; - /** - * 构造函数,初始化模拟仓库数据 + * 构造函数 */ constructor() { - this.warehouses = [ - { - id: 1, - name: '瓦尔塔蓄电池昆明总店', - address: '昆明市五华区二环西路599号', - contact: '刘经理', - phone: '0871-65123456', - description: '瓦尔塔蓄电池云南总代理,提供全系列蓄电池产品', - status: 'open', - capacity: 2000, // 仓库容量(吨) - longitude: 102.705745, - latitude: 25.055281 - }, - { - id: 2, - name: '瓦尔塔蓄电池盘龙区分店', - address: '昆明市盘龙区北京路1188号', - contact: '张经理', - phone: '0871-65678901', - description: '专业销售瓦尔塔汽车蓄电池,提供安装和售后服务', - status: 'open', - capacity: 800, // 仓库容量(吨) - longitude: 102.728421, - latitude: 25.042498 - }, - { - id: 3, - name: '瓦尔塔蓄电池西山区分店', - address: '昆明市西山区滇池路1234号', - contact: '李经理', - phone: '0871-65234567', - description: '瓦尔塔蓄电池授权经销商,专业汽车电池解决方案', - status: 'open', - capacity: 600, // 仓库容量(吨) - longitude: 102.689190, - latitude: 25.028234 - }, - { - id: 4, - name: '瓦尔塔蓄电池官渡区分店', - address: '昆明市官渡区春城路456号', - contact: '王经理', - phone: '0871-65345678', - description: '瓦尔塔蓄电池专卖店,各类车型电池齐全', - status: 'open', - capacity: 700, // 仓库容量(吨) - longitude: 102.725745, - latitude: 25.025281 - }, - { - id: 5, - name: '瓦尔塔蓄电池呈贡区分店', - address: '昆明市呈贡区春融街789号', - contact: '赵经理', - phone: '0871-65456789', - description: '瓦尔塔蓄电池呈贡区授权经销商,提供专业安装服务', - status: 'open', - capacity: 500, // 仓库容量(吨) - longitude: 102.788421, - latitude: 24.902498 - }, - { - id: 6, - name: '瓦尔塔蓄电池五华区服务中心', - address: '昆明市五华区一二一大街200号', - contact: '陈经理', - phone: '0871-65567890', - description: '瓦尔塔蓄电池专业售后服务中心,提供电池检测和更换', - status: 'open', - capacity: 400, // 仓库容量(吨) - longitude: 102.722745, - latitude: 25.040281 - }, - { - id: 7, - name: '瓦尔塔蓄电池安宁市分店', - address: '昆明市安宁市金方路300号', - contact: '杨经理', - phone: '0871-65678902', - description: '瓦尔塔蓄电池安宁市授权经销商,各类车型电池销售', - status: 'open', - capacity: 300, // 仓库容量(吨) - longitude: 102.458421, - latitude: 24.902498 - }, - { - id: 8, - name: '瓦尔塔蓄电池晋宁区分店', - address: '昆明市晋宁区昆阳大街120号', - contact: '黄经理', - phone: '0871-65789012', - description: '瓦尔塔蓄电池晋宁区销售点,提供汽车和电动车电池', - status: 'open', - capacity: 250, // 仓库容量(吨) - longitude: 102.519190, - latitude: 24.688234 - }, - { - id: 9, - name: '瓦尔塔蓄电池宜良县分店', - address: '昆明市宜良县匡远镇汇东路88号', - contact: '吴经理', - phone: '0871-65890123', - description: '瓦尔塔蓄电池宜良县授权经销商,专业汽车电池服务', - status: 'open', - capacity: 200, // 仓库容量(吨) - longitude: 103.115745, - latitude: 24.905281 - }, - { - id: 10, - name: '瓦尔塔蓄电池嵩明县分店', - address: '昆明市嵩明县嵩阳镇玉明路66号', - contact: '郑经理', - phone: '0871-65901234', - description: '瓦尔塔蓄电池嵩明县销售中心,各类蓄电池产品齐全', - status: 'open', - capacity: 180, // 仓库容量(吨) - longitude: 103.048421, - latitude: 25.362498 - } - ]; + // 真实API模式下不需要模拟数据 } - /** - * 获取所有仓库信息(同步版本) - */ - private getAllWarehousesSync(): WarehouseInfo[] { - return [...this.warehouses]; - } - /** - * 根据ID获取仓库信息(同步版本) - */ - private getWarehouseByIdSync(id: number): WarehouseInfo | undefined { - return this.warehouses.find(warehouse => warehouse.id === id); - } /** * 获取仓库状态的中文描述 @@ -168,9 +31,6 @@ class WarehouseService { * 获取所有仓库信息 */ async getWarehouses(): Promise { - if (isMockMode) { - return this.getAllWarehousesSync(); - } return apiService.getWarehouses(); } @@ -178,9 +38,6 @@ class WarehouseService { * 根据ID获取仓库信息 */ async getWarehouseById(id: number): Promise { - if (isMockMode) { - return this.getWarehouseByIdSync(id) || null; - } return apiService.getWarehouseById(id); } diff --git a/miniprogram/types/index.ts b/miniprogram/types/index.ts index c012c22..ba469a0 100644 --- a/miniprogram/types/index.ts +++ b/miniprogram/types/index.ts @@ -26,10 +26,13 @@ export interface SearchResult { phone: string; } +// 导入角色枚举 +import { Role } from '../utils/roleUtils'; + // 用户信息接口 export interface UserInfo { id: number; // 用户ID - role: 'ADMIN' | 'DELIVERY_PERSON' | 'GUEST'; // 用户角色 + role: Role; // 用户角色 token?: string; // 认证token openid?: string; // 微信openid session_key?: string; // 微信会话密钥 @@ -42,7 +45,7 @@ export interface EmployeeInfo { id: number; // 员工ID name: string; // 员工姓名 phone: string; // 员工电话 - role?: 'ADMIN' | 'DELIVERY_PERSON' | 'GUEST'; // 员工角色 + role?: Role; // 员工角色 } // 地图标记点接口 @@ -143,4 +146,55 @@ export interface AMapRegeoResponse { }; }; status: string; +} + +// 页面组件接口定义 +export interface IndexPageComponent { + data: { + longitude: number; + latitude: number; + scale: number; + markers: Marker[]; + userInfo: UserInfo | null; + authStatus: { + hasWxCode: boolean; + userStatus: 'unknown' | 'registered' | 'unregistered' | 'signed_in' | 'signed_out'; + }; + showUserPanel: boolean; + showOrderPanel: boolean; + currentOrder: any; + currentDeliveryPerson: any; + currentWarehouse: any; + currentPanelPosition: { x: number; y: number }; + polyline: any; + pendingOrders: any[]; + currentRoute: any; + showRoute: boolean; + routeDistance: number; + routeDuration: number; + showWarehouseModal: boolean; + showDeliveryPersonModal: boolean; + warehouseModalState: 'bottom' | 'full'; + deliveryPersonModalState: 'bottom' | 'full'; + showSignOutButton: boolean; + showSignInButton: boolean; + showRegisterButton: boolean; + showAuthButton: boolean; + }; + setData(data: Partial): void; + mainPageModule?: any; +} + + + +// 登录模块接口定义 +export interface LoginModule { + processUserInfo(userInfo: UserInfo): Promise; + updatePageAfterLogin(userInfo: UserInfo): void; + checkLoginStatus(): boolean; + logout(): Promise; + showManualLoginModal(): Promise; + handleLoginFailure(resolve: (value: boolean) => void): Promise; + showCloseAppOption(resolve: (value: boolean) => void): void; + handleSignIn(): Promise; } \ No newline at end of file diff --git a/miniprogram/utils/helpers.js b/miniprogram/utils/helpers.js deleted file mode 100644 index 7e9611c..0000000 --- a/miniprogram/utils/helpers.js +++ /dev/null @@ -1,166 +0,0 @@ -"use strict"; -// 工具函数文件 -Object.defineProperty(exports, "__esModule", { value: true }); -exports.formatCoordinate = formatCoordinate; -exports.formatSingleCoordinate = formatSingleCoordinate; -exports.calculateDistance = calculateDistance; -exports.formatDistance = formatDistance; -exports.showToast = showToast; -exports.showLoading = showLoading; -exports.hideLoading = hideLoading; - -exports.cacheUserAvatar = cacheUserAvatar; -exports.showConfirmDialog = showConfirmDialog; -/** - * 格式化坐标信息 - * @param longitude 经度 - * @param latitude 纬度 - * @param decimalPlaces 保留小数位数,默认6位 - * @returns 格式化后的坐标字符串 - */ -function formatCoordinate(longitude, latitude, decimalPlaces = 6) { - const formattedLongitude = longitude.toFixed(decimalPlaces); - const formattedLatitude = latitude.toFixed(decimalPlaces); - return `经度 ${formattedLongitude}, 纬度 ${formattedLatitude}`; -} -/** - * 格式化单个坐标值 - * @param coordinate 坐标值 - * @param decimalPlaces 保留小数位数,默认6位 - * @returns 格式化后的坐标字符串 - */ -function formatSingleCoordinate(coordinate, decimalPlaces = 6) { - return coordinate.toFixed(decimalPlaces); -} -/** - * 计算两点之间的距离(简单的平面距离,非球面距离) - * @param lng1 第一个点的经度 - * @param lat1 第一个点的纬度 - * @param lng2 第二个点的经度 - * @param lat2 第二个点的纬度 - * @returns 两点之间的距离(单位:米) - */ -function calculateDistance(lng1, lat1, lng2, lat2) { - // 地球半径(单位:米) - const EARTH_RADIUS = 6378137; - // 将角度转换为弧度 - const radLat1 = (lat1 * Math.PI) / 180.0; - const radLat2 = (lat2 * Math.PI) / 180.0; - const a = radLat1 - radLat2; - const b = (lng1 * Math.PI) / 180.0 - (lng2 * Math.PI) / 180.0; - // 应用haversine公式计算球面距离 - let s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + - Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); - s = s * EARTH_RADIUS; - s = Math.round(s * 10000) / 10000; // 保留4位小数 - return s; -} -/** - * 格式化距离(米转换为千米) - * @param distance 距离(单位:米) - * @returns 格式化后的距离字符串 - */ -function formatDistance(distance) { - if (distance < 1000) { - return `${Math.round(distance)} 米`; - } - else { - return `${(distance / 1000).toFixed(1)} 千米`; - } -} -/** - * 显示消息提示 - * @param title 提示信息 - * @param icon 图标类型,默认'none' - * @param duration 显示时长,默认2000毫秒 - */ -function showToast(title, icon = 'none', duration = 2000) { - wx.showToast({ - title, - icon, - duration - }); -} -/** - * 显示加载提示 - * @param title 提示信息,默认'加载中...' - */ -function showLoading(title = '加载中...') { - wx.showLoading({ - title - }); -} -/** - * 隐藏加载提示 - */ -function hideLoading() { - wx.hideLoading(); -} - -/** - * 缓存用户头像图片 - * @param avatarUrl 头像URL地址 - * @returns Promise 返回缓存后的头像路径或原始URL - */ -async function cacheUserAvatar(avatarUrl) { - if (!avatarUrl) - return ''; - try { - // 检查是否已缓存 - const cachedPath = wx.getStorageSync(`avatar_${avatarUrl}`); - if (cachedPath) { - console.log('使用缓存的头像:', cachedPath); - return cachedPath; - } - // 下载头像 - const downloadRes = await new Promise((resolve) => { - wx.downloadFile({ - url: avatarUrl, - success: (res) => resolve(res), - fail: (err) => { - console.error('下载头像失败:', err); - resolve({ statusCode: 0 }); // 返回失败状态 - } - }); - }); - if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) { - // 缓存头像路径 - wx.setStorageSync(`avatar_${avatarUrl}`, downloadRes.tempFilePath); - console.log('头像下载并缓存成功:', downloadRes.tempFilePath); - return downloadRes.tempFilePath; - } - else { - console.warn('头像下载失败,使用原始URL:', avatarUrl); - return avatarUrl; - } - } - catch (error) { - console.error('缓存头像过程中发生错误:', error); - return avatarUrl; - } -} -/** - * 显示确认对话框 - * @param title 标题 - * @param content 内容 - * @returns Promise 用户点击确定返回true,点击取消返回false - */ -function showConfirmDialog(title, content) { - return new Promise((resolve) => { - wx.showModal({ - title, - content, - success: (res) => { - if (res.confirm) { - resolve(true); - } - else if (res.cancel) { - resolve(false); - } - }, - fail: () => { - resolve(false); - } - }); - }); -} diff --git a/miniprogram/utils/roleUtils.ts b/miniprogram/utils/roleUtils.ts new file mode 100644 index 0000000..604839d --- /dev/null +++ b/miniprogram/utils/roleUtils.ts @@ -0,0 +1,85 @@ +/** + * 角色工具类 - 处理角色枚举的转换和验证 + */ + +/** + * 角色枚举定义 + * 服务器可能返回数字角色值,前端使用字符串枚举 + */ +export enum Role { + DELIVERY_PERSON = 'DELIVERY_PERSON', // 配送员 + ADMIN = 'ADMIN' // 管理员 +} + +/** + * 角色值映射 + * 服务器返回的数字角色值到前端字符串枚举的映射 + */ +export const RoleMapping = { + 0: Role.DELIVERY_PERSON, // 0对应配送员 + 1: Role.ADMIN // 1对应管理员 +} as const; + +/** + * 将服务器返回的角色值转换为前端枚举 + * @param roleValue 服务器返回的角色值(可能是数字或字符串) + * @returns 标准化的角色枚举值 + */ +export function normalizeRole(roleValue: string | number): Role { + if (typeof roleValue === 'number') { + // 如果是数字,使用映射表转换 + return RoleMapping[roleValue as keyof typeof RoleMapping] || Role.DELIVERY_PERSON; + } + + // 如果是字符串,直接转换为大写进行比较 + const normalizedRole = roleValue.toUpperCase(); + + // 检查是否是有效的角色值 + if (normalizedRole === Role.ADMIN) { + return Role.ADMIN; + } + + // 默认返回配送员 + return Role.DELIVERY_PERSON; +} + +/** + * 获取角色显示文本 + * @param role 角色枚举值 + * @returns 对应的中文显示文本 + */ +export function getRoleText(role: Role): string { + switch (role) { + case Role.ADMIN: + return '管理员'; + case Role.DELIVERY_PERSON: + return '配送员'; + default: + return '未知角色'; + } +} + +/** + * 验证角色值是否有效 + * @param roleValue 角色值 + * @returns 是否有效 + */ +export function isValidRole(roleValue: string | number): boolean { + try { + const normalized = normalizeRole(roleValue); + return normalized === Role.ADMIN || normalized === Role.DELIVERY_PERSON; + } catch { + return false; + } +} + +/** + * 获取角色选项列表(用于下拉选择器) + * @returns 角色选项数组 + */ +export function getRoleOptions(): Array<{ value: Role; label: string }> { + return [ + { value: Role.DELIVERY_PERSON, label: '配送员' }, + { value: Role.ADMIN, label: '管理员' } + ]; +} \ No newline at end of file diff --git a/miniprogram/utils/util.js b/miniprogram/utils/util.js deleted file mode 100644 index 131ded6..0000000 --- a/miniprogram/utils/util.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.formatTime = void 0; -const formatTime = (date) => { - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - const hour = date.getHours(); - const minute = date.getMinutes(); - const second = date.getSeconds(); - return ([year, month, day].map(formatNumber).join('/') + - ' ' + - [hour, minute, second].map(formatNumber).join(':')); -}; -exports.formatTime = formatTime; -const formatNumber = (n) => { - const s = n.toString(); - return s[1] ? s : '0' + s; -}; diff --git a/tsconfig.json b/tsconfig.json index ade784e..1056576 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "lib": ["ES2020"], "typeRoots": [ "./typings" - ] + ], + "outDir": "./dist" }, "include": [ "./**/*.ts"