feat(M08-B): 接入保洁任务端基础流程

This commit is contained in:
Codex
2026-06-25 23:19:04 +08:00
parent 9f7c2e4be7
commit 2a4dcd0fd8
19 changed files with 1081 additions and 10 deletions
+108
View File
@@ -0,0 +1,108 @@
const { request, ensureLogin, cents } = require('../../utils/api.js')
Page({
data: {
loading: false,
errorMessage: '',
activeTab: 'hall',
hallTasks: [],
myTasks: [],
stats: { byStatus: {}, pendingSettlementText: cents(0) },
submitPhotoText: '',
},
onShow() {
this.refreshAll()
},
async refreshAll() {
this.setData({ loading: true, errorMessage: '' })
try {
await ensureLogin()
const [hall, mine, stats] = await Promise.all([
request('/cleaning/tasks/hall?page=1&pageSize=20'),
request('/cleaning/tasks/mine?page=1&pageSize=20'),
request('/cleaning/stats'),
])
this.setData({
hallTasks: (hall.data.items || []).map(formatTask),
myTasks: (mine.data.items || []).map(formatTask),
stats: formatStats(stats.data),
})
} catch (error) {
this.setData({ errorMessage: error.message || '保洁任务加载失败' })
} finally {
this.setData({ loading: false })
}
},
switchTab(event) {
const tab = event.currentTarget.dataset.tab
if (!tab || tab === this.data.activeTab) return
this.setData({ activeTab: tab })
},
async claimTask(event) {
await this.mutateTask(event.currentTarget.dataset.taskId, 'claim')
},
async startTask(event) {
await this.mutateTask(event.currentTarget.dataset.taskId, 'start')
},
onPhotoTextInput(event) {
this.setData({ submitPhotoText: event.detail.value })
},
async submitTask(event) {
const photoUrls = this.data.submitPhotoText
.split('\n')
.map((item) => item.trim())
.filter(Boolean)
.slice(0, 9)
await this.mutateTask(event.currentTarget.dataset.taskId, 'submit', { photoUrls })
this.setData({ submitPhotoText: '' })
},
async mutateTask(taskId, action, data = {}) {
if (!taskId) return
this.setData({ loading: true, errorMessage: '' })
try {
await ensureLogin()
await request(`/cleaning/tasks/${encodeURIComponent(taskId)}/${action}`, {
method: 'POST',
data,
})
await this.refreshAll()
} catch (error) {
this.setData({ errorMessage: error.message || '保洁任务处理失败' })
} finally {
this.setData({ loading: false })
}
},
})
function formatTask(task) {
return {
...task,
rewardText: cents(task.rewardCents),
timeText: formatDate(task.updatedAt || task.createdAt),
canClaim: task.status === 'WAITING',
canStart: task.status === 'CLAIMED',
canSubmit: task.status === 'STARTED',
}
}
function formatStats(stats) {
return {
byStatus: stats.byStatus || {},
pendingSettlementText: cents(stats.pendingSettlementCents),
}
}
function formatDate(value) {
if (!value) return ''
const date = new Date(value)
const pad = (input) => String(input).padStart(2, '0')
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
+3
View File
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "保洁任务"
}
+57
View File
@@ -0,0 +1,57 @@
<scroll-view class="scrollarea" scroll-y type="list">
<view class="page">
<view class="header">
<view>
<view class="title">保洁任务</view>
<view class="muted">待结算 {{stats.pendingSettlementText}}</view>
</view>
<button size="mini" loading="{{loading}}" bindtap="refreshAll">刷新</button>
</view>
<view class="tabs">
<button class="{{activeTab === 'hall' ? 'tab active' : 'tab'}}" data-tab="hall" bindtap="switchTab">大厅</button>
<button class="{{activeTab === 'mine' ? 'tab active' : 'tab'}}" data-tab="mine" bindtap="switchTab">我的</button>
</view>
<view class="stats">
<text>已抢 {{stats.byStatus.CLAIMED || 0}}</text>
<text>进行 {{stats.byStatus.STARTED || 0}}</text>
<text>待验 {{stats.byStatus.SUBMITTED || 0}}</text>
<text>完成 {{stats.byStatus.COMPLETED || 0}}</text>
</view>
<view wx:if="{{errorMessage}}" class="error">{{errorMessage}}</view>
<block wx:if="{{activeTab === 'hall'}}">
<view wx:if="{{!loading && hallTasks.length === 0}}" class="empty">暂无可接任务</view>
<view wx:for="{{hallTasks}}" wx:key="id" class="task-card">
<view class="task-title">{{item.storeName}} · {{item.roomName}}</view>
<view class="muted">任务号 {{item.taskNo}}</view>
<view class="muted">{{item.requirement || '常规保洁'}}</view>
<view class="row">
<text class="status">{{item.status}}</text>
<text class="amount">{{item.rewardText}}</text>
</view>
<button wx:if="{{item.canClaim}}" data-task-id="{{item.id}}" bindtap="claimTask">接单</button>
</view>
</block>
<block wx:if="{{activeTab === 'mine'}}">
<view wx:if="{{!loading && myTasks.length === 0}}" class="empty">暂无我的任务</view>
<view wx:for="{{myTasks}}" wx:key="id" class="task-card">
<view class="task-title">{{item.storeName}} · {{item.roomName}}</view>
<view class="muted">任务号 {{item.taskNo}}</view>
<view class="muted">{{item.timeText}}</view>
<view class="row">
<text class="status">{{item.status}}</text>
<text class="amount">{{item.rewardText}}</text>
</view>
<button wx:if="{{item.canStart}}" data-task-id="{{item.id}}" bindtap="startTask">开始</button>
<view wx:if="{{item.canSubmit}}" class="submit-box">
<textarea value="{{submitPhotoText}}" bindinput="onPhotoTextInput" placeholder="每行一个照片 URL" />
<button data-task-id="{{item.id}}" bindtap="submitTask">提交验收</button>
</view>
</view>
</block>
</view>
</scroll-view>
+106
View File
@@ -0,0 +1,106 @@
.scrollarea {
height: 100vh;
background: #f5f6f8;
}
.page {
padding: 32rpx;
}
.header,
.row,
.stats {
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
font-size: 40rpx;
font-weight: 600;
}
.muted {
margin-top: 8rpx;
color: #667085;
font-size: 26rpx;
}
.tabs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
margin: 28rpx 0 20rpx;
}
.tab {
border: 1rpx solid #d0d5dd;
background: #ffffff;
color: #344054;
}
.active {
border-color: #1f6feb;
background: #eaf2ff;
color: #1f6feb;
}
.stats {
padding: 20rpx 24rpx;
border-radius: 16rpx;
background: #ffffff;
color: #344054;
font-size: 24rpx;
}
.task-card {
margin-top: 20rpx;
padding: 28rpx;
border-radius: 16rpx;
background: #ffffff;
}
.task-title {
font-size: 32rpx;
font-weight: 600;
}
.row {
margin: 18rpx 0;
}
.status {
color: #1f6feb;
font-weight: 600;
}
.amount {
color: #b42318;
font-weight: 700;
}
.submit-box {
margin-top: 16rpx;
}
textarea {
box-sizing: border-box;
width: 100%;
min-height: 144rpx;
margin-bottom: 16rpx;
padding: 18rpx;
border: 1rpx solid #d0d5dd;
border-radius: 12rpx;
background: #f9fafb;
}
.error {
margin: 20rpx 0;
color: #c73535;
}
.empty {
padding: 80rpx 0;
color: #888888;
text-align: center;
}