Release/test #1

Merged
admin merged 102 commits from release/test into main 2026-09-17 11:53:23 +08:00
5 changed files with 91 additions and 37 deletions
Showing only changes of commit 37d399752d - Show all commits
+17 -8
View File
@@ -11,7 +11,7 @@
</view>
</view>
<view class="work-content">
<view class="work-content" :class="{ 'evidence-active': activeStage === 'evidence' && loadingStageReady }">
<view class="box-select-wrap">
<button class="box-select-btn" @tap="toggleBoxDropdown">
<text>集装箱号 {{ currentBox.currentContainerNo || '同步中' }} · 柜次号 {{ currentBox.cabinetNo || '同步中' }} · 订单 {{ currentBox.orderNo }}</text>
@@ -130,18 +130,27 @@
<text class="sync-state" :class="remoteSyncState.tone">{{ remoteSyncState.label }}</text>
</view>
<view class="evidence-grid">
<view v-for="slot in evidenceSlots" :key="slot.type" class="evidence-card" :class="{ filled: evidenceEntries(slot.type).length }">
<text class="evidence-label">{{ slot.label }}</text>
<view v-for="photo in evidenceEntries(slot.type)" :key="photo.id || photo.fileId || photo.localPath" class="evidence-file-row">
<image v-if="evidencePreviewSource(photo)" class="evidence-thumbnail" :src="evidencePreviewSource(photo)" mode="aspectFill" @tap="previewEvidencePhoto(photo, slot.type)" />
<view class="evidence-file-detail" @tap="previewEvidencePhoto(photo, slot.type)"><text class="evidence-file">{{ photo.fileName || '已记录' }}</text></view>
<button v-if="photo.id && photo.status !== 'VOID' && photo.status !== 'VOIDED'" class="evidence-retry-btn" @tap="voidEvidencePhoto(photo)">作废</button>
<view v-for="slot in evidenceSlots" :key="slot.type" class="evidence-card" :class="{ filled: evidenceEntries(slot.type).length }">
<view class="evidence-card-header">
<view class="evidence-card-title">
<text class="evidence-label">{{ slot.label }}</text>
<text class="evidence-count" :class="{ complete: evidenceEntries(slot.type).length }">{{ evidenceEntries(slot.type).length ? evidenceEntries(slot.type).length + ' 张' : '待拍摄' }}</text>
</view>
<button class="evidence-btn evidence-add-btn" @tap="chooseEvidence(slot)">{{ evidenceUploadLabel(slot.type) }}</button>
</view>
<view v-if="evidenceEntries(slot.type).length" class="evidence-photo-grid">
<view v-for="photo in evidenceEntries(slot.type)" :key="photo.id || photo.fileId || photo.localPath" class="evidence-photo-item">
<image v-if="evidencePreviewSource(photo)" class="evidence-thumbnail" :src="evidencePreviewSource(photo)" mode="aspectFill" @tap="previewEvidencePhoto(photo, slot.type)" />
<view v-else class="evidence-thumbnail evidence-thumbnail-placeholder" @tap="previewEvidencePhoto(photo, slot.type)">图片处理中</view>
<text class="evidence-file" @tap="previewEvidencePhoto(photo, slot.type)">{{ photo.fileName || '已记录' }}</text>
<button v-if="photo.id && photo.status !== 'VOID' && photo.status !== 'VOIDED'" class="evidence-void-btn" @tap="voidEvidencePhoto(photo)">作废</button>
</view>
</view>
<text v-else class="evidence-empty-hint">现场拍摄后自动上传,可继续追加</text>
<text v-if="evidenceQueueEntry(slot.type)" class="evidence-upload-state" :class="evidenceQueueEntry(slot.type).status.toLowerCase()">
{{ evidenceUploadState(slot.type) }}
</text>
<text v-else-if="hasPendingLocalEvidence(slot.type)" class="evidence-upload-state pending">本地暂存,待自动上传</text>
<button class="evidence-btn" @tap="chooseEvidence(slot)">{{ evidenceUploadLabel(slot.type) }}</button>
<button v-if="evidenceQueueEntry(slot.type)?.status === 'FAILED'" class="evidence-retry-btn" @tap="retryEvidence(slot.type)">重试上传</button>
</view>
</view>
+49 -21
View File
@@ -3,19 +3,28 @@
<PortalHeader title="拣货任务" :user="user" :fallback-url="'/pages/home/home'" />
<view class="task-content">
<view class="task-toolbar">
<text class="task-title">待处理任务</text>
<button class="task-refresh" :disabled="loading" @tap="loadTasks">刷新</button>
<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 v-for="task in tasks" :key="task.taskId" class="task-card">
<view class="task-card-head"><text class="task-code">集装箱号 {{ task.currentContainerNo }}</text><text class="task-status">{{ statusLabel(task.workflowStatus) }}</text></view>
<text class="task-order">柜次号 {{ task.cabinetNo }}</text>
<text class="task-order">{{ task.displayNo || '未命名订单' }}</text>
<text class="task-meta">{{ task.warehouseName || (task.warehouseId ? `仓库 ${task.warehouseId}` : '缺少仓库信息') }} · 待处理 {{ task.pendingItemCount ?? task.itemCount ?? 0 }} 项</text>
<button class="task-enter" :disabled="!isTaskReady(task)" @tap="enterTask(task)">{{ taskActionLabel(task) }}</button>
<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)">
<view class="task-card-head">
<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-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-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>
@@ -73,19 +82,38 @@
</script>
<style scoped>
.task-content { padding: 24px 16px 40px; }
.task-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.task-title { color: #12233f; font-size: 22px; font-weight: 700; }
.task-refresh { margin: 0; min-width: 92px; height: 44px; border: 0; border-radius: 10px; background: #1a73e8; color: #fff; font-size: 15px; }
.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: 24px 16px; border-radius: 12px; background: #fff; color: #667085; text-align: center; }
.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: 12px 14px; margin-bottom: 12px; border-left: 4px solid #f79009; background: #fffaeb; color: #7a2e0e; font-size: 14px; }
.task-card { padding: 16px; margin-bottom: 12px; border: 1px solid #e3eaf3; border-radius: 12px; background: #fff; box-shadow: 0 4px 16px rgba(23, 55, 93, .06); }
.task-card-head { display: flex; align-items: center; justify-content: space-between; }
.task-code { color: #12233f; font-weight: 700; }
.task-status { color: #16794a; font-size: 13px; }
.task-order, .task-meta { display: block; margin-top: 8px; color: #667085; font-size: 14px; }
.task-enter { width: 100%; margin: 16px 0 0; height: 48px; border: 0; border-radius: 10px; background: #1a73e8; color: #fff; font-size: 16px; }
.task-enter[disabled] { background: #d0d5dd; color: #667085; }
.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; gap: 9px; }
.task-card { box-sizing: border-box; width: 100%; margin: 0; 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: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-label { color: #667085; font-size: 10px; line-height: 15px; }
.task-code { overflow: hidden; margin-top: 1px; color: #12233f; font-size: 17px; font-weight: 700; line-height: 23px; text-overflow: ellipsis; white-space: nowrap; }
.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-identifiers { display: flex; flex-wrap: wrap; gap: 5px 8px; margin-top: 8px; }
.task-cabinet, .task-order { padding: 3px 7px; border-radius: 5px; color: #475467; font-size: 11px; line-height: 17px; background: #f2f4f7; }
.task-order { overflow: hidden; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; }
.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 { overflow: hidden; color: #667085; font-size: 10px; line-height: 15px; text-overflow: ellipsis; white-space: nowrap; }
.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>
+20 -7
View File
@@ -259,6 +259,10 @@
padding: 1px 0 104px;
}
.work-content.evidence-active {
padding-bottom: 1px;
}
.box-select-wrap {
position: relative;
z-index: 5;
@@ -1148,19 +1152,27 @@
.sync-state.ready { color: $success; background: #e8f8ef; }
.sync-state.error { color: $danger; background: #fff0f0; }
.sync-state.loading { color: #8a5a00; background: #fff7df; }
.evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
.evidence-card { min-height: 86px; padding: 10px; border: 1px dashed #b8c9df; border-radius: 8px; background: #f8fafc; }
.evidence-card.filled { border-style: solid; border-color: #69b88a; background: #f0fbf4; }
.evidence-grid { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.evidence-card { padding: 10px; border: 1px solid #d7e1ed; border-radius: 9px; background: #f8fafc; }
.evidence-card.filled { border-color: #a7d6ba; background: #f4fbf6; }
.evidence-card-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.evidence-card-title { display: flex; min-width: 0; align-items: center; gap: 7px; }
.evidence-label { display: block; color: $text; font-size: 12px; font-weight: 700; }
.evidence-file-row { display: flex; align-items: center; gap: 7px; min-width: 0; margin-top: 7px; }
.evidence-thumbnail { flex: 0 0 48px; width: 48px; height: 48px; border-radius: 6px; background: #e7edf5; }
.evidence-file-detail { flex: 1; min-width: 0; }
.evidence-file { display: block; overflow: hidden; margin-top: 5px; color: $text-secondary; font-size: 10px; line-height: 15px; text-overflow: ellipsis; white-space: nowrap; }
.evidence-count { padding: 2px 6px; border-radius: 99px; color: $text-secondary; font-size: 10px; line-height: 16px; background: #e9eef5; }
.evidence-count.complete { color: #087443; background: #dff4e7; }
.evidence-photo-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 9px; }
.evidence-photo-item { position: relative; min-width: 0; padding: 4px; border: 1px solid #dbe5ef; border-radius: 8px; background: #ffffff; }
.evidence-thumbnail { display: flex; width: 100%; height: 68px; align-items: center; justify-content: center; border-radius: 6px; color: $text-secondary; font-size: 9px; line-height: 13px; text-align: center; background: #e7edf5; }
.evidence-thumbnail-placeholder { box-sizing: border-box; padding: 6px; }
.evidence-file { display: block; overflow: hidden; margin-top: 4px; padding: 0 2px; color: $text-secondary; font-size: 9px; line-height: 14px; text-overflow: ellipsis; white-space: nowrap; }
.evidence-void-btn { position: absolute; top: 8px; right: 8px; min-width: 40px; height: 40px; padding: 0 7px; border: 0; border-radius: 20px; color: #ffffff; font-size: 10px; line-height: 40px; background: rgba(185, 28, 28, 0.9); }
.evidence-empty-hint { display: block; margin-top: 7px; color: $text-secondary; font-size: 10px; line-height: 15px; }
.evidence-upload-state { display: block; margin-top: 5px; color: $text-secondary; font-size: 10px; line-height: 15px; }
.evidence-upload-state.uploading, .evidence-upload-state.pending { color: #174d91; }
.evidence-upload-state.failed { color: $danger; }
.evidence-upload-state.succeeded { color: $success; }
.evidence-btn { min-height: 40px; margin-top: 8px; padding: 0 9px; border: 1px solid #9db7d9; border-radius: 7px; color: #174d91; font-size: 12px; line-height: 40px; background: #ffffff; }
.evidence-add-btn { flex-shrink: 0; min-width: 78px; min-height: 40px; margin-top: 0; line-height: 40px; }
.evidence-retry-btn { min-height: 40px; margin-top: 6px; padding: 0 9px; border: 1px solid $danger; border-radius: 7px; color: $danger; font-size: 12px; line-height: 40px; background: #fff5f4; }
.seal-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.seal-input { flex: 1; height: 42px; box-sizing: border-box; padding: 0 10px; border: 1px solid $border; border-radius: 7px; font-size: 12px; background: #f8fafc; }
@@ -1206,6 +1218,7 @@
.pick-item-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pick-btn-confirm { grid-column: 1 / -1; }
.processed-actions { grid-template-columns: 1fr; }
.evidence-photo-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.submit-section { gap: 8px; width: calc(100% - 12px); padding-right: 8px; padding-left: 8px; }
.submit-info { width: 96px; min-width: 96px; }
}
+2 -1
View File
@@ -48,8 +48,9 @@ test('picking task list uses remote loading orders and never falls back to demo
assert.match(tasks, /\['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW'\]\.includes\(task\.workflowStatus\)/)
assert.match(tasks, /task\.workflowStatus === 'AWAITING_REVIEW' \|\| \(task\.assignmentVersion !== null/)
assert.match(tasks, /暂无可领取的拣货任务/)
assert.match(tasks, /集装箱号 \{\{ task\.currentContainerNo \}\}/)
assert.match(tasks, /class="task-label">集装箱号<\/text><text class="task-code">\{\{ task\.currentContainerNo \}\}/)
assert.match(tasks, /柜次号 \{\{ task\.cabinetNo \}\}/)
assert.match(tasks, /button v-for="task in tasks"[\s\S]*?class="task-card"[\s\S]*?@tap="enterTask\(task\)"/)
assert.match(tasks, /records\.filter\(\(task\) => this\.isTaskReady\(task\)\)/)
assert.match(tasks, /isTaskReady\(task\)[\s\S]*?task\.recordStatus === 'ACTIVE'[\s\S]*?!task\.mergedIntoContainerBookingId/)
assert.match(tasks, /records\.filter\(\(task\) => this\.isMergedTask\(task\)\)\.forEach\(\(task\) => this\.clearMergedTaskDraft\(task\)\)/)
+3
View File
@@ -146,6 +146,9 @@ test('refresh preserves local photos for the same evidence identity and flushes
test('evidence photos render thumbnails and support full-screen preview', () => {
assert.match(page, /class="evidence-thumbnail" :src="evidencePreviewSource\(photo\)"/)
assert.match(page, /class="work-content" :class="\{ 'evidence-active': activeStage === 'evidence' && loadingStageReady \}"/)
assert.match(page, /class="evidence-photo-grid"/)
assert.match(page, /class="evidence-count"/)
assert.match(page, /@tap="previewEvidencePhoto\(photo, slot\.type\)"/)
assert.match(page, /evidencePreviewSource\(photo\)[\s\S]*?localPath[\s\S]*?fileUrl/)
assert.match(page, /previewEvidencePhoto\(photo, type\)[\s\S]*?uni\.previewImage\([\s\S]*?current[\s\S]*?urls/)