243 lines
8.0 KiB
JavaScript
243 lines
8.0 KiB
JavaScript
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())}`
|
|
},
|
|
})
|