Files
pda-uniapp/pages/tasks/tasks.vue
T
2026-09-16 17:49:33 +08:00

128 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page-shell task-page">
<PortalHeader title="拣货任务" :user="user" :fallback-url="'/pages/home/home'" />
<view class="task-content">
<view class="task-toolbar">
<view class="task-heading">
<text class="task-title">待处理任务</text>
<text class="task-summary">{{ loading ? '正在同步任务' : `共 ${tasks.length} 个待处理任务` }}</text>
</view>
<button class="task-refresh" :disabled="loading" @tap="loadTasks">{{ loading ? '刷新中' : '刷新' }}</button>
</view>
<view v-if="error" class="task-error" role="alert">{{ error }}</view>
<view v-if="loading" class="task-state">正在加载真实任务...</view>
<view v-else-if="!tasks.length && !error" class="task-state">暂无待处理的拣货任务</view>
<view v-if="notice" class="task-notice" role="status">{{ notice }}</view>
<view class="task-list">
<button v-for="task in tasks" :key="task.taskId" class="task-card" :disabled="!isTaskReady(task)" :aria-label="`${taskActionLabel(task)},${containerReference(task)}`" @tap="enterTask(task)">
<view class="task-card-head">
<view class="task-container-info"><text class="task-code">{{ containerReference(task) }}</text></view>
<text class="task-status" :class="{ progress: task.workflowStatus === 'IN_PROGRESS', evidence: task.workflowStatus === 'AWAITING_REVIEW', processing: task.workflowStatus === 'PROCESSING', failed: task.workflowStatus === 'COMPLETION_FAILED' }">{{ statusLabel(task.workflowStatus) }}</text>
</view>
<view class="task-card-foot">
<view class="task-meta-group"><text class="task-warehouse">仓库 {{ displayBusinessValue(task.warehouseName, '未关联') }}</text><text class="task-pending">待处理 {{ task.pendingItemCount ?? task.itemCount ?? 0 }} 项</text></view>
<view class="task-enter"><text>{{ taskActionLabel(task) }}</text><text class="task-enter-arrow">›</text></view>
</view>
</button>
</view>
</view>
<view v-if="toast.visible" class="app-toast" :class="toast.type" role="status">{{ toast.message }}</view>
</view>
</template>
<script>
import PortalHeader from '@/components/PortalHeader.vue'
import { listLoadingOrders } from '@/api/pda-loading.js'
import { session, requireSession } from '@/stores/session.js'
import { clearPickingDraft } from '@/stores/picking.js'
import { isOnline, watchNetwork } from '@/utils/network.js'
import { containerReference, displayBusinessValue, formatApiError } from '@/utils/user-facing.js'
export default {
components: { PortalHeader },
data() { return { user: session.currentUser, tasks: [], loading: false, error: '', notice: '', online: true, toast: { visible: false, message: '', type: '' } } },
onLoad() { if (!requireSession()) return; this._stopNetwork = watchNetwork((value) => { this.online = value }) },
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.loadTasks() },
onUnload() { if (this._stopNetwork) this._stopNetwork(); clearTimeout(this._toastTimer) },
methods: {
displayBusinessValue,
containerReference,
statusLabel(status) {
if (status === 'PROCESSING') return '完成处理中'
if (status === 'COMPLETION_FAILED') return '完成失败'
if (status === 'AWAITING_REVIEW') return '待上传证据/确认装柜'
return status === 'IN_PROGRESS' ? '拣货中' : '待领取'
},
taskActionLabel(task) {
if (!task.currentContainerNo) return '待平台补录集装箱号'
if (!this.isTaskReady(task)) return '任务暂不可进入'
if (task.workflowStatus === 'COMPLETION_FAILED') return '重试完成'
return task.workflowStatus === 'AWAITING_REVIEW' ? '继续装柜取证' : '进入并开始拣货'
},
isMergedTask(task) { return task?.recordStatus === 'MERGED' || Boolean(task?.mergedIntoContainerBookingId) },
clearMergedTaskDraft(task) {
const operatorId = session.currentUser?.id || session.currentUser?.userId || session.currentUser?.accountId
clearPickingDraft({ operatorId, taskId: task.taskId, loadingPlanId: task.loadingPlanId, containerBookingId: task.containerBookingId, warehouseId: task.warehouseId })
},
isTaskReady(task) { return Boolean(task.recordStatus === 'ACTIVE' && !task.mergedIntoContainerBookingId && task.taskId && task.loadingPlanId && task.containerBookingId && task.cabinetNo && task.currentContainerNo && task.warehouseId && task.version !== null && task.version !== undefined && Number.isInteger(Number(task.version)) && (['AWAITING_REVIEW', 'COMPLETION_FAILED'].includes(task.workflowStatus) || (task.assignmentVersion !== null && task.assignmentVersion !== undefined && Number.isInteger(Number(task.assignmentVersion)))) && ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'COMPLETION_FAILED'].includes(task.workflowStatus)) },
isTaskVisible(task) { return Boolean(task.recordStatus === 'ACTIVE' && !task.mergedIntoContainerBookingId && task.taskId && ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'PROCESSING', 'COMPLETION_FAILED'].includes(task.workflowStatus)) },
async loadTasks() {
if (!isOnline()) { this.error = '当前网络不可用,请联网后刷新'; return }
if (!this.user?.warehouseId) { this.error = '请返回首页选择作业仓库'; return }
this.loading = true; this.error = ''; this.notice = ''
try {
const result = await listLoadingOrders({ page: 1, size: 50, warehouseId: this.user.warehouseId })
const records = result.records || []
records.filter((task) => this.isMergedTask(task)).forEach((task) => this.clearMergedTaskDraft(task))
this.tasks = records.filter((task) => this.isTaskVisible(task))
if (records.length > this.tasks.length) this.notice = '部分任务暂不可作业,请稍后刷新'
} catch (error) {
this.error = error.network ? '当前网络不可用,请联网后刷新' : formatApiError(error, '任务加载失败')
} finally { this.loading = false }
},
enterTask(task) {
if (!task.currentContainerNo) { this.showToast('请先在平台预装详情补录真实集装箱号', 'error'); return }
if (!this.isTaskReady(task)) { this.showToast('任务尚未满足后端进入条件', 'error'); return }
uni.navigateTo({ url: `/pages/picking/picking?taskId=${encodeURIComponent(task.taskId)}&warehouseId=${encodeURIComponent(task.warehouseId)}` })
},
showToast(message, type) { clearTimeout(this._toastTimer); this.toast = { visible: true, message, type }; this._toastTimer = setTimeout(() => { this.toast.visible = false }, 2200) }
}
}
</script>
<style scoped>
.task-content { padding: 12px 12px 40px; }
.task-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.task-heading { display: flex; min-width: 0; flex-direction: column; }
.task-title { color: #12233f; font-size: 18px; font-weight: 700; line-height: 25px; }
.task-summary { margin-top: 2px; color: #667085; font-size: 11px; line-height: 16px; }
.task-refresh { flex-shrink: 0; margin: 0; min-width: 76px; height: 44px; border: 0; border-radius: 9px; background: #1a73e8; color: #fff; font-size: 14px; line-height: 44px; }
.task-refresh[disabled] { opacity: .6; }
.task-error, .task-state { padding: 20px 14px; border-radius: 10px; background: #fff; color: #667085; font-size: 13px; line-height: 20px; text-align: center; }
.task-error { color: #b42318; background: #fff1f0; }
.task-notice { padding: 10px 12px; margin-bottom: 10px; border-left: 3px solid #f79009; border-radius: 6px; background: #fffaeb; color: #7a2e0e; font-size: 12px; line-height: 18px; }
.task-list { display: flex; flex-direction: column; }
.task-card { box-sizing: border-box; width: 100%; margin: 0 0 12px; padding: 12px; border: 1px solid #dce6f1; border-radius: 10px; color: #12233f; text-align: left; background: #fff; box-shadow: 0 2px 8px rgba(23, 55, 93, .05); }
.task-card:last-child { margin-bottom: 0; }
.task-card:active { border-color: #8ab8ef; background: #f7faff; }
.task-card[disabled] { opacity: .62; }
.task-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.task-container-info { display: flex; min-width: 0; flex-direction: column; }
.task-code { margin-top: 1px; color: #12233f; font-size: 17px; font-weight: 700; line-height: 23px; overflow-wrap: anywhere; }
.task-status { flex-shrink: 0; padding: 4px 7px; border-radius: 99px; color: #52616f; font-size: 10px; line-height: 16px; background: #eef2f6; }
.task-status.progress { color: #155eef; background: #eff4ff; }
.task-status.evidence { color: #9a5b13; background: #fff4e5; }
.task-status.processing { color: #155eef; background: #eff4ff; }
.task-status.failed { color: #b42318; background: #fff1f0; }
.task-card-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 10px; padding-top: 9px; border-top: 1px solid #edf1f5; }
.task-meta-group { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
.task-warehouse { color: #667085; font-size: 10px; line-height: 15px; overflow-wrap: anywhere; }
.task-pending { color: #344054; font-size: 12px; font-weight: 600; line-height: 17px; }
.task-enter { display: flex; min-height: 40px; flex-shrink: 0; align-items: center; gap: 5px; padding-left: 10px; color: #155eef; font-size: 12px; font-weight: 600; }
.task-enter-arrow { font-size: 22px; font-weight: 400; line-height: 22px; }
@media (max-width: 340px) {
.task-content { padding-right: 10px; padding-left: 10px; }
.task-enter { max-width: 124px; text-align: right; }
}
</style>