Merge branch 'release/test' of http://8.163.30.128:10082/html/pda-overseas-uniapp into release/test

This commit is contained in:
2026-10-30 17:59:59 +08:00
15 changed files with 482 additions and 66 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
2. 登录/Token/统一请求先读下表登录API/会话、`src/config/runtime.js`、`../../docs/统一认证与服务通信契约.md`。
3. 沿实际 API 读直接服务指引:入仓/库存 `../../baiyue-firstleg/lastleg-service/AGENTS.md`,资源 `../../baiyue-firstleg/warehouse-overseas-service/AGENTS.md`;网关路径/鉴权问题才读 `../../baiyue-firstleg/gateway/AGENTS.md`。跨模块或入口不明才读 `../../docs/海外仓业务/README.md`。
仅读适用指引/目标页面/API;纯页面不读后端,跨服务状态/鉴权不明才扩读。遵根预算,先规则后命中段,不全文读海外仓 README。
按根预算先规则后命中,不通读海外仓 README;纯页面不读后端,跨服务状态/鉴权不明才扩读。样式读[规范](../../docs/海外PDA界面样式规范.md)。
## 项目定位
+70
View File
@@ -0,0 +1,70 @@
import { listReceivingCandidates } from './inbound.js'
import { assertReceivingWarehouse } from './receiving-adapter.js'
import { listWarehouseTransferCandidates } from './warehouse-transfers.js'
import { listReturnCountTasks } from './return-counting.js'
import { listPackingPlans } from './return-packing.js'
import { listDispatchTasks, listOutboundTasks, listCargoDiscardTasks } from './fulfillment.js'
import { listRelabelCandidates } from './relabel.js'
export const WORKBENCH_COUNT_KEYS = ['receiving', 'warehouseTransferList', 'warehouseTransferWork', 'returnCounting', 'returnPacking', 'dispatchPicking', 'outboundPicking', 'relabelReceive', 'relabelExecute', 'relabelReview']
const ready = (count, unit = '单') => ({ status: 'ready', count, unit })
const distinctCount = (rows, key) => {
if (!Array.isArray(rows)) throw new Error('待办列表返回结构无效')
const codes = rows.map((row) => String(row?.[key] || '').trim())
if (codes.some((code) => !code)) throw new Error('待办列表缺少业务编号')
return new Set(codes).size
}
export async function loadWorkbenchCounts({ profile, activeTransferNo = '' }) {
const sources = [
{ keys: ['receiving'], load: async () => {
const rows = await listReceivingCandidates()
rows.forEach((row) => assertReceivingWarehouse(row, profile, '待收货柜'))
if (rows.some((row) => !row.sourceContainerBookingId || !row.containerNo)) throw new Error('待收货柜缺少业务定位信息')
const keys = rows.map((row) => ({ key: `${row.sourceContainerBookingId}:${row.containerNo}` }))
return { receiving: ready(distinctCount(keys, 'key'), '柜') }
} },
{ keys: ['warehouseTransferList', 'warehouseTransferWork'], load: async () => {
const rows = (await listWarehouseTransferCandidates()).filter((row) => ['WAIT_PDA_RECEIVING', 'PARTIALLY_RECEIVED'].includes(row.status))
const active = rows.filter((row) => String(row.transferNo).trim() === String(activeTransferNo).trim())
return { warehouseTransferList: ready(distinctCount(rows, 'transferNo')), warehouseTransferWork: ready(distinctCount(active, 'transferNo'), '单进行中') }
} },
{ keys: ['returnCounting'], load: async () => {
const value = await listReturnCountTasks({ page: 1, size: 1 })
const total = value?.total
if ((typeof total !== 'number' && !(typeof total === 'string' && /^\d+$/.test(total))) || !Number.isSafeInteger(Number(total)) || Number(total) < 0) throw new Error('退货清点待办总数无效')
return { returnCounting: ready(Number(total), '托') }
} },
{ keys: ['returnPacking'], load: async () => ({ returnPacking: ready(distinctCount((await listPackingPlans()).filter((row) => row.status === 'READY'), 'planNo'), '单计划') }) },
{ keys: ['dispatchPicking'], load: async () => {
const rows = (await listDispatchTasks()).filter((row) => row.status !== 'CANCELLED' && ['BEFORE_LOADING', 'LOADING', 'AFTER_LOADING'].includes(row.stage))
return { dispatchPicking: ready(distinctCount(rows, 'dispatchNo'), '单未发车') }
} },
{ keys: ['outboundPicking'], load: async () => {
const [outbounds, discards] = await Promise.all([listOutboundTasks('PENDING'), listCargoDiscardTasks()])
return { outboundPicking: { ...ready(distinctCount(outbounds, 'taskNo')), discardCount: distinctCount(discards.filter((row) => row.status !== 'COMPLETED'), 'discardNo') } }
} },
{ keys: ['relabelReceive', 'relabelExecute', 'relabelReview'], load: async () => {
const rows = await listRelabelCandidates()
return {
relabelReceive: ready(distinctCount(rows.filter((row) => !row.taskStatus || row.taskStatus === 'AVAILABLE'), 'relabelNo'), '单待接'),
relabelExecute: ready(distinctCount(rows.filter((row) => row.taskStatus === 'PROCESSING' && row.claimedByCurrentOperator === true), 'relabelNo'), '单进行中'),
relabelReview: ready(distinctCount(rows.filter((row) => row.taskStatus === 'WAIT_REVIEW'), 'relabelNo'), '单待复核')
}
} }
]
const results = await Promise.allSettled(sources.map((source) => source.load()))
return Object.assign({}, ...results.map((result, index) => result.status === 'fulfilled' ? result.value : Object.fromEntries(sources[index].keys.map((key) => [key, { status: 'error', error: result.reason }]))))
}
export function workbenchBadge(key, value) {
if (!WORKBENCH_COUNT_KEYS.includes(key)) return { label: key.endsWith('Query') ? '扫码查询' : key === 'returnReceipt' ? '扫码签收' : '扫码作业', tone: 'is-static' }
if (!value) return { label: '加载中…', tone: 'is-loading' }
if (value.status === 'error') return { label: '获取失败', tone: 'is-error' }
const hasWork = value.count > 0 || value.discardCount > 0
let label = `${value.count} ${value.unit}`
if (key === 'warehouseTransferWork' && value.count === 0) label = '未开始'
if (key === 'outboundPicking' && value.discardCount > 0) label = `${value.count} 出库 · ${value.discardCount} 废弃`
return { label, tone: hasWork ? 'has-work' : 'is-empty' }
}
+6 -5
View File
@@ -1,7 +1,7 @@
<template>
<view class="receiving-summary">
<view class="receiving-summary-head">
<view><text class="receiving-summary-label">当前柜号</text><text class="receiving-summary-container">{{ session?.containerNo || '—' }}</text></view>
<view class="receiving-summary-identity"><text class="receiving-summary-label">当前柜号</text><text class="receiving-summary-container">{{ session?.containerNo || '—' }}</text></view>
<text class="receiving-summary-status">{{ statusLabel }}</text>
</view>
<view class="receiving-summary-context">
@@ -45,10 +45,11 @@
</script>
<style scoped>
.receiving-summary{box-sizing:border-box;margin-bottom:10px;padding:10px 12px;border:1px solid #b8d2f3;border-radius:13px;background:linear-gradient(145deg,#fff,#f2f7ff);box-shadow:0 3px 12px rgba(31,74,125,.08)}
.receiving-summary-head,.receiving-summary-progress>view:first-child{display:flex;align-items:center;justify-content:space-between;gap:10px}
.receiving-summary-head > view:first-child{flex:1;min-width:0}
.receiving-summary-container{white-space:normal;overflow-wrap:anywhere;word-break:break-all}
.receiving-summary{box-sizing:border-box;width:100%;min-width:0;max-width:100%;margin-bottom:10px;padding:10px 12px;border:1px solid #b8d2f3;border-radius:13px;background:linear-gradient(145deg,#fff,#f2f7ff);box-shadow:0 3px 12px rgba(31,74,125,.08)}
.receiving-summary-head{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:10px}
.receiving-summary-progress>view:first-child{display:flex;align-items:center;justify-content:space-between;gap:10px}
.receiving-summary-identity{min-width:0;max-width:100%}
.receiving-summary-container{width:100%;min-width:0;max-width:100%;white-space:normal;overflow-wrap:anywhere;word-break:break-all}
.receiving-summary-label,.receiving-summary-container,.metric-value,.metric-label{display:block}.receiving-summary-label{color:#718095;font-size:9px;line-height:13px}.receiving-summary-container{margin-top:1px;color:#17324d;font-size:17px;font-weight:850;line-height:22px}
.receiving-summary-status{flex-shrink:0;padding:4px 8px;border-radius:99px;color:#1668dc;font-size:9px;font-weight:700;background:#e3efff}
.receiving-summary-context{display:flex;flex-direction:column;gap:4px;margin-top:7px;color:#617389;font-size:9px;line-height:14px}.receiving-summary-destinations{display:flex;min-width:0;align-items:flex-start}.destination-label{flex-shrink:0}.destination-tags{display:flex;min-width:0;flex-wrap:wrap;gap:3px 4px}.destination-tags text{padding:0 5px;border-radius:99px;color:#42617f;line-height:14px;background:#e8f0fa;white-space:nowrap}
+1 -1
View File
@@ -1,7 +1,7 @@
export const MODULES = [
{ key: 'inbound', name: '入仓作业', nameKey: 'modules.inbound.name', icon: '入', description: '处理拆柜收货与客户退货实收入仓', descriptionKey: 'modules.inbound.description', operations: [
{ key: 'receiving', name: '拆柜收货', nameKey: 'modules.receiving.name', icon: '卸', iconType: 'download', description: '三类照片签收,箱码定位入仓码后按目的类型收货', descriptionKey: 'modules.receiving.description', available: true },
{ key: 'warehouseTransferList', name: '仓储转运测量列表', nameKey: 'modules.warehouseTransferList.name', icon: '列', iconType: 'list', description: '查看待测量的仓储转运单', descriptionKey: 'modules.warehouseTransferList.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=list' },
{ key: 'warehouseTransferList', name: '仓储转运测量', nameKey: 'modules.warehouseTransferList.name', icon: '列', iconType: 'list', description: '查看待测量的仓储转运单', descriptionKey: 'modules.warehouseTransferList.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=list' },
{ key: 'warehouseTransferWork', name: '转运测量', nameKey: 'modules.warehouseTransferWork.name', icon: '转', iconType: 'compose', description: '进入转运单详情完成测量、打托和拍照', descriptionKey: 'modules.warehouseTransferWork.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=work' },
{ key: 'returnReceipt', name: '退货入库', nameKey: 'modules.returnReceipt.name', icon: '退', iconType: 'undo', description: '扫描退货单或 SKU,按本包实收数量生成退货库存', descriptionKey: 'modules.returnReceipt.description', available: true, route: '/pages/return-receipt/return-receipt' },
{ key: 'returnPacking', name: '合箱', nameKey: 'modules.returnPacking.name', icon: '合', iconType: 'folder-add', description: '选择同客户已签收退货单,按 SKU 数量生成新箱', descriptionKey: 'modules.returnPacking.description', available: true, route: '/pages/return-packing/return-packing' },
+12 -4
View File
@@ -1,7 +1,7 @@
<template>
<view v-if="user" class="page-shell portal-page fulfillment-page">
<PortalHeader title="排车作业" :user="user" />
<scroll-view class="fulfillment-scroll" scroll-y><view class="portal-content fulfillment-content">
<scroll-view class="fulfillment-scroll" scroll-y><view class="portal-content fulfillment-content" :class="{ 'pda-task-list': !current }">
<view class="fulfillment-heading"><text class="fulfillment-title">理货审核、真实拣货与装车</text></view>
<view v-if="current" class="detail-toolbar"><text class="detail-title">{{ current.dispatchNo }}</text><button :disabled="busy" @tap="closeTask">‹ 返回列表</button></view>
<view v-if="current" class="stage-switch" role="tablist" aria-label="排车作业">
@@ -16,10 +16,14 @@
<view v-if="loading" class="fulfillment-state"><text>正在加载排车事实…</text></view>
<view v-if="error" class="fulfillment-error" role="alert"><text class="state-title">{{ error.message }}</text><text v-if="error.code">错误码:{{ error.code }}</text><text v-if="error.requestId">requestId:{{ error.requestId }}</text><text v-if="error.traceId">traceId:{{ error.traceId }}</text><text v-if="error.network">网络中断,请稍后重新操作。</text><button :disabled="busy || loading" @tap="retry">重新核对</button></view>
<template v-if="!current && !loading">
<view class="list-toolbar"><text class="list-title">排车任务</text><button :disabled="busy" @tap="loadTasks">刷新</button></view>
<view class="list-toolbar pda-list-toolbar"><text>排车任务<text v-if="!error"> · {{ tasks.length }} 单</text></text><button :disabled="busy" @tap="loadTasks"><uni-icons type="refresh" color="#135ee8" size="18" /><text>刷新</text></button></view>
<view v-if="!tasks.length && !error" class="fulfillment-state">暂无排车任务,可扫描排车单号读取。</view>
<view v-if="tasks.length" class="task-list">
<button v-for="task in tasks" :key="task.dispatchNo" class="task-card" @tap="openTask(task.dispatchNo)"><text class="business-code">{{ task.dispatchNo }}</text><text v-if="task.dispatchNo === recentTaskNo" class="recent-badge">上次作业</text><text class="status-badge">{{ stageLabel(task) }}</text><text class="task-meta">任务号 {{ task.taskNo || '—' }}</text></button>
<view v-if="tasks.length" class="task-list pda-list-cards">
<button v-for="task in tasks" :key="task.dispatchNo" class="task-card pda-task-card" hover-class="pda-task-card-pressed" @tap="openTask(task.dispatchNo)">
<view class="pda-task-head"><text class="pda-task-no">{{ task.dispatchNo }}</text><text class="status-badge pda-task-status">{{ stageLabel(task) }}</text></view>
<text v-if="String(task.taskNo || '').trim() && String(task.taskNo || '').trim() !== String(task.dispatchNo || '').trim()" class="pda-task-meta">任务号 {{ task.taskNo }}</text>
<view class="pda-task-footer"><text v-if="task.dispatchNo === recentTaskNo" class="recent-badge">上次作业</text><view class="pda-task-action"><text>详情</text><uni-icons type="right" color="#135ee8" size="14" /></view></view>
</button>
</view>
</template>
<template v-if="current">
@@ -298,3 +302,7 @@ export default {
.scan-notice-hint { display: block; margin-top: 24rpx; color: #72839b; font-size: 24rpx; }
</style>
<style scoped lang="scss">
@import '../../styles/task-list.scss';
</style>
+52 -7
View File
@@ -8,15 +8,15 @@
<text class="home-overview-title">{{ greeting }},{{ displayName }}</text>
<text class="home-overview-warehouse">{{ warehouseLabel }}</text>
</view>
<view class="home-overview-status"><text class="home-overview-status-value">{{ availableCount }}</text><text class="home-overview-status-label">项可处理</text></view>
</view>
<view class="home-workload-toolbar"><text>作业待办</text><button :disabled="countsLoading" @tap="refreshCounts"><uni-icons type="refresh" color="#135ee8" size="18" /><text>{{ countsLoading ? '刷新中…' : '刷新' }}</text></button></view>
<view v-for="group in groups" :key="group.key" class="home-group">
<view class="home-group-title"><view class="home-group-mark" :class="`home-group-mark-${group.key}`"></view><text>{{ group.name }}</text><text class="home-group-count">{{ group.operations.length }}</text></view>
<view class="home-group-title"><view class="home-group-mark" :class="`home-group-mark-${group.key}`"></view><text>{{ group.name }}</text></view>
<view class="home-operation-grid">
<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">可处理</text>
<text v-if="operation.available" class="home-operation-pending" :class="countBadge(operation).tone">{{ countBadge(operation).label }}</text>
<text v-else class="home-operation-unavailable">暂未开放</text>
</button>
</view>
@@ -28,8 +28,10 @@
<script>
import PortalHeader from '@/components/PortalHeader.vue'
import { getCurrentWarehouseProfile } from '@/api/inbound.js'
import { loadWorkbenchCounts, workbenchBadge } from '@/api/workbench.js'
import { MODULES } from '@/data/overseas-workbench.js'
import { applyWarehouseProfile, session, requireSession } from '@/stores/session.js'
import { restoreWarehouseWork, warehouseWork } from '@/stores/warehouse-work.js'
const HOME_GROUPS = MODULES.map((module) => ({
...module,
@@ -39,9 +41,8 @@
export default {
components: { PortalHeader },
data() { return { user: session.currentUser, groups: HOME_GROUPS, menuOpen: false } },
data() { return { user: session.currentUser, groups: HOME_GROUPS, menuOpen: false, counts: {}, countsLoading: false, countsGeneration: 0 } },
computed: {
availableCount() { return this.groups.reduce((total, group) => total + group.operations.filter((operation) => operation.available).length, 0) },
displayName() {
const name = String(this.user?.name || '').trim()
return name && !/^[a-z0-9._-]+$/i.test(name) ? name : '仓库作业员'
@@ -57,17 +58,48 @@
return '晚上好'
}
},
onShow() {
async onShow() {
if (!requireSession()) return
const generation = ++this.countsGeneration
this.user = session.currentUser
if (!this.user?.warehouseCode || !this.user?.warehouseName) this.loadWarehouseProfile()
this.counts = {}; this.countsLoading = true
if (!this.user?.warehouseCode || !this.user?.warehouseName) await this.loadWarehouseProfile()
if (generation !== this.countsGeneration) return
this.countsLoading = false
if (!session.currentUser) { this.user = null; return }
this.user = session.currentUser
await this.refreshCounts()
},
onHide() { this.invalidateCounts() },
onUnload() { this.invalidateCounts() },
beforeUnmount() { this.invalidateCounts() },
onBackPress() {
if (!this.menuOpen) return false
this.$refs.portalHeader.closeMenu()
return true
},
methods: {
countBadge(operation) { return workbenchBadge(operation.key, this.counts[operation.key]) },
countsScope() { const user = session.currentUser; return user?.id && user?.warehouseId ? `${session.apiBase}:${user.id}:${user.warehouseId}` : '' },
invalidateCounts() { this.countsGeneration += 1; this.countsLoading = false; this.counts = {} },
async refreshCounts() {
if (this.countsLoading || !this.countsScope()) return
const generation = ++this.countsGeneration
const scope = this.countsScope()
const profile = { ...session.currentUser }
restoreWarehouseWork(`${profile.id}:${profile.warehouseId}`)
const activeTransferNo = warehouseWork.warehouseTransfer.currentNo
this.counts = {}; this.countsLoading = true
try {
const counts = await loadWorkbenchCounts({ profile, activeTransferNo })
if (generation === this.countsGeneration && scope === this.countsScope()) this.counts = counts
} finally {
if (generation === this.countsGeneration) {
this.countsLoading = false
if (scope !== this.countsScope()) { this.counts = {}; this.user = session.currentUser }
}
}
},
async loadWarehouseProfile() {
try { this.user = applyWarehouseProfile(await getCurrentWarehouseProfile()) } catch (_) { this.user = session.currentUser }
},
@@ -79,3 +111,16 @@
}
}
</script>
<style scoped>
.home-overview{margin-bottom:12px}
.home-workload-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px;color:#526174;font-size:14px;font-weight:700}
.home-workload-toolbar button{display:flex;align-items:center;justify-content:center;gap:5px;width:auto;height:36px;margin:0;padding:0 14px;border:1px solid #b8d1f5;border-radius:20px;color:#135ee8;font-size:13px;font-weight:700;line-height:normal;background:#fff}
.home-workload-toolbar button[disabled]{opacity:.55}
.home-operation{height:auto;min-height:104px;padding:10px 6px}
.home-operation-name{min-height:17px}
.home-operation-pending{box-sizing:border-box;max-width:100%;font-size:10px;line-height:16px;white-space:normal}
.home-operation-pending.has-work{color:#9a5300;background:#fff0d6}
.home-operation-pending.is-empty,.home-operation-pending.is-loading,.home-operation-pending.is-static{color:#6f8194;background:#e7edf4}
.home-operation-pending.is-error{color:#b42318;background:#fff1f0}
</style>
File diff suppressed because one or more lines are too long
+13 -5
View File
@@ -2,7 +2,7 @@
<view v-if="user" class="page-shell portal-page fulfillment-page">
<PortalHeader title="出库作业" :user="user" />
<scroll-view class="fulfillment-scroll" scroll-y>
<view class="portal-content fulfillment-content">
<view class="portal-content fulfillment-content" :class="{ 'pda-task-list': !current && !discardCurrent }">
<view class="fulfillment-heading"><text class="fulfillment-title">出库任务</text></view>
<view v-if="current" class="detail-toolbar"><view><text class="detail-kicker">当前任务</text><text class="detail-title">出库作业详情</text></view><button :disabled="busy" @tap="closeTask">‹ 返回列表</button></view>
<view v-if="!discardCurrent" class="fulfillment-card">
@@ -14,9 +14,13 @@
<view v-else-if="error" class="fulfillment-error" role="alert"><text class="state-title">{{ error.message }}</text><text v-if="error.code">错误码:{{ error.code }}</text><text v-if="error.requestId">requestId:{{ error.requestId }}</text><text v-if="error.traceId">traceId:{{ error.traceId }}</text><text v-if="error.network" class="offline-note">网络连接失败,请刷新核对任务状态后重试。</text><button class="secondary-button" :disabled="loading || busy || discardLoading || discardBusy" @tap="retry">重试</button></view>
<template v-if="!loading && !current && !discardCurrent">
<view class="list-toolbar"><view><text class="list-title">待处理出库任务</text><text class="list-count">{{ tasks.length }} 个任务</text></view><button :disabled="loading" @tap="loadTasks">刷新</button></view>
<view v-if="tasks.length" class="task-list">
<button v-for="task in tasks" :key="task.taskNo" class="task-card" @tap="openTask(task.taskNo)"><view class="task-head"><view class="task-code-row"><text class="business-code">{{ task.outboundNo || '—' }}</text><text v-if="task.taskNo === recentTaskNo" class="recent-badge">上次作业</text></view><text class="status-badge">{{ statusLabel(task.status) }}</text></view><text class="task-meta">{{ taskType(task) }} · 任务号 {{ task.taskNo || '—' }}</text><text class="task-enter">查看并开始出库 ›</text></button>
<view class="list-toolbar pda-list-toolbar"><text>待处理出库任务<text v-if="!error"> · {{ tasks.length }} 项</text></text><button :disabled="loading" @tap="loadTasks"><uni-icons type="refresh" color="#135ee8" size="18" /><text>刷新</text></button></view>
<view v-if="tasks.length" class="task-list pda-list-cards">
<button v-for="task in tasks" :key="task.taskNo" class="task-card pda-task-card" hover-class="pda-task-card-pressed" @tap="openTask(task.taskNo)">
<view class="pda-task-head"><text class="pda-task-no">{{ task.outboundNo || '—' }}</text><text class="status-badge pda-task-status">{{ statusLabel(task.status) }}</text></view>
<view class="pda-task-meta"><text>{{ taskType(task) }}</text><text v-if="String(task.taskNo || '').trim() && String(task.taskNo || '').trim() !== String(task.outboundNo || '').trim()"> · 任务号 {{ task.taskNo }}</text></view>
<view class="pda-task-footer"><text v-if="task.taskNo === recentTaskNo" class="recent-badge">上次作业</text><view class="pda-task-action"><text>查看并开始出库</text><uni-icons type="right" color="#135ee8" size="14" /></view></view>
</button>
</view>
<view v-else-if="!error && !discardTasks.length" class="fulfillment-state"><text class="state-title">暂无出库任务</text><text>可刷新列表,或扫描任务号恢复未完成作业。</text></view>
<view v-if="discardTasks.length" class="discard-section"><view class="list-toolbar"><view><text class="list-title">废弃订单</text><text class="list-count">{{ discardOrderCount }} 个订单</text></view></view><view class="task-list"><button v-for="order in discardOrders" :key="order.discardNo" class="task-card" @tap="openDiscardOrder(order.discardNo)"><view class="task-head"><view class="task-code-row"><text class="business-code">{{ order.discardNo || '—' }}</text></view><text class="status-badge">废弃订单</text></view><text v-if="order.outboundNo" class="task-meta">出库单 {{ order.outboundNo }} · 废弃出库</text><text class="task-meta">托盘 {{ order.palletCount }} 个 · 按箱废弃 {{ order.cartonCount }} 箱 · 待作业 {{ order.pendingPalletCount }} 个</text><text class="task-enter">点击进入废弃出库作业 ›</text></button></view></view>
@@ -39,7 +43,7 @@
<view v-if="selectedPosition && selectedRemaining > 0" class="fulfillment-card"><text class="card-title">本次拣货:{{ lineBusinessCode(selectedPosition) }}</text><text v-if="selectedItem.returnSkuQuantity != null" class="scan-help">本来源计划出库 {{ selectedItem.returnSkuQuantity }} 件,请清点这些件数后确认;余数保留原箱。</text><view class="count-row"><text>拣货箱数</text><input type="number" :value="packages" :placeholder="`请输入 1-${selectedRemaining} 箱`" @input="updatePackages" /><text>最多 {{ selectedRemaining }} 箱</text></view><button class="primary-button" :disabled="!canPick || busy" @tap="submitPick">{{ busyAction === 'pick' ? '提交中…' : '确认出库拣货' }}</button></view>
<view class="fulfillment-card gate-card"><text class="card-title">完成出库任务</text><text v-if="remainingPackages > 0" class="gate-warning">尚有 {{ remainingPackages }} 箱未拣,不能完成出库。</text><text v-else-if="current.status === 'COMPLETED'" class="gate-success">出库任务已完成,不可重复提交。</text><text v-else class="gate-note">全部计划货物已扫描核平,服务端将再次校验库存门禁。</text><button v-if="current.status !== 'COMPLETED'" class="primary-button" :disabled="!canComplete || busy" @tap="completeTask">{{ busyAction === 'complete' ? '完成中…' : '完成出库任务' }}</button></view>
<view class="fulfillment-card gate-card"><text class="card-title">完成出库任务</text><text v-if="remainingPackages > 0" class="gate-warning">尚有 {{ remainingPackages }} 箱未拣,不能完成出库。</text><text v-else-if="current.status === 'COMPLETED'" class="gate-success">出库任务已完成,不可重复提交。</text><button v-if="current.status !== 'COMPLETED'" class="primary-button" :disabled="!canComplete || busy" @tap="completeTask">{{ busyAction === 'complete' ? '完成中…' : '完成出库任务' }}</button></view>
</template>
<template v-else-if="discardCurrent">
@@ -354,3 +358,7 @@
.discard-remark .card-title, .discard-remark .task-meta { color: #dc2626; }
.discard-scanned-count { display: block; margin-bottom: 8px; color: #dc2626; font-size: 15px; font-weight: 700; }
</style>
<style scoped lang="scss">
@import '../../styles/task-list.scss';
</style>
+12 -7
View File
@@ -1,18 +1,19 @@
<template>
<view v-if="user" class="page-shell portal-page work-page">
<PortalHeader :title="title" :user="user" />
<scroll-view class="work-scroll" scroll-y><view class="portal-content work-content">
<scroll-view class="work-scroll" scroll-y><view class="portal-content work-content" :class="{ 'pda-task-list': !showCurrent && stage !== 'review' }">
<view v-if="showCurrent" class="detail-toolbar"><button :disabled="busy" @tap="closeCurrent">‹ 返回列表</button></view>
<template v-if="!showCurrent">
<view class="row-head candidate-toolbar"><text class="card-title">{{ candidateHeading }}</text><button v-if="stage !== 'review'" class="link-button" :disabled="loading" @tap="bootstrap">{{ loading ? '刷新中…' : '刷新' }}</button></view>
<view class="row-head candidate-toolbar" :class="{ 'pda-list-toolbar': stage !== 'review' }"><text class="card-title">{{ candidateHeading }}<text v-if="stage !== 'review' && !error"> · {{ visibleCandidates.length }} 单</text></text><button v-if="stage !== 'review'" class="link-button" :disabled="loading" @tap="bootstrap"><uni-icons type="refresh" color="#135ee8" size="18" /><text>{{ loading ? '刷新中…' : '刷新' }}</text></button></view>
<view v-if="loading && !visibleCandidates.length" class="empty-box"><uni-load-more status="loading" /><text class="empty-copy">正在加载换标任务…</text></view>
<view v-else-if="!visibleCandidates.length" class="empty-box"><uni-icons type="info" color="#8090a3" size="32" /><text class="empty-copy">{{ emptyText }}</text></view>
<view v-else-if="stage !== 'review'" class="task-list">
<view v-for="item in visibleCandidates" :key="item.relabelNo" class="task-card relabel-candidate" :class="{ current: current?.relabelNo === item.relabelNo, 'rework-card': item.reviewDecision === 'REWORK' }" @tap="openExecution(item)">
<view class="row-head"><text class="business-code">{{ item.relabelNo }}</text><text class="status-pill">{{ statusLabel(item.taskStatus || item.applicationStatus) }}</text></view>
<view class="candidate-kind"><text>{{ relabelTypeName(item.operationType) }}</text><text v-if="item.reviewDecision === 'REWORK'" class="rework-tag">退回重做</text></view>
<view class="candidate-footer"><text class="candidate-progress">已换标 <text class="candidate-progress-value">{{ item.relabelledPackages }} / {{ item.expectedPackages }}</text></text><text v-if="item.skippedPackages + item.exceptionPackages > 0" class="candidate-warning">跳过/异常 {{ item.skippedPackages + item.exceptionPackages }}</text><text class="candidate-enter">进入 ›</text></view>
<view v-else-if="stage !== 'review'" class="task-list pda-list-cards">
<view v-for="item in visibleCandidates" :key="item.relabelNo" class="task-card relabel-candidate pda-task-card" :class="{ current: current?.relabelNo === item.relabelNo, 'rework-card': item.reviewDecision === 'REWORK' }" hover-class="pda-task-card-pressed" @tap="openExecution(item)">
<view class="pda-task-head"><text class="pda-task-no">{{ item.relabelNo }}</text><text class="status-pill pda-task-status">{{ statusLabel(item.taskStatus || item.applicationStatus) }}</text></view>
<view class="candidate-kind pda-task-flags"><text>{{ relabelTypeName(item.operationType) }}</text><text v-if="item.reviewDecision === 'REWORK'" class="rework-tag">退回重做</text></view>
<view class="pda-task-metrics" :class="{ 'has-three': item.skippedPackages + item.exceptionPackages > 0 }"><view><strong>{{ item.expectedPackages }}</strong><text>应处理</text></view><view><strong>{{ item.relabelledPackages }}</strong><text>已换标</text></view><view v-if="item.skippedPackages + item.exceptionPackages > 0" class="pda-task-metric-warning"><strong>{{ item.skippedPackages + item.exceptionPackages }}</strong><text>跳过/异常</text></view></view>
<view class="pda-task-footer"><view class="pda-task-action"><text>进入</text><uni-icons type="right" color="#135ee8" size="14" /></view></view>
</view>
</view>
<view v-else class="task-list">
@@ -391,3 +392,7 @@
.summary-metrics{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:4px;margin-left:auto;font-size:9px;line-height:18px}.summary-metric{display:inline-block;padding:1px 4px;border-radius:4px;background:#dcfce7;color:#16a34a;font-weight:700;white-space:nowrap}
.candidate-toolbar{align-items:center;margin-bottom:12px}.candidate-toolbar .card-title{margin-bottom:0;font-size:16px;line-height:24px}.candidate-toolbar .link-button{display:flex;align-items:center;justify-content:center;box-sizing:border-box;min-width:88px;height:44px;min-height:44px;margin:0;padding:0 16px;border:1px solid #9dc0f4;border-radius:8px;color:#135ee8;font-size:16px;font-weight:700;line-height:normal;background:#eaf2ff}.candidate-toolbar .link-button::after{border:0}.candidate-toolbar .link-button[disabled]{color:#718095;border-color:#dce5f0;background:#e8edf3}
.relabel-candidate .business-code{min-width:0;font-size:18px;line-height:24px;overflow-wrap:anywhere}.relabel-candidate .status-pill{font-size:12px;line-height:18px}.candidate-kind{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px;color:#52647c;font-size:14px;line-height:22px}.candidate-footer{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;margin-top:10px}.candidate-progress{color:#52647c;font-size:14px;line-height:24px}.candidate-progress-value{color:#135ee8;font-size:18px;font-weight:700}.candidate-warning{color:#b54708;font-size:12px;line-height:20px}.candidate-enter{margin-left:auto;color:#135ee8;font-size:14px;font-weight:600;line-height:24px}</style>
<style scoped lang="scss">
@import '../../styles/task-list.scss';
</style>
File diff suppressed because one or more lines are too long
+89
View File
@@ -0,0 +1,89 @@
.pda-task-list {
.pda-list-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 36px;
margin: 12px 0 11px;
color: #526174;
font-size: 14px;
font-weight: 700;
line-height: 20px;
}
.pda-list-toolbar > text { min-width: 0; margin: 0; color: inherit; font-size: inherit; font-weight: inherit; line-height: inherit; }
.pda-list-toolbar button {
display: flex;
box-sizing: border-box;
align-items: center;
justify-content: center;
flex-shrink: 0;
gap: 5px;
width: auto;
min-width: 72px;
height: 36px;
min-height: 36px;
margin: 0;
padding: 0 14px;
border: 1px solid #b8d1f5;
border-radius: 20px;
color: #135ee8;
font-size: 14px;
font-weight: 700;
line-height: normal;
background: #fff;
}
.pda-list-toolbar button[disabled] { opacity: .55; }
.pda-list-cards { display: flex; flex-direction: column; gap: 11px; }
.pda-task-card {
display: block;
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 14px;
border: 1px solid #dce5f0;
border-radius: 13px;
color: #1f2937;
line-height: normal;
text-align: left;
white-space: normal;
background: #fff;
box-shadow: none;
}
.pda-task-card-pressed { border-color: #93baf0; background: #f4f8ff; }
.pda-task-card.current { border-color: #8eb8f5; background: #f7faff; }
.pda-task-card.rework-card { border-color: #f29b38; background: #fffaf2; box-shadow: none; }
.pda-task-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.pda-task-no {
display: block;
flex: 1;
min-width: 0;
color: #1f2937;
font-size: 18px;
font-weight: 750;
line-height: 26px;
overflow-wrap: anywhere;
word-break: break-all;
}
.pda-task-status { flex-shrink: 0; max-width: 45%; padding: 6px 10px; border-radius: 9px; font-size: 13px; line-height: 16px; white-space: normal; overflow-wrap: anywhere; }
.pda-task-meta { display: block; margin-top: 8px; color: #718095; font-size: 12px; line-height: 20px; overflow-wrap: anywhere; }
.pda-task-flags { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 8px; color: #526174; font-size: 12px; line-height: 20px; }
.pda-task-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 12px; padding: 9px 0; border-top: 1px solid #e1e7f0; border-bottom: 1px solid #e1e7f0; }
.pda-task-metrics.has-three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.pda-task-metrics > view { min-width: 0; padding: 0 6px; border-right: 1px solid #e1e7f0; text-align: center; }
.pda-task-metrics > view:last-child { border-right: 0; }
.pda-task-metrics strong { display: block; color: #1f2937; font-size: 22px; font-weight: 750; line-height: 26px; overflow-wrap: anywhere; }
.pda-task-metrics text { display: block; margin-top: 3px; color: #6f8194; font-size: 12px; line-height: 18px; }
.pda-task-metric-warning strong { color: #a35b0c; }
.pda-task-footer { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; min-height: 28px; margin-top: 10px; }
.pda-task-action { display: flex; align-items: center; gap: 2px; margin-left: auto; color: #135ee8; font-size: 13px; font-weight: 700; line-height: 22px; white-space: nowrap; }
.pda-task-footer .recent-badge { font-size: 11px; line-height: 16px; }
}
+1 -1
View File
@@ -91,7 +91,7 @@ test('domestic cabinet photos are read-only, previewable and non-blocking in sig
})
test('fixed receiving action leaves enough scroll-safe space above the PDA bottom bar', () => {
assert.match(page, /v-if="canFinishOnsite" class="sticky-action one"/)
assert.match(page, /v-if="canFinishOnsite" class="sticky-action-dock"><view class="sticky-action one"/)
assert.match(page, /allInboundsReconciled \? '完成整柜入库' : '现场收货结束(标记差异)'/)
assert.match(page, /receiving-content-with-action\{[^}]*padding-bottom:calc\(152px \+ env\(safe-area-inset-bottom,0px\)\)/)
assert.match(page, /primary-button,\.secondary-button,\.ghost-button\{[^}]*align-items:center;justify-content:center[^}]*line-height:normal;text-align:center/)
+1 -1
View File
@@ -30,7 +30,7 @@ test('warehouse transfer page loads detail without exposing internal identifiers
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
assert.match(page, /getWarehouseTransferDetail\(item\.transferNo\)/)
assert.match(page, /待测转运单/)
assert.match(page, /仓储转运测量列表/)
assert.match(page, /仓储转运测量/)
assert.match(page, /转运测量作业/)
assert.match(page, /!selected && entryMode === 'list'/)
assert.match(page, /entryMode === 'work' && taskNo/)
+2 -2
View File
@@ -7,7 +7,7 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
const enabled = operations.filter((operation) => operation.available)
assert.deepEqual(enabled.map(({ module, name }) => ({ module, name })), [
{ module: 'inbound', name: '拆柜收货' },
{ module: 'inbound', name: '仓储转运测量列表' }, { module: 'inbound', name: '转运测量' },
{ module: 'inbound', name: '仓储转运测量' }, { module: 'inbound', name: '转运测量' },
{ module: 'inbound', name: '退货入库' },
{ module: 'inbound', name: '合箱' },
{ module: 'inbound', name: '退货清点' },
@@ -28,7 +28,7 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
assert.equal(operations.find((item) => item.name === '退货入库')?.route, '/pages/return-receipt/return-receipt')
assert.equal(operations.find((item) => item.name === '合箱')?.route, '/pages/return-packing/return-packing')
assert.equal(operations.find((item) => item.name === '退货清点')?.route, '/pages/return-counting/return-counting')
assert.equal(operations.find((item) => item.name === '仓储转运测量列表')?.route, '/pages/warehouse-transfer/warehouse-transfer?mode=list')
assert.equal(operations.find((item) => item.name === '仓储转运测量')?.route, '/pages/warehouse-transfer/warehouse-transfer?mode=list')
assert.equal(operations.find((item) => item.name === '转运测量')?.route, '/pages/warehouse-transfer/warehouse-transfer?mode=work')
assert.equal(operations.some((item) => item.name === '库存盘点'), false)
+122
View File
@@ -0,0 +1,122 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { parse, compileScript, compileTemplate, compileStyle } from 'vue/compiler-sfc'
import { assertReceivingWarehouse } from '../src/api/receiving-adapter.js'
import { MODULES } from '../src/data/overseas-workbench.js'
const apiSource = readFileSync(new URL('../src/api/workbench.js', import.meta.url), 'utf8')
const homeSource = readFileSync(new URL('../src/pages/home/home.vue', import.meta.url), 'utf8')
const profile = { id: 'OP', warehouseId: 'WH', warehouseCode: 'WH-CODE', warehouseName: '测试仓' }
const emptySources = () => ({
assertReceivingWarehouse,
listReceivingCandidates: async () => [], listWarehouseTransferCandidates: async () => [],
listReturnCountTasks: async () => ({ records: [], total: 0 }), listPackingPlans: async () => [],
listDispatchTasks: async () => [], listOutboundTasks: async () => [], listCargoDiscardTasks: async () => [],
listRelabelCandidates: async () => []
})
function api(overrides = {}) {
const dependencies = { ...emptySources(), ...overrides }
return new Function(...Object.keys(dependencies), apiSource.replace(/^import .*$/gm, '').replace(/^export /gm, '') + '\nreturn { loadWorkbenchCounts, workbenchBadge, WORKBENCH_COUNT_KEYS }')(...Object.values(dependencies))
}
function home(loadWorkbenchCounts) {
const session = { currentUser: { ...profile }, apiBase: 'LOCAL' }
const scopes = []
const dependencies = { PortalHeader: {}, MODULES, session, requireSession: () => true, applyWarehouseProfile: value => value, getCurrentWarehouseProfile: async () => session.currentUser, loadWorkbenchCounts, workbenchBadge: api().workbenchBadge, restoreWarehouseWork: scope => scopes.push(scope), warehouseWork: { warehouseTransfer: { currentNo: 'T2' } } }
const page = new Function(...Object.keys(dependencies), homeSource.match(/<script>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/gm, '').replace('export default', 'return'))(...Object.values(dependencies))
const value = page.data()
for (const [key, method] of Object.entries(page.methods)) value[key] = method.bind(value)
return { page, value, session, scopes }
}
test('workbench counts separate queues, current work and paginated totals without duplicate business records', async () => {
const container = { sourceContainerBookingId: 'B1', containerNo: 'C1', overseasWarehouseCode: profile.warehouseCode, overseasWarehouseName: profile.warehouseName }
const source = api({
listReceivingCandidates: async () => [container, container, { ...container, sourceContainerBookingId: 'B2', containerNo: 'C2', operable: false }],
listWarehouseTransferCandidates: async () => [{ transferNo: 'T1', status: 'WAIT_PDA_RECEIVING' }, { transferNo: 'T2', status: 'PARTIALLY_RECEIVED' }, { transferNo: 'T2', status: 'PARTIALLY_RECEIVED' }, { transferNo: 'T3', status: 'COMPLETED' }],
listReturnCountTasks: async options => { assert.deepEqual(options, { page: 1, size: 1 }); return { total: 37, records: [{ receiptNo: 'RR1' }] } },
listPackingPlans: async () => [{ planNo: 'P1', status: 'READY' }, { planNo: 'P1', status: 'READY' }, { planNo: 'P2', status: 'COMPLETED' }],
listDispatchTasks: async () => [{ dispatchNo: 'D1', stage: 'LOADING' }, { dispatchNo: 'D1', stage: 'LOADING' }, { dispatchNo: 'D2', stage: 'BEFORE_LOADING' }, { dispatchNo: 'D3', stage: 'DELIVERING' }, { dispatchNo: 'D4', stage: 'DELIVERED' }, { dispatchNo: 'D5', stage: 'LOADING', status: 'CANCELLED' }],
listOutboundTasks: async status => { assert.equal(status, 'PENDING'); return [{ taskNo: 'O1' }, { taskNo: 'O1' }] },
listCargoDiscardTasks: async () => [{ discardNo: 'CD1', status: 'PENDING' }, { discardNo: 'CD1', status: 'PENDING' }, { discardNo: 'CD2', status: 'COMPLETED' }],
listRelabelCandidates: async () => [{ relabelNo: 'R1', taskStatus: 'AVAILABLE' }, { relabelNo: 'R1', taskStatus: 'AVAILABLE' }, { relabelNo: 'R2', taskStatus: 'PROCESSING', claimedByCurrentOperator: true }, { relabelNo: 'R3', taskStatus: 'PROCESSING', claimedByCurrentOperator: false }, { relabelNo: 'R4', taskStatus: 'WAIT_REVIEW' }, { relabelNo: 'R5', taskStatus: 'COMPLETED' }]
})
const counts = await source.loadWorkbenchCounts({ profile, activeTransferNo: 'T2' })
assert.deepEqual(Object.fromEntries(Object.entries(counts).map(([key, value]) => [key, value.count])), { receiving: 2, warehouseTransferList: 2, warehouseTransferWork: 1, returnCounting: 37, returnPacking: 1, dispatchPicking: 2, outboundPicking: 1, relabelReceive: 1, relabelExecute: 1, relabelReview: 1 })
assert.equal(counts.outboundPicking.discardCount, 1)
assert.equal(source.workbenchBadge('outboundPicking', counts.outboundPicking).label, '1 出库 · 1 废弃')
})
test('a failed source stays unknown while other queue counts remain available', async () => {
const error = Object.assign(new Error('网络异常'), { requestId: 'REQ-COUNT', traceId: 'TRACE-COUNT' })
const source = api({ listWarehouseTransferCandidates: async () => { throw error } })
const counts = await source.loadWorkbenchCounts({ profile })
assert.equal(counts.warehouseTransferList.status, 'error')
assert.equal(counts.warehouseTransferWork.error, error)
assert.equal(counts.warehouseTransferList.count, undefined)
assert.equal(counts.receiving.count, 0)
assert.equal(source.workbenchBadge('warehouseTransferList', counts.warehouseTransferList).label, '获取失败')
assert.equal(source.workbenchBadge('receiving', counts.receiving).label, '0 柜')
})
test('foreign receiving warehouses and absent pagination totals cannot produce a success count', async () => {
const source = api({ listReceivingCandidates: async () => [{ overseasWarehouseCode: 'OTHER', overseasWarehouseName: '其他仓' }], listReturnCountTasks: async () => ({ records: [] }) })
const counts = await source.loadWorkbenchCounts({ profile })
assert.equal(counts.receiving.status, 'error')
assert.equal(counts.receiving.error.code, 'RECEIVING_WAREHOUSE_MISMATCH')
assert.equal(counts.returnCounting.status, 'error')
assert.equal(counts.returnCounting.count, undefined)
})
test('a completed or missing local transfer does not count as current measurement work', async () => {
const source = api({ listWarehouseTransferCandidates: async () => [{ transferNo: 'DONE', status: 'COMPLETED' }, { transferNo: 'OTHER', status: 'WAIT_PDA_RECEIVING' }] })
const counts = await source.loadWorkbenchCounts({ profile, activeTransferNo: 'DONE' })
assert.equal(counts.warehouseTransferWork.count, 0)
assert.equal(counts.warehouseTransferList.count, 1)
assert.equal(source.workbenchBadge('warehouseTransferWork', counts.warehouseTransferWork).label, '未开始')
})
test('scan tools and queries do not pretend to have task counts', () => {
const { workbenchBadge } = api()
assert.equal(workbenchBadge('returnReceipt').label, '扫码签收')
assert.equal(workbenchBadge('move').label, '扫码作业')
assert.equal(workbenchBadge('inventoryQuery').label, '扫码查询')
assert.equal(workbenchBadge('receiving').label, '加载中…')
})
test('returning home refreshes counts and ignores a response from a hidden page', async () => {
let resolveFirst, calls = 0
const { page, value, scopes } = home(async ({ activeTransferNo }) => { assert.equal(activeTransferNo, 'T2'); calls++; return calls === 1 ? new Promise(resolve => { resolveFirst = resolve }) : { receiving: { status: 'ready', count: 4, unit: '柜' } } })
const first = page.onShow.call(value)
assert.equal(value.countsLoading, true)
page.onHide.call(value)
resolveFirst({ receiving: { status: 'ready', count: 99, unit: '柜' } })
await first
assert.deepEqual(value.counts, {})
await page.onShow.call(value)
assert.equal(value.counts.receiving.count, 4)
assert.equal(calls, 2)
assert.deepEqual(scopes, ['OP:WH', 'OP:WH'])
})
test('warehouse or account changes invalidate pending home counts', async () => {
let resolve
const { page, value, session } = home(() => new Promise(done => { resolve = done }))
const pending = page.onShow.call(value)
session.currentUser = { ...profile, id: 'OTHER-OP', warehouseId: 'OTHER-WH' }
resolve({ receiving: { status: 'ready', count: 99, unit: '柜' } })
await pending
assert.deepEqual(value.counts, {})
assert.equal(value.countsLoading, false)
assert.equal(value.user.id, 'OTHER-OP')
})
test('home compiles with per-entry badges and no function-count summary', () => {
const { descriptor, errors } = parse(homeSource, { filename: 'home.vue' })
assert.deepEqual(errors, [])
compileScript(descriptor, { id: 'home-counts' })
assert.deepEqual(compileTemplate({ source: descriptor.template.content, filename: 'home.vue', id: 'home-counts' }).errors, [])
for (const style of descriptor.styles) assert.deepEqual(compileStyle({ source: style.content, filename: 'home.vue', id: 'data-v-home-counts', scoped: style.scoped }).errors, [])
assert.doesNotMatch(descriptor.template.content, /availableCount|项可处理|group\.operations\.length/)
assert.match(descriptor.template.content, /countBadge\(operation\)\.label/)
})