This commit is contained in:
2026-08-27 13:50:44 +08:00
parent 35d0595d85
commit c704032d66
12 changed files with 128 additions and 84 deletions
+4 -5
View File
@@ -8,8 +8,8 @@
<button v-for="operation in group.operations" :key="operation.name" class="home-operation" :class="{ 'home-operation-primary': operation.available, 'home-operation-disabled': !operation.available }" :disabled="!operation.available" @tap="openOperation(operation)">
<view class="home-operation-icon" aria-hidden="true"><uni-icons :type="operation.iconType" :color="operation.available ? '#135ee8' : '#9aa6b2'" size="28" /></view>
<text class="home-operation-name">{{ operation.name }}</text>
<text v-if="operation.available" class="home-operation-pending">{{ overview.pendingBoxCount }} 箱待拣</text>
<text v-else class="home-operation-unavailable">未开放</text>
<text v-if="operation.available" class="home-operation-meta home-operation-pending">开始作业</text>
<text v-else class="home-operation-meta home-operation-unavailable">未开放</text>
</button>
</view>
</view>
@@ -21,7 +21,6 @@
import PortalHeader from '@/components/PortalHeader.vue'
import { MODULES } from '@/data/demo-data.js'
import { session, requireSession } from '@/stores/session.js'
import { getPickingOverview } from '@/stores/picking.js'
const ICON_TYPES = {
'到货收货': 'download', '商品上架': 'upload', '入库质检': 'checkbox',
@@ -39,8 +38,8 @@
export default {
components: { PortalHeader },
data() { return { user: session.currentUser, groups: HOME_GROUPS, overview: getPickingOverview(), menuOpen: false } },
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.overview = getPickingOverview() },
data() { return { user: session.currentUser, groups: HOME_GROUPS, menuOpen: false } },
onShow() { if (!requireSession()) return; this.user = session.currentUser },
onBackPress() {
if (!this.menuOpen) return false
this.$refs.portalHeader.closeMenu()
+1 -1
View File
@@ -20,7 +20,7 @@
import { loadLoginPreference, saveLoginPreference, session, restoreSession, signInWithTokens } from '@/stores/session.js'
import { requireOnline } from '@/utils/network.js'
export default {
data() { return { form: { userId: 'platform_admin', password: 'ChangeMe123!' }, rememberAccount: true, error: '', submitting: false } },
data() { return { form: { userId: '', password: '' }, rememberAccount: true, error: '', submitting: false } },
async onLoad() {
const preference = loadLoginPreference()
this.rememberAccount = preference.rememberAccount
+16 -15
View File
@@ -11,10 +11,10 @@
</view>
</view>
<view class="work-content" :class="{ 'evidence-active': activeStage === 'evidence' && loadingStageReady }">
<view class="work-content" :class="{ 'evidence-active': activeStage === 'evidence' }">
<view class="box-select-wrap">
<button class="box-select-btn" @tap="toggleBoxDropdown">
<text>集装箱号 {{ currentBox.currentContainerNo || '同步中' }} · 柜次号 {{ currentBox.cabinetNo || '同步中' }} · 订单 {{ currentBox.orderNo }}</text>
<text class="box-select-summary">集装箱号 {{ currentBox.currentContainerNo || '同步中' }} · 柜次号 {{ currentBox.cabinetNo || '同步中' }} · 订单 {{ currentBox.orderNo }}</text>
<text class="box-chevron" :class="{ open: boxDropdownOpen }">▼</text>
</button>
<view v-if="boxDropdownOpen" class="box-dropdown">
@@ -33,10 +33,8 @@
</view>
<view class="stage-switch" role="tablist" aria-label="作业阶段">
<button class="stage-tab" :class="{ active: activeStage === 'picking' }" role="tab" :aria-selected="activeStage === 'picking'" @tap="selectStage('picking')">拣货作业</button>
<button class="stage-tab" :class="{ active: activeStage === 'evidence' }" :disabled="!loadingStageReady" role="tab" :aria-selected="activeStage === 'evidence'" @tap="selectStage('evidence')">装柜取证</button>
<button class="stage-tab" :class="{ active: activeStage === 'evidence' }" role="tab" :aria-selected="activeStage === 'evidence'" @tap="selectStage('evidence')">装柜取证</button>
</view>
<view v-if="!loadingStageReady" class="evidence-stage-hint" role="status">请先处理并提交全部待拣明细;任务增量更新后,已拍证据会按当前柜次保留。</view>
<view v-if="activeStage === 'picking'" class="picking-stage">
<view class="scan-section">
<view class="scan-row">
@@ -124,7 +122,7 @@
</view>
</view>
<view v-if="activeStage === 'evidence' && loadingStageReady" class="loading-evidence-section">
<view v-if="activeStage === 'evidence'" class="loading-evidence-section">
<view class="section-heading-row">
<view><text class="section-title">装柜证据</text><text class="section-subtitle">空仓、半仓、满仓、锁仓及带锁码照</text></view>
<text class="sync-state" :class="remoteSyncState.tone">{{ remoteSyncState.label }}</text>
@@ -624,10 +622,6 @@
}
},
selectStage(stage) {
if (stage === 'evidence' && !this.loadingStageReady) {
this.showToast('请先完成新增待拣明细', 'warning')
return
}
this.activeStage = stage
},
debugEvidence(event, payload = {}) {
@@ -1160,7 +1154,6 @@
const completedBoxes = { ...this.completedBoxes }
delete completedBoxes[this.currentBoxIndex]
this.completedBoxes = completedBoxes
this.activeStage = 'picking'
this.completionSummary = { picked: 0, partial: 0, skipped: 0, autoSkipped: 0 }
}
const addedItemIds = this.currentBox.items.map((item) => String(item.id)).filter((id) => !previousItemIds.has(id))
@@ -1255,8 +1248,7 @@
const next = { evidenceType: slot.type, fileName: path.split('/').pop() || path, localPath: path, uploadStatus: 'PENDING' }
this.evidenceFiles = { ...this.evidenceFiles, [slot.type]: [...(this.evidenceFiles[slot.type] || []), next] }
this.rememberEvidenceDraft(identity)
if (this.loadingStageReady) this.queueEvidenceUpload(slot.type, path)
else this.showToast('照片已暂存,装柜状态同步后自动上传', 'success')
this.queueEvidenceUpload(slot.type, path)
}, fail: (error) => {
this.cameraPicking = false
if (!this.isEvidenceContextActive(contextId, identity)) return
@@ -1265,7 +1257,6 @@
} })
},
async queueEvidenceUpload(type, path) {
if (!this.loadingStageReady) return
if (!this.evidenceIdentity?.inboundId || !isRemoteReady(this.evidenceIdentity.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
const contextId = this.evidenceContextId
const identity = { ...this.evidenceIdentity }
@@ -1285,6 +1276,11 @@
this.showToast('带锁码照已上传,请输入封签号并点击保存封签', 'success')
return
}
if (!this.loadingStageReady) {
this.rememberEvidenceDraft(identity)
this.showToast('照片已上传,拣货完成后自动保存', 'success')
return
}
await this.saveEvidence(type, null, path)
},
async flushPendingEvidence() {
@@ -1298,7 +1294,7 @@
}
},
async retryEvidence(type) {
if (!this.requireEvidenceStage()) return
if (!this.requireEvidenceCapture()) return
const item = this.evidenceQueueEntry(type)
const contextId = this.evidenceContextId
const identity = { ...this.evidenceIdentity }
@@ -1316,6 +1312,11 @@
this.showToast('带锁码照已上传,请输入封签号并点击保存封签', 'success')
return
}
if (!this.loadingStageReady) {
this.rememberEvidenceDraft(identity)
this.showToast('照片已上传,拣货完成后自动保存', 'success')
return
}
await this.saveEvidence(type)
},
async saveEvidence(type, operationId = null, localPath = null) {
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<view v-if="user" class="page-shell portal-page"><PortalHeader title="个人中心" :user="user" :show-profile="false" /><view class="portal-content profile-content"><view class="profile-card"><view class="profile-avatar">{{ user.name.slice(0, 1) }}</view><view><text class="profile-name">{{ user.name }}</text><text class="profile-role">{{ user.role }} · {{ user.id }}</text></view></view><view class="profile-list"><view class="profile-row"><text>作业仓库</text><text class="profile-value">{{ user.warehouse }}</text></view><view class="profile-row"><text>当前班次</text><text class="profile-value">08:00–17:00</text></view><view class="profile-row"><text>应用版本</text><text class="profile-value">{{ appVersion }}</text></view></view><button class="profile-action" @tap="checkUpdate">检查更新 <text>›</text></button><button class="profile-action" @tap="showSettings">系统设置 <text>›</text></button><button class="logout-action" @tap="openLogout">退出登录</button></view><uni-popup ref="logoutPopup" type="center" :is-mask-click="true" @change="handleLogoutChange"><view class="logout-dialog-card" role="dialog" aria-modal="true" aria-label="确认退出登录"><PopupHeader title="确认退出登录?" tone="danger" @close="closeLogout" /><text class="logout-dialog-content">退出后会清空本次会话中的箱号草稿、扫码记录与作业进度。</text><view class="logout-dialog-actions"><button class="logout-dialog-cancel" @tap="closeLogout">取消</button><button class="logout-dialog-confirm" @tap="logout">确认退出</button></view></view></uni-popup><view v-if="toast" class="app-toast warning" role="status">设置功能将在后续版本开放</view></view>
<view v-if="user" class="page-shell portal-page"><PortalHeader title="个人中心" :user="user" :show-profile="false" /><view class="portal-content profile-content"><view class="profile-card"><view class="profile-avatar">{{ user.name.slice(0, 1) }}</view><view><text class="profile-name">{{ user.name }}</text><text class="profile-role">{{ user.role }} · {{ user.id }}</text></view></view><view class="profile-list"><view class="profile-row"><text>作业仓库</text><text class="profile-value">{{ user.warehouse }}</text></view><view class="profile-row"><text>当前班次</text><text class="profile-value">08:00–17:00</text></view><view class="profile-row"><text>应用版本</text><text class="profile-value">{{ appVersion }}</text></view><button class="profile-row profile-row-action" @tap="checkUpdate"><text>检查更新</text><text class="profile-row-arrow">›</text></button><button class="profile-row profile-row-action" @tap="showSettings"><text>系统设置</text><text class="profile-row-arrow">›</text></button></view><button class="logout-action" @tap="openLogout">退出登录</button></view><uni-popup ref="logoutPopup" type="center" :is-mask-click="true" @change="handleLogoutChange"><view class="logout-dialog-card" role="dialog" aria-modal="true" aria-label="确认退出登录"><PopupHeader title="确认退出登录?" tone="danger" @close="closeLogout" /><text class="logout-dialog-content">退出后会清空本次会话中的箱号草稿、扫码记录与作业进度。</text><view class="logout-dialog-actions"><button class="logout-dialog-cancel" @tap="closeLogout">取消</button><button class="logout-dialog-confirm" @tap="logout">确认退出</button></view></view></uni-popup><view v-if="toast" class="app-toast warning" role="status">设置功能将在后续版本开放</view></view>
</template>
<script>
import { logout as logoutRequest } from '@/api/auth.js'
+7 -18
View File
@@ -5,14 +5,13 @@
<view class="task-toolbar">
<view class="task-heading">
<text class="task-title">待处理任务</text>
<text class="task-summary">{{ loading ? '正在同步' : `接口返回 ${rawTaskCount} 个,可处理 ${tasks.length} 个` }}</text>
<text class="task-environment">{{ apiEnvironment }}</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-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)},集装箱号 ${task.currentContainerNo}`" @tap="enterTask(task)">
@@ -20,9 +19,9 @@
<view class="task-container-info"><text class="task-label">集装箱号</text><text class="task-code">{{ task.currentContainerNo }}</text></view>
<text class="task-status" :class="{ progress: task.workflowStatus === 'IN_PROGRESS', evidence: task.workflowStatus === 'AWAITING_REVIEW' }">{{ statusLabel(task.workflowStatus) }}</text>
</view>
<view class="task-identifiers"><text class="task-cabinet">柜次号 {{ task.cabinetNo }}</text><text class="task-order">{{ task.displayNo || '未命名订单' }}</text></view>
<view class="task-identifiers"><text class="task-cabinet">柜次号 {{ task.cabinetNo }}</text><text v-if="task.displayNo" class="task-order">{{ task.displayNo }}</text></view>
<view class="task-card-foot">
<view class="task-meta-group"><text class="task-warehouse">{{ task.warehouseName || (task.warehouseId ? `仓库 ${task.warehouseId}` : '缺少仓库信息') }}</text><text class="task-pending">待处理 {{ task.pendingItemCount ?? task.itemCount ?? 0 }} 项</text></view>
<view class="task-meta-group"><text v-if="task.warehouseName" class="task-warehouse">{{ 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>
@@ -41,14 +40,7 @@
export default {
components: { PortalHeader },
data() { return { user: session.currentUser, tasks: [], rawTaskCount: 0, loading: false, error: '', notice: '', online: true, toast: { visible: false, message: '', type: '' } } },
computed: {
apiEnvironment() {
const apiBase = String(session.apiBase || '')
const host = apiBase.match(/^https?:\/\/([^/]+)/i)?.[1]
return `当前接口:${host || apiBase || '未配置'} · 账号:${session.currentUser?.name || session.currentUser?.id || '未知'}`
}
},
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) },
@@ -69,15 +61,13 @@
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)) && (task.workflowStatus === 'AWAITING_REVIEW' || (task.assignmentVersion !== null && task.assignmentVersion !== undefined && Number.isInteger(Number(task.assignmentVersion)))) && ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW'].includes(task.workflowStatus)) },
async loadTasks() {
if (!isOnline()) { this.error = '当前网络不可用,请联网后刷新'; return }
this.loading = true; this.error = ''; this.notice = ''; this.rawTaskCount = 0
this.loading = true; this.error = ''; this.notice = ''
try {
const result = await listLoadingOrders({ page: 1, size: 50 })
const records = result.records || []
this.rawTaskCount = records.length
records.filter((task) => this.isMergedTask(task)).forEach((task) => this.clearMergedTaskDraft(task))
this.tasks = records.filter((task) => this.isTaskReady(task))
try { console.info('[PDA] pending tasks', { apiBase: session.apiBase, operatorId: session.currentUser?.id, received: records.length, visible: this.tasks.length }) } catch (_) {}
if (records.length > this.tasks.length) this.notice = '部分任务尚未完成服务端就绪校验,本机已隐藏,请稍后刷新'
if (records.length > this.tasks.length) this.notice = '部分任务暂不可作业,请稍后刷新'
} catch (error) {
this.error = error.network ? '当前网络不可用,请联网后刷新' : (error.message || '任务加载失败')
} finally { this.loading = false }
@@ -97,7 +87,6 @@
.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-environment { margin-top: 1px; color: #98a2b3; font-size: 10px; line-height: 15px; }
.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; }