优化
This commit is contained in:
+1
-1
@@ -156,7 +156,7 @@ export function normalizePdaOrder(row = {}) {
|
||||
cabinetNo: row.cabinetNo ?? row.loading?.cabinetNo ?? row.container?.cabinetNo ?? row.containerNo ?? '',
|
||||
currentContainerNo: row.currentContainerNo ?? row.containerNo ?? row.container?.containerNo ?? row.loadingPlan?.containerNo ?? row.loading?.containerNo ?? null,
|
||||
warehouseId: row.warehouseId ?? row.warehouse?.id ?? row.loadingPlan?.warehouseId ?? null,
|
||||
displayNo: row.orderNo ?? row.inboundCode ?? row.order?.orderNo ?? '业务单号待同步'
|
||||
displayNo: row.orderNo ?? row.inboundCode ?? row.order?.orderNo ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -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()
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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; }
|
||||
|
||||
+34
-26
@@ -302,7 +302,18 @@
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.box-select-summary {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
line-height: 44px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.box-chevron {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
transition: transform 160ms ease-out;
|
||||
}
|
||||
@@ -348,16 +359,6 @@
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.evidence-stage-hint {
|
||||
margin-top: 10px;
|
||||
padding: 8px 10px;
|
||||
border-left: 3px solid #d39a25;
|
||||
color: #795b16;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
background: #fff8e7;
|
||||
}
|
||||
|
||||
.box-chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -752,22 +753,23 @@
|
||||
|
||||
.portal-content { padding: 12px 14px 24px; }
|
||||
|
||||
.home-menu { padding: 12px 12px 24px; }
|
||||
.home-group + .home-group { margin-top: 18px; }
|
||||
.home-group-title { display: flex; align-items: center; margin-bottom: 10px; color: #445166; font-size: 16px; line-height: 24px; }
|
||||
.home-group-mark { width: 4px; height: 20px; flex-shrink: 0; margin-right: 10px; background: #1769e8; }
|
||||
.home-menu { padding: 10px 10px 18px; }
|
||||
.home-group + .home-group { margin-top: 11px; }
|
||||
.home-group-title { display: flex; align-items: center; margin-bottom: 6px; color: #27364a; font-size: 14px; font-weight: 700; line-height: 20px; }
|
||||
.home-group-mark { width: 3px; height: 16px; flex-shrink: 0; margin-right: 7px; border-radius: 2px; background: #1769e8; }
|
||||
.home-group-disabled .home-group-title { color: #7b8795; }
|
||||
.home-group-disabled .home-group-mark { background: #aeb8c4; }
|
||||
.home-operation-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
||||
.home-operation { position: relative; display: flex; box-sizing: border-box; min-width: 0; height: 96px; padding: 10px 5px 9px; flex-direction: column; align-items: center; justify-content: center; border: 1px solid #dce3eb; border-radius: 4px; color: #334155; background: #ffffff; }
|
||||
.home-operation:active { border-color: #9bbcf2; background: #edf4ff; }
|
||||
.home-operation-primary { border-color: #8cb5f5; color: #135ee8; background: #edf4ff; }
|
||||
.home-operation-disabled, .home-operation-disabled[disabled] { border-color: #e1e6ec; color: #8b97a5; opacity: 1; background: #f1f3f6; }
|
||||
.home-operation-disabled:active { border-color: #e1e6ec; background: #f1f3f6; }
|
||||
.home-operation-icon { display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; font-size: 20px; font-weight: 700; }
|
||||
.home-operation-name { display: block; width: 100%; margin-top: 3px; overflow: hidden; font-size: 14px; line-height: 20px; text-align: center; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.home-operation-pending { color: #52657c; font-size: 11px; line-height: 16px; }
|
||||
.home-operation-unavailable { color: #929dab; font-size: 11px; line-height: 16px; }
|
||||
.home-operation-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
|
||||
.home-operation { position: relative; display: flex; box-sizing: border-box; width: 100%; min-width: 0; min-height: 78px; margin: 0; padding: 6px 3px 5px; align-self: stretch; flex-direction: column; align-items: center; justify-content: center; border: 1px solid #e0e6ee; border-radius: 7px; color: #3b4b60; background: #ffffff; box-shadow: 0 1px 2px rgba(38, 54, 74, 0.05); }
|
||||
.home-operation:active { border-color: #77a7f2; background: #edf4ff; transform: translateY(1px); }
|
||||
.home-operation-primary { border-color: #6d9ff0; color: #0d58d3; background: #f3f7ff; box-shadow: 0 2px 6px rgba(19, 94, 232, 0.14); }
|
||||
.home-operation-disabled, .home-operation-disabled[disabled] { border-color: #e3e8ef; color: #7f8b99; opacity: 1; background: #fafbfc; box-shadow: none; }
|
||||
.home-operation-disabled:active { border-color: #e3e8ef; background: #fafbfc; transform: none; }
|
||||
.home-operation-icon { display: flex; align-items: center; justify-content: center; width: 30px; height: 26px; flex: 0 0 26px; font-size: 20px; font-weight: 700; }
|
||||
.home-operation-name { display: block; width: 100%; min-height: 30px; margin-top: 1px; font-size: 12px; font-weight: 600; line-height: 15px; text-align: center; overflow-wrap: anywhere; white-space: normal; }
|
||||
.home-operation-meta { display: inline-flex; min-height: 14px; margin-top: 2px; padding: 0 5px; align-items: center; justify-content: center; border-radius: 7px; font-size: 9px; font-weight: 600; line-height: 14px; white-space: nowrap; }
|
||||
.home-operation-pending { color: #0d58d3; background: #dfeaff; }
|
||||
.home-operation-unavailable { color: #778494; background: #edf0f4; }
|
||||
|
||||
.header-menu-sheet { box-sizing: border-box; width: 100vw; max-width: 480px; }
|
||||
.header-menu-content { padding-top: 1px; }
|
||||
@@ -873,8 +875,11 @@
|
||||
.profile-row { display: flex; align-items: center; justify-content: space-between; min-height: 52px; border-bottom: 1px solid $border; font-size: 13px; }
|
||||
.profile-row:last-child { border-bottom: 0; }
|
||||
.profile-value { color: $text-secondary; font-size: 12px; }
|
||||
.profile-action, .logout-action { box-sizing: border-box; width: 100%; height: 50px; margin-top: 14px; padding: 0 15px; border-radius: 11px; font-size: 13px; line-height: 50px; text-align: left; background: #ffffff; }
|
||||
.profile-action { display: flex; justify-content: space-between; color: $text; }
|
||||
.profile-row-action { box-sizing: border-box; width: 100%; height: 52px; margin: 0; padding: 0; border-radius: 0; color: $text; line-height: normal; text-align: left; background: transparent; }
|
||||
.profile-row-action::after { border: 0; }
|
||||
.profile-row-action:active { background: #f7f9fc; }
|
||||
.profile-row-arrow { color: $text-secondary; font-size: 18px; }
|
||||
.logout-action { box-sizing: border-box; width: 100%; height: 50px; margin-top: 14px; padding: 0 15px; border-radius: 11px; font-size: 13px; line-height: 50px; background: #ffffff; }
|
||||
.logout-action { color: $danger; text-align: center; }
|
||||
.logout-dialog-card { box-sizing: border-box; width: calc(100vw - 48px); max-width: 320px; overflow: hidden; border-radius: 12px; background: #ffffff; box-shadow: 0 18px 50px rgba(0, 0, 0, 0.24); }
|
||||
.logout-dialog-title { display: block; padding: 24px 22px 0; color: $danger; font-size: 17px; font-weight: 700; text-align: center; }
|
||||
@@ -1227,6 +1232,9 @@
|
||||
|
||||
@media (max-width: 340px) {
|
||||
.login-page { padding-right: 16px; padding-left: 16px; }
|
||||
.home-menu { padding-right: 7px; padding-left: 7px; }
|
||||
.home-operation-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
||||
.home-operation { min-height: 76px; }
|
||||
.header-title { font-size: 18px; }
|
||||
.box-select-btn { padding: 0 10px; font-size: 12px; }
|
||||
.pick-item { padding-right: 10px; padding-left: 10px; }
|
||||
|
||||
@@ -35,7 +35,8 @@ test('profile exposes manual App update checking and the runtime version', async
|
||||
const profile = await read('../pages/profile/profile.vue')
|
||||
|
||||
assert.match(profile, /应用版本[\s\S]*?\{\{ appVersion \}\}/)
|
||||
assert.match(profile, /@tap="checkUpdate">检查更新/)
|
||||
assert.match(profile, /profile-list[\s\S]*?应用版本[\s\S]*?profile-row-action" @tap="checkUpdate"[\s\S]*?检查更新[\s\S]*?profile-row-action" @tap="showSettings"[\s\S]*?系统设置/)
|
||||
assert.doesNotMatch(profile, /class="profile-action"/)
|
||||
assert.match(profile, /plus\.runtime\.version/)
|
||||
assert.match(profile, /checkForAppUpdate\(\{ manual: true \}\)/)
|
||||
assert.match(profile, /#ifndef APP-PLUS[\s\S]*?请在 PDA App 中检查更新/)
|
||||
|
||||
@@ -48,9 +48,10 @@ test('manual logout revokes the refresh token without delaying local cleanup', a
|
||||
assert.match(profile, /logoutRequest\(refreshToken, accessToken\)\.catch/)
|
||||
})
|
||||
|
||||
test('development login remembers only the account preference, never the entered password', async () => {
|
||||
test('production login starts empty and remembers only the account preference', async () => {
|
||||
const [login, session] = await Promise.all([read('../pages/login/login.vue'), read('../stores/session.js')])
|
||||
assert.match(login, /form: \{ userId: 'platform_admin', password: 'ChangeMe123!' \}/)
|
||||
assert.match(login, /form: \{ userId: '', password: '' \}/)
|
||||
assert.doesNotMatch(login, /platform_admin|ChangeMe123!/)
|
||||
assert.match(session, /export function saveLoginPreference\(rememberAccount, account = ''\)/)
|
||||
const savePreference = session.match(/export function saveLoginPreference[\s\S]*?\n\}/)?.[0] || ''
|
||||
assert.match(savePreference, /account: rememberAccount/)
|
||||
|
||||
@@ -31,7 +31,8 @@ test('login uses the real auth endpoint and relaunches to the modular workbench'
|
||||
assert.match(auth, /\/auth\/login/)
|
||||
assert.match(login, /loginRequest\(userId, this\.form\.password\)/)
|
||||
assert.match(login, /signInWithTokens\(tokens\)/)
|
||||
assert.match(login, /userId: 'platform_admin', password: 'ChangeMe123!'/)
|
||||
assert.match(login, /form: \{ userId: '', password: '' \}/)
|
||||
assert.doesNotMatch(login, /platform_admin|ChangeMe123!/)
|
||||
assert.match(login, /记住账号/)
|
||||
assert.match(login, /loadLoginPreference\(\)/)
|
||||
assert.match(login, /saveLoginPreference\(this\.rememberAccount, userId\)/)
|
||||
@@ -60,7 +61,7 @@ test('picking task list uses remote loading orders and never falls back to demo
|
||||
assert.match(tasks, /task\.workflowStatus === 'AWAITING_REVIEW' \? '继续装柜取证'/)
|
||||
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, /暂无待处理的拣货任务/)
|
||||
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\)"/)
|
||||
@@ -79,11 +80,14 @@ test('workbench, module, history and profile are independent sources', async ()
|
||||
const [home, module, history, profile] = await Promise.all([
|
||||
read('../pages/home/home.vue'), read('../pages/module/module.vue'), read('../pages/history/history.vue'), read('../pages/profile/profile.vue')
|
||||
])
|
||||
for (const text of ['首页', '库内作业', 'home-operation-grid', 'home-operation-primary', 'getPickingOverview', 'uni-icons']) assert.match(home, new RegExp(text))
|
||||
for (const text of ['首页', '库内作业', 'home-operation-grid', 'home-operation-primary', '开始作业', 'uni-icons']) assert.match(home, new RegExp(text))
|
||||
assert.doesNotMatch(home, /getPickingOverview|pendingBoxCount|箱待拣/)
|
||||
assert.doesNotMatch(home, /BottomNav|status-strip|task-card/)
|
||||
for (const text of ['业务模块', '业务功能', '第一版本仅开放']) assert.match(module, new RegExp(text))
|
||||
assert.match(home, /:disabled="!operation\.available"/)
|
||||
assert.match(home, /home-operation-disabled/)
|
||||
assert.match(home, /home-operation-meta home-operation-pending/)
|
||||
assert.match(home, /home-operation-meta home-operation-unavailable/)
|
||||
assert.match(home, /openOperation\(operation\) \{ if \(operation\.available\) uni\.navigateTo/)
|
||||
assert.doesNotMatch(home, /pages\/operation\/operation\?module=/)
|
||||
assert.match(module, /:disabled="!operation\.available"/)
|
||||
@@ -280,9 +284,17 @@ test('shared visual contracts preserve accessible PDA targets', async () => {
|
||||
assert.match(tasks, /\.task-card\s*\{[^}]*margin:\s*0 0 12px;/)
|
||||
assert.match(tasks, /\.task-card:last-child\s*\{[^}]*margin-bottom:\s*0;/)
|
||||
assert.match(styles, /\.work-content\s*\{[^}]*padding:\s*1px 0 104px;/)
|
||||
assert.match(picking, /class="box-select-summary"/)
|
||||
assert.match(styles, /\.box-select-summary\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;[^}]*text-overflow:\s*ellipsis;[^}]*white-space:\s*nowrap;/)
|
||||
assert.match(styles, /\.submit-section\s*\{[\s\S]*?bottom:\s*8px;[\s\S]*?width:\s*calc\(100% - 20px\);/)
|
||||
assert.match(styles, /\.submit-btn\s*\{[\s\S]*?flex:\s*1;[\s\S]*?height:\s*56px;/)
|
||||
assert.match(styles, /\.home-group-mark\s*\{[^}]*margin-right:\s*10px;/)
|
||||
assert.match(styles, /\.home-group-mark\s*\{[^}]*margin-right:\s*7px;/)
|
||||
assert.match(styles, /\.home-operation-grid\s*\{[^}]*grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\);/)
|
||||
assert.match(styles, /\.home-operation\s*\{[^}]*width:\s*100%;[^}]*min-height:\s*78px;[^}]*align-self:\s*stretch;/)
|
||||
assert.match(styles, /\.home-operation-name\s*\{[^}]*min-height:\s*30px;[^}]*white-space:\s*normal;/)
|
||||
assert.doesNotMatch(styles, /\.home-operation-name\s*\{[^}]*(text-overflow:\s*ellipsis|-webkit-line-clamp|white-space:\s*nowrap)/)
|
||||
assert.match(styles, /\.home-operation-meta\s*\{[^}]*border-radius:\s*7px;[^}]*white-space:\s*nowrap;/)
|
||||
assert.match(styles, /@media \(max-width:\s*340px\)\s*\{[\s\S]*?\.home-operation-grid\s*\{[^}]*grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\);/)
|
||||
assert.match(styles, /\.header-menu-item\s*\{[^}]*width:\s*calc\(100% - 32px\);[^}]*margin:\s*0 16px;/)
|
||||
assert.match(styles, /\.header-menu-close\s*\{[^}]*width:\s*calc\(100% - 32px\);/)
|
||||
assert.match(picking, /aria-modal="true"/)
|
||||
|
||||
@@ -5,10 +5,12 @@ import { readFile } from 'node:fs/promises'
|
||||
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('picking and photo workspaces are independently selectable', () => {
|
||||
assert.match(page, /v-if="activeStage === 'evidence' && loadingStageReady" class="loading-evidence-section"/)
|
||||
assert.match(page, /v-if="activeStage === 'evidence'" class="loading-evidence-section"/)
|
||||
assert.match(page, /selectStage\(stage\) \{[\s\S]*?this\.activeStage = stage/)
|
||||
assert.doesNotMatch(page, /:disabled="!loadingStageReady"/)
|
||||
assert.match(page, /isPickingCompleteTask\(task\)[\s\S]*?AWAITING_REVIEW[\s\S]*?COMPLETED/)
|
||||
assert.match(page, /pickingCompleted\(\)[\s\S]*?this\.remoteTaskId \|\| this\.currentBox\.taskId[\s\S]*?if \(!this\.remotePick\) return false[\s\S]*?hasServerPending[\s\S]*?isPickingCompleteTask\(this\.remotePick\) && !hasServerPending/)
|
||||
assert.match(page, /请先处理并提交全部待拣明细/)
|
||||
assert.doesNotMatch(page, /任务增量更新后|evidence-stage-hint/)
|
||||
})
|
||||
|
||||
test('a server task never inherits a completed marker from the previously opened local cabinet', () => {
|
||||
@@ -46,12 +48,12 @@ test('AWAITING_REVIEW alone cannot request or mutate server evidence without a s
|
||||
assert.doesNotMatch(page, /if \(this\.pickingCompleted\) await this\.loadLoadingEvidence\(\)/)
|
||||
})
|
||||
|
||||
test('dynamic pending items withdraw evidence until the server task is picking-complete again', () => {
|
||||
test('dynamic pending items do not block switching between picking and evidence', () => {
|
||||
assert.match(page, /class="stage-switch" role="tablist"/)
|
||||
assert.match(page, /@tap="selectStage\('picking'\)"/)
|
||||
assert.match(page, /@tap="selectStage\('evidence'\)"/)
|
||||
assert.match(page, /selectStage\(stage\) \{[\s\S]*?stage === 'evidence' && !this\.loadingStageReady[\s\S]*?请先完成新增待拣明细[\s\S]*?return[\s\S]*?this\.activeStage = stage/)
|
||||
assert.match(page, /class="stage-tab"[\s\S]*?:disabled="!loadingStageReady"[\s\S]*?装柜取证/)
|
||||
assert.doesNotMatch(page, /stage === 'evidence' && !this\.loadingStageReady/)
|
||||
assert.doesNotMatch(page, /请先完成新增待拣明细/)
|
||||
assert.match(page, /activeStage === 'picking'/)
|
||||
assert.match(page, /:disabled="submitting \|\| !loadingStageReady \|\| pickingVersion === null/)
|
||||
})
|
||||
@@ -65,7 +67,8 @@ test('incremental refresh merges new pending items without overwriting submitted
|
||||
assert.match(page, /const previousTaskVersion = this\.pickingVersion/)
|
||||
assert.match(page, /const previousItemIds = new Set/)
|
||||
assert.match(page, /previousEvidenceState[\s\S]*?isSameEvidenceIdentity\(previousEvidenceState\.identity, this\.evidenceIdentity\)[\s\S]*?this\.evidenceFiles = \{ \.\.\.this\.evidenceFiles, \.\.\.previousEvidenceState\.files \}/)
|
||||
assert.match(page, /if \(!this\.pickingCompleted\)[\s\S]*?delete completedBoxes\[this\.currentBoxIndex\][\s\S]*?this\.activeStage = 'picking'/)
|
||||
assert.match(page, /if \(!this\.pickingCompleted\)[\s\S]*?delete completedBoxes\[this\.currentBoxIndex\]/)
|
||||
assert.doesNotMatch(page, /delete completedBoxes\[this\.currentBoxIndex\][\s\S]{0,160}?this\.activeStage = 'picking'/)
|
||||
assert.match(page, /refresh\.incremental[\s\S]*?addedItemIds/)
|
||||
})
|
||||
|
||||
@@ -139,11 +142,16 @@ test('merged or non-active cabinet details are rejected before claim and clear s
|
||||
for (const reset of ['remoteLoading = false', 'submitting = false', 'remoteTaskId = null', 'loadingVersion = null', 'scanHistory = []', 'currentBox.containerBookingId = null', 'currentBox.currentContainerNo = null', 'resetEvidenceState()']) assert.match(clear, new RegExp(reset.replace(/[.()[\]]/g, '\\$&')))
|
||||
})
|
||||
|
||||
test('server evidence mutations remain closed before the loading stage is ready', () => {
|
||||
test('photos upload before picking completion while evidence binding remains gated', () => {
|
||||
assert.match(page, /requireEvidenceStage\(\)[\s\S]*?if \(!this\.pickingCompleted\)[\s\S]*?请先提交拣货完成[\s\S]*?if \(!this\.loadingStageReady\)/)
|
||||
const queue = page.match(/async queueEvidenceUpload\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
|
||||
assert.match(queue, /if \(!this\.loadingStageReady\) return/)
|
||||
for (const method of ['retryEvidence', 'saveEvidence', 'voidEvidencePhoto', 'completeLoadingTask']) {
|
||||
assert.ok(queue.indexOf('await queue.flush()') < queue.indexOf('if (!this.loadingStageReady)'))
|
||||
assert.ok(queue.indexOf('if (!this.loadingStageReady)') < queue.indexOf('await this.saveEvidence(type, null, path)'))
|
||||
assert.match(page, /chooseEvidence\(slot\)[\s\S]*?this\.queueEvidenceUpload\(slot\.type, path\)/)
|
||||
const retry = page.match(/async retryEvidence\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
|
||||
assert.match(retry, /if \(!this\.requireEvidenceCapture\(\)\) return/)
|
||||
assert.ok(retry.indexOf('await queue.flush()') < retry.indexOf('if (!this.loadingStageReady)'))
|
||||
for (const method of ['saveEvidence', 'voidEvidencePhoto', 'completeLoadingTask']) {
|
||||
const body = page.match(new RegExp(`(?:async )?${method}\\([^)]*\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0] || ''
|
||||
assert.match(body, /if \(!this\.requireEvidenceStage\(\)\)(?: return| \{)/, `${method} should guard the evidence stage`)
|
||||
}
|
||||
@@ -158,7 +166,7 @@ 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="work-content" :class="\{ 'evidence-active': activeStage === 'evidence' \}"/)
|
||||
assert.match(page, /class="evidence-photo-grid"/)
|
||||
assert.match(page, /class="evidence-count"/)
|
||||
assert.match(page, /@tap="previewEvidencePhoto\(photo, slot\.type\)"/)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
|
||||
|
||||
test('picking task list exposes only operator-facing production information', async () => {
|
||||
const [tasks, adapter] = await Promise.all([
|
||||
read('../pages/tasks/tasks.vue'),
|
||||
read('../api/pda-loading.js')
|
||||
])
|
||||
|
||||
assert.match(tasks, /共 \$\{tasks\.length\} 个待处理任务/)
|
||||
assert.match(tasks, /v-if="task\.displayNo"/)
|
||||
assert.match(tasks, /v-if="task\.warehouseName"/)
|
||||
assert.doesNotMatch(tasks, /接口返回|当前接口|apiEnvironment|rawTaskCount|账号:|未命名订单|缺少仓库信息|服务端就绪校验|console\.info/)
|
||||
assert.doesNotMatch(adapter, /业务单号待同步/)
|
||||
})
|
||||
|
||||
test('production login contains no built-in account or password', async () => {
|
||||
const login = await read('../pages/login/login.vue')
|
||||
|
||||
assert.match(login, /form: \{ userId: '', password: '' \}/)
|
||||
assert.doesNotMatch(login, /platform_admin|ChangeMe123!/)
|
||||
})
|
||||
Reference in New Issue
Block a user