const { request, cents, ensureLogin } = require('../../utils/api.js') const cleaningStatusLabels = { WAITING: '待接单', CLAIMED: '已接单', STARTED: '清洁中', SUBMITTED: '待验收', COMPLETED: '已完成', REJECTED: '已驳回', EXEMPT: '免清洁', SETTLED: '已结算', CANCELLED: '已取消', } const deviceStatusLabels = { ONLINE: '在线', OFFLINE: '离线', FAULT: '故障', } const deviceTypeLabels = { CONTROL_BOX: '控制箱', SUB_LOCK: '子锁', SMART_SOCKET: '智能插座', } Page({ data: { storeId: '', storeName: '', loading: false, errorMessage: '', canReadCleaning: false, canWriteCleaning: false, canReadDevices: false, canWriteDevices: false, cleaningSummary: { taskTotal: 0, pendingReview: 0, active: 0, rejected: 0, completed: 0, }, cleaningTasks: [], devices: [], openAlerts: [], maintenance: [], busyTaskId: '', busyDeviceKey: '', }, async onLoad(options) { this.setData({ storeId: options.storeId || '', storeName: options.storeName || '', }) await this.loadOperations() }, async onPullDownRefresh() { await this.loadOperations() wx.stopPullDownRefresh() }, async loadOperations() { if (!this.data.storeId) { this.setData({ errorMessage: '缺少门店信息' }) return } this.setData({ loading: true, errorMessage: '' }) try { await ensureLogin() const me = await request('/auth/me') const access = me.data?.access || { roles: [], capabilities: [] } const isPlatform = access.roles.includes('PLATFORM_ADMIN') const canManageTenant = access.capabilities.includes('tenant.manage') || isPlatform const canReadCleaning = access.capabilities.includes('cleaning.task.read') || canManageTenant const canWriteCleaning = access.capabilities.includes('cleaning.task.write') || canManageTenant const canReadDevices = access.capabilities.some((code) => ['device.read', 'device.write'].includes(code)) || canManageTenant const canWriteDevices = access.capabilities.includes('device.write') || canManageTenant this.setData({ canReadCleaning, canWriteCleaning, canReadDevices, canWriteDevices }) await Promise.all([ canReadCleaning ? this.loadCleaning() : Promise.resolve(), canReadDevices ? this.loadDevices() : Promise.resolve(), ]) } catch (error) { this.setData({ errorMessage: error.message || '运营数据加载失败' }) } finally { this.setData({ loading: false }) } }, async loadCleaning() { const storeId = encodeURIComponent(this.data.storeId) const [tasksResponse, statsResponse] = await Promise.all([ request(`/management/cleaning/tasks?page=1&pageSize=50&storeId=${storeId}`), request(`/management/cleaning/statistics?storeId=${storeId}`), ]) const cleaningTasks = (tasksResponse.data?.items || []).map((item) => ({ ...item, statusText: cleaningStatusLabels[item.status] || item.status, rewardText: cents(item.rewardCents), roomText: item.roomName ? `${item.roomName} · ${item.roomNo || ''}` : `房间 ${item.roomId}`, canReview: item.status === 'SUBMITTED', canExempt: ['WAITING', 'CLAIMED', 'STARTED', 'SUBMITTED', 'REJECTED'].includes(item.status), })) this.setData({ cleaningTasks, cleaningSummary: statsResponse.data?.summary || this.data.cleaningSummary, }) }, async loadDevices() { const storeId = encodeURIComponent(this.data.storeId) const response = await request(`/management/device-topology?storeId=${storeId}`) const topology = response.data || {} const devices = (topology.assets || []).map((item) => ({ ...item, typeText: deviceTypeLabels[item.deviceType] || item.deviceType, statusText: deviceStatusLabels[item.status] || item.status, roomText: item.roomId ? `房间 ${item.roomId}` : '门店公共设备', lastSeenText: this.formatTime(item.lastSeenAt || item.lastHeartbeatAt), })) this.setData({ devices, openAlerts: topology.openAlerts || [], maintenance: (topology.maintenance || []).filter((item) => item.status === 'OPEN'), }) }, reviewTask(event) { if (!this.data.canWriteCleaning) return const taskId = event.currentTarget.dataset.taskId const action = event.currentTarget.dataset.action if (action === 'reject') { wx.showModal({ title: '驳回保洁任务', editable: true, placeholderText: '请输入补做原因', success: ({ confirm, content }) => { const reason = String(content || '').trim() if (confirm && reason) this.submitCleaningAction(taskId, 'reject', { reason }, '任务已驳回') }, }) return } const labels = action === 'complete' ? { title: '验收通过', content: '确认照片与现场清洁结果已达标吗?', toast: '验收已完成' } : { title: '设为免清洁', content: '确认该房间本次无需清洁吗?', toast: '已设为免清洁' } wx.showModal({ title: labels.title, content: labels.content, success: ({ confirm }) => { if (confirm) this.submitCleaningAction(taskId, action, { note: `管理员小程序:${labels.title}` }, labels.toast) }, }) }, async submitCleaningAction(taskId, action, data, toast) { if (!taskId || this.data.busyTaskId) return this.setData({ busyTaskId: taskId, errorMessage: '' }) try { await request(`/management/cleaning/tasks/${encodeURIComponent(taskId)}/${action}`, { method: 'POST', data, }) await this.loadCleaning() wx.showToast({ title: toast, icon: 'success' }) } catch (error) { this.setData({ errorMessage: error.message || '保洁任务处理失败' }) } finally { this.setData({ busyTaskId: '' }) } }, controlDevice(event) { if (!this.data.canWriteDevices) return const { roomId, action } = event.currentTarget.dataset if (!roomId || !action) return const actions = { 'door-open': { title: '临时开门', endpoint: '/management/device-control/door', data: { order: 'open', holdopen: 0, delayTime: 4 }, }, 'power-on': { title: '全屋通电', endpoint: '/management/device-control/power', data: { slotall: 'on' }, }, 'power-off': { title: '全屋断电', endpoint: '/management/device-control/power', data: { slotall: 'off' }, }, 'socket-on': { title: '插座通电', endpoint: '/management/device-control/socket/switch', data: { on: true, slotNum: 1 }, }, 'socket-off': { title: '插座断电', endpoint: '/management/device-control/socket/switch', data: { on: false, slotNum: 1 }, }, } const selected = actions[action] if (!selected) return wx.showModal({ title: selected.title, content: `确认对房间 ${roomId} 执行“${selected.title}”吗?`, success: ({ confirm }) => { if (confirm) this.submitDeviceControl(roomId, action, selected) }, }) }, async submitDeviceControl(roomId, action, selected) { const busyDeviceKey = `${roomId}:${action}` if (this.data.busyDeviceKey) return this.setData({ busyDeviceKey, errorMessage: '' }) try { await request(selected.endpoint, { method: 'POST', data: { storeId: this.data.storeId, roomId, ...selected.data }, }) wx.showToast({ title: '指令已发送', icon: 'success' }) } catch (error) { this.setData({ errorMessage: error.message || '设备控制失败' }) } finally { this.setData({ busyDeviceKey: '' }) } }, formatTime(value) { if (!value) return '暂无心跳' const date = new Date(value) if (Number.isNaN(date.getTime())) return String(value) const pad = (part) => String(part).padStart(2, '0') return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}` }, })