56 lines
8.0 KiB
Vue
56 lines
8.0 KiB
Vue
<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">
|
||
<view class="work-heading"><text class="work-title">{{ title }}</text></view>
|
||
<view class="work-card"><ScanTargetInput name="query" v-model="code" :label="label" :placeholder="placeholder" :active="true" @submit="search" /><button class="primary" :disabled="!code.trim() || loading" @tap="search(code)">{{ loading ? '查询中…' : '查询' }}</button></view>
|
||
<view v-if="loading" class="empty-box"><uni-load-more status="loading" /><text class="empty-copy">正在读取最新库存事实…</text></view>
|
||
<template v-else-if="searched && mode === 'inventory'">
|
||
<view v-if="!inventoryItems.length" class="empty-box"><uni-icons type="info" color="#8090a3" size="32" /><text class="empty-copy">未找到与 {{ result?.queryCode || code }} 匹配的库存</text></view>
|
||
<view v-else class="item-list"><view v-for="(item,index) in inventoryItems" :key="`${item.palletCode}:${item.locationCode}:${item.skuCode}:${index}`" class="item-card"><view class="row-head"><view><text class="business-code">{{ item.skuCode || 'SKU 未关联' }}</text><text class="meta-line">{{ item.productName || '商品名称未关联' }}</text></view><text class="status-pill">{{ statusLabel(item.status) }}</text></view><text class="meta-line">入仓码:{{ item.inboundCode || '—' }}</text><text class="meta-line">箱码:{{ item.cartonMark || '—' }}</text><text class="meta-line">托盘:{{ item.palletCode || '未绑定' }} · 库位:{{ item.locationCode || '未绑定' }}</text><view class="metric-row"><view class="metric"><strong>{{ item.availablePackages }}</strong><text>可用</text></view><view class="metric"><strong>{{ item.holdPackages }}</strong><text>冻结</text></view><view class="metric"><strong>{{ item.receivingPackages }}</strong><text>收货中</text></view></view></view></view>
|
||
</template>
|
||
<view v-else-if="searched && mode === 'location' && result" class="work-card"><view class="row-head"><view><text class="business-code">{{ result.locationCode }}</text><text class="meta-line">{{ locationTypeLabel(result.locationType) }}</text></view><text class="status-pill">{{ statusLabel(result.status) }}</text></view><view class="metric-row"><view class="metric"><strong>{{ result.occupiedPackages }}</strong><text>在库箱数</text></view><view class="metric"><strong>{{ result.inventoryPositionCount }}</strong><text>库存位置</text></view><view class="metric"><strong>{{ result.maxLoadKg || '—' }}</strong><text>最大载重 kg</text></view></view></view>
|
||
<template v-else-if="searched && mode === 'pallet' && result"><view class="work-card"><view class="row-head"><view><text class="business-code">{{ result.palletCode }}</text><text class="meta-line">库位:{{ result.locationCode || '未绑定' }}</text></view><text class="status-pill">{{ statusLabel(result.status) }}</text></view><view class="metric-row"><view class="metric"><strong>{{ result.occupiedPackages }}</strong><text>在托箱数</text></view><view class="metric"><strong>{{ result.inventoryPositionCount }}</strong><text>库存位置</text></view></view></view><view v-if="!palletItems.length" class="empty-box"><text class="empty-copy">当前托盘未绑定库存</text></view><view v-else class="item-list"><view v-for="(item,index) in palletItems" :key="`${item.skuCode}:${item.inboundCode}:${index}`" class="item-card"><text class="business-code">{{ item.skuCode || 'SKU 未关联' }}</text><text class="meta-line">{{ item.productName || '商品名称未关联' }}</text><text class="meta-line">{{ item.inboundCode || '无入仓码' }} · {{ item.cartonMark || '无箱码' }}</text><view class="metric-row"><view class="metric"><strong>{{ item.availablePackages }}</strong><text>可用</text></view><view class="metric"><strong>{{ item.holdPackages }}</strong><text>冻结</text></view><view class="metric"><strong>{{ item.receivingPackages }}</strong><text>收货中</text></view></view></view></view></template>
|
||
<view v-if="error" class="error-box" role="alert"><text class="error-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><button class="secondary" @tap="search(code)">重新查询</button></view>
|
||
</view></scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import PortalHeader from '@/components/PortalHeader.vue'
|
||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||
import { resolveInventory, resolveWarehouseLocation, resolveWarehousePallet } from '@/api/warehouse-operations.js'
|
||
import { session, requireSession } from '@/stores/session.js'
|
||
import { restoreWarehouseWork, updateWarehouseWork, warehouseWork } from '@/stores/warehouse-work.js'
|
||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||
|
||
const MODES = {
|
||
inventoryQuery: { mode: 'inventory', title: '库存查询', label: '商品码或箱码', placeholder: '扫描或输入 SKU / 箱码' },
|
||
locationQuery: { mode: 'location', title: '库位查询', label: '库位码', placeholder: '扫描或输入库位码' },
|
||
palletQuery: { mode: 'pallet', title: '托盘查询', label: '托盘码', placeholder: '扫描或输入托盘码' }
|
||
}
|
||
export default {
|
||
components: { PortalHeader, ScanTargetInput },
|
||
data() { return { user: session.currentUser, operationKey: 'inventoryQuery', mode: 'inventory', title: '库存查询', label: '商品码或箱码', placeholder: '扫描或输入 SKU / 箱码', loading: false, searched: false, result: null, error: null } },
|
||
computed: {
|
||
storeField() { return `${this.mode}Code` },
|
||
code: { get() { return warehouseWork.query[this.storeField] || '' }, set(value) { updateWarehouseWork('query', { [this.storeField]: value }) } },
|
||
inventoryItems() { return Array.isArray(this.result?.items) ? this.result.items : [] }, palletItems() { return Array.isArray(this.result?.inventory) ? this.result.inventory : [] }
|
||
},
|
||
onLoad(options) { this.operationKey = options.op || 'inventoryQuery'; Object.assign(this, MODES[this.operationKey] || MODES.inventoryQuery) },
|
||
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); this.startScanner() },
|
||
onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() },
|
||
methods: {
|
||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => { this.code = value; this.search(value) }, onError: (error) => this.setError(error, '扫码广播注册失败') }) },
|
||
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
|
||
async search(raw) { const code = String(raw?.detail?.value ?? raw ?? this.code).trim(); if (!code || this.loading) return; this.code = code; this.loading = true; this.error = null; this.result = null; this.searched = false; try { if (this.mode === 'location') this.result = await resolveWarehouseLocation(code); else if (this.mode === 'pallet') this.result = await resolveWarehousePallet(code); else this.result = await resolveInventory(code); this.searched = true; playSuccessSound() } catch (error) { this.searched = true; this.setError(error, `${this.title}失败`) } finally { this.loading = false } },
|
||
setError(error, fallback) { this.error = { message: error?.message || fallback, code: error?.code, requestId: error?.requestId, traceId: error?.traceId }; playErrorSound() },
|
||
statusLabel(value) { return ({ AVAILABLE: '可用', ACTIVE: '可用', IDLE: '空闲', OCCUPIED: '已占用', HOLD: '冻结', LOCKED: '已锁定' })[value] || value || '状态未知' },
|
||
locationTypeLabel(value) { return ({ STORAGE: '存储库位', STAGING: '暂存库位', RELABEL: '换标库位', RECEIVING: '收货库位' })[value] || value || '库位类型未标注' }
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">@import '../warehouse/operations.scss';</style>
|