feat(M08-B): 接入保洁任务端基础流程
This commit is contained in:
@@ -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())}`
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "保洁任务"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user