@@ -0,0 +1,183 @@
< template >
< section class = "goods-page" >
< el-alert v-if = "lastError" :title="lastError" type="error" show-icon closable @close="lastError = ''" / >
< section class = "goods-metrics" >
< span > < small > 门店商品 < / small > < strong > { { listings . length } } < / strong > < / span >
< span > < small > 低库存 < / small > < strong > { { lowStockCount } } < / strong > < / span >
< span > < small > 待处理订单 < / small > < strong > { { pendingOrderCount } } < / strong > < / span >
< span > < small > 有效寄存 < / small > < strong > { { activeStorageCount } } < / strong > < / span >
< / section >
< section class = "panel" >
< header class = "panel-toolbar goods-toolbar" >
< div > < p class = "section-kicker" > M09 - D4 · 商品运营 < / p > < h3 > 商品 、 库存 、 订单与寄存 < / h3 > < / div >
< div class = "toolbar-actions" >
< el-select v-model = "storeId" filterable placeholder="选择授权门店" @change="changeStore" >
< el -option v-for = "store in stores" :key="store.id" :label="store.name" :value="store.id" / >
< / el-select >
< el-button :icon = "RefreshCw" :loading = "loading" @click ="loadAll" > 刷新 < / el -button >
< / div >
< / header >
< el-tabs v-model = "activeTab" >
< el -tab -pane label = "商品目录" name = "catalog" :disabled = "!canCatalogRead" >
< el-table v-loading = "loading" :data="listings" row-key="id" class="data-table" >
< el -table -column label = "商品" min -width = " 220 " > < template # default = "{ row }" > < div class = "stack" > < strong > { { row . productName } } < / strong > < span > { { row . productCode } } · { { row . categoryName } } < / span > < / div > < / template > < / el-table-column >
< el-table-column label = "履约" width = "150" > < template # default = "{ row }" > < el-tag effect = "plain" > { { fulfillmentLabel ( row . fulfillmentMode ) } } < / el-tag > < / template > < / el-table-column >
< el-table-column label = "状态" width = "110" > < template # default = "{ row }" > < el-tag : type = "row.status === 'ACTIVE' ? 'success' : 'info'" > { { row . status === 'ACTIVE' ? '上架' : '下架' } } < / el-tag > < / template > < / el-table-column >
< el-table-column label = "售卖时段" min -width = " 230 " > < template # default = "{ row }" > { { rangeText ( row . salesStartAt , row . salesEndAt ) } } < / template > < / el-table-column >
< el-table-column prop = "version" label = "版本" width = "90" / >
< / el-table >
< el-empty v-if = "!loading && !listings.length" description="当前门店暂无商品上架配置" / >
< / el-tab-pane >
< el-tab-pane : label = "`库存 (${stocks.total})`" name = "inventory" :disabled = "!canInventoryRead" >
< div class = "tab-tools" > < el-input v-model = "stockSearch" clearable placeholder="搜索商品或 SKU" @keyup.enter="loadStocks" /><el-button @click="loadStocks" > 查询 < / el -button > < / div >
< el-table v-loading = "loading" :data="stocks.items" row-key="id" class="data-table" >
< el -table -column label = "商品 / SKU" min -width = " 230 " > < template # default = "{ row }" > < div class = "stack" > < strong > { { row . productName } } · { { row . skuName } } < / strong > < span > { { row . productCode } } / { { row . skuCode } } < / span > < / div > < / template > < / el-table-column >
< el-table-column label = "策略" width = "120" > < template # default = "{ row }" > < el-tag effect = "plain" > { { row . policyType } } < / el-tag > < / template > < / el-table-column >
< el-table-column prop = "availableQuantity" label = "可用" width = "90" / >
< el-table-column prop = "lockedQuantity" label = "锁定" width = "90" / >
< el-table-column prop = "lossQuantity" label = "损耗" width = "90" / >
< el-table-column label = "预警" width = "100" > < template # default = "{ row }" > < el-tag : type = "row.lowStock ? 'danger' : 'success'" > { { row . lowStock ? '低库存' : '正常' } } < / el-tag > < / template > < / el-table-column >
< el-table-column v-if = "canInventoryWrite" label="操作" min-width="245" fixed="right"><template #default="{ row }"><div class="row-actions"><el-button size="small" @click="inventoryQuantity(row, 'inbound')">入库</el-button><el-button size="small" @click="inventoryQuantity(row, 'loss')">损耗</el-button><el-button size="small" @click="stocktake(row)" > 盘点 < / el -button > < / div > < / template > < / el-table-column >
< / el-table >
< / el-tab-pane >
< el-tab-pane : label = "`商品订单 (${orders.total})`" name = "orders" :disabled = "!canOrderRead" >
< div class = "tab-tools" > < el-select v-model = "orderStatus" clearable placeholder="全部状态" @change="loadOrders"><el-option v-for="status in orderStatuses" :key="status" :label="statusLabel(status)" :value="status" /></el-select><el-button @click="loadOrders" > 刷新订单 < / el -button > < / div >
< el-table v-loading = "loading" :data="orders.items" row-key="id" class="data-table" >
< el -table -column label = "订单" min -width = " 200 " > < template # default = "{ row }" > < div class = "stack" > < strong > { { row . orderNo } } < / strong > < span > 会员 # { { row . memberId || '-' } } · { { dateText ( row . createdAt ) } } < / span > < / div > < / template > < / el-table-column >
< el-table-column label = "状态" width = "150" > < template # default = "{ row }" > < el-tag :type = "orderStatusType(row.status)" > { { statusLabel ( row . status ) } } < / el-tag > < / template > < / el-table-column >
< el-table-column label = "履约" width = "130" > < template # default = "{ row }" > { { fulfillmentLabel ( row . fulfillmentMode ) } } < / template > < / el-table-column >
< el-table-column label = "数量 / 金额" min -width = " 150 " > < template # default = "{ row }" > { { row . totalQuantity } } 件 · { { money ( row . totalAmountCents ) } } < / template > < / el-table-column >
< el-table-column label = "操作" min -width = " 300 " fixed = "right" > < template # default = "{ row }" > < div class = "row-actions" > < el-button size = "small" @click ="showOrder(row)" > 详情 < / el -button > < template v-if = "canOrderWrite"><el-button v-for="action in orderActions(row)" :key="action" size="small" type="primary" plain @click="actOnOrder(row, action)" > {{ actionLabel ( action ) }} < / el -button > < / template > < / div > < / template > < / el-table-column >
< / el-table >
< / el-tab-pane >
< el-tab-pane : label = "`寄存 (${storages.total})`" name = "storages" :disabled = "!canStorageRead" >
< div class = "tab-tools" > < el-select v-model = "storageStatus" clearable placeholder="全部状态" @change="loadStorages"><el-option v-for="status in storageStatuses" :key="status" :label="storageStatusLabel(status)" :value="status" /></el-select><el-button v-if="canStorageWrite" type="primary" @click="openCreateStorage">从订单寄存</el-button><el-button @click="loadStorages" > 刷新寄存 < / el -button > < / div >
< el-table v-loading = "loading" :data="storages.items" row-key="id" class="data-table" >
< el -table -column label = "寄存单" min -width = " 210 " > < template # default = "{ row }" > < div class = "stack" > < strong > { { row . storageNo } } < / strong > < span > 会员 # { { row . memberId } } · { { row . sourceType === 'ORDER' ? ` 订单 # ${ row . sourceOrderId } ` : '员工手工创建' } } < / span > < / div > < / template > < / el-table-column >
< el-table-column label = "状态" width = "150" > < template # default = "{ row }" > < el-tag :type = "storageStatusType(row.status)" > { { storageStatusLabel ( row . status ) } } < / el-tag > < / template > < / el-table-column >
< el-table-column label = "数量" width = "130" > < template # default = "{ row }" > { { row . remainingQuantity } } / { { row . totalQuantity } } < / template > < / el-table-column >
< el-table-column label = "到期" min -width = " 165 " > < template # default = "{ row }" > { { dateText ( row . expiresAt ) } } < / template > < / el-table-column >
< el-table-column label = "凭证" width = "120" > < template # default = "{ row }" > 第 { { row . claimCredentialVersion } } 版 < / template > < / el-table-column >
< el-table-column label = "操作" min -width = " 320 " fixed = "right" > < template # default = "{ row }" > < div class = "row-actions" > < el-button size = "small" @click ="showStorage(row)" > 详情 < / el -button > < template v-if = "canStorageWrite && isStorageActive(row.status)"><el-button size="small" type="primary" plain @click="openRetrieve(row)">取出</el-button><el-button size="small" @click="rotateStorage(row)">换码</el-button><el-button size="small" type="danger" plain @click="cancelStorage(row)" > 取消 < / el -button > < / template > < / div > < / template > < / el-table-column >
< / el-table >
< / el-tab-pane >
< / el-tabs >
< / section >
< el-drawer v-model = "detailDrawer" :title="detailTitle" size="min(720px, 96vw)" >
< template v-if = "selectedOrder"><el-descriptions :column="2" border><el-descriptions-item label="订单号">{{ selectedOrder.orderNo }}</el-descriptions-item><el-descriptions-item label="状态">{{ statusLabel(selectedOrder.status) }}</el-descriptions-item><el-descriptions-item label="会员">#{{ selectedOrder.memberId }}</el-descriptions-item><el-descriptions-item label="金额">{{ money(selectedOrder.totalAmountCents) }}</el-descriptions-item></el-descriptions><el-table :data="selectedOrder.items || []"><el-table-column prop="productName" label="商品" /><el-table-column prop="skuName" label="规格" /><el-table-column prop="quantity" label="数量" width="80" /><el-table-column label="小计" width="110"><template #default="{ row }" > {{ money ( row.subtotalCents ) }} < / template > < / el -table -column > < / el-table > < / template >
< template v-if = "selectedStorage"><el-descriptions :column="2" border><el-descriptions-item label="寄存单">{{ selectedStorage.storageNo }}</el-descriptions-item><el-descriptions-item label="状态">{{ storageStatusLabel(selectedStorage.status) }}</el-descriptions-item><el-descriptions-item label="寄存人">#{{ selectedStorage.memberId }}</el-descriptions-item><el-descriptions-item label="到期">{{ dateText(selectedStorage.expiresAt) }}</el-descriptions-item></el-descriptions><el-table :data="selectedStorage.items || []"><el-table-column prop="productName" label="商品" /><el-table-column prop="skuName" label="规格" /><el-table-column prop="totalQuantity" label="寄存" width="80" /><el-table-column prop="remainingQuantity" label="剩余" width="80" /></el-table><h4>取出流水</h4><el-table :data="selectedStorage.movements || []"><el-table-column prop="storageItemId" label="明细" /><el-table-column prop="quantity" label="取出" width="80" /><el-table-column prop="remainingAfter" label="取出后" width="90" / > < / el-table > < / template >
< / el-drawer >
< el-dialog v-model = "retrieveDialog" title="核验凭证并取出商品" width="min(560px, 94vw)"><el-alert title="领取码只用于本次核验;后台不会读取或展示数据库中的凭证哈希。" type="warning" show-icon :closable="false" /><el-form label-position="top"><el-form-item label="寄存明细"><el-select v-model="retrieveForm.storageItemId"><el-option v-for="item in retrieveStorage?.items || []" :key="item.id" :label="`${item.productName} · ${item.skuName}(剩余 ${item.remainingQuantity}) `" :value="item.id" /></el-select></el-form-item><el-form-item label="取出数量"><el-input-number v-model="retrieveForm.quantity" :min="1" :max="selectedRetrieveItem?.remainingQuantity || 1" /></el-form-item><el-form-item label="一次性领取码"><el-input v-model="retrieveForm.credential" type="password" show-password autocomplete="off" /></el-form-item></el-form><template #footer><el-button @click="retrieveDialog = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitRetrieve" > 确认取出 < / el -button > < / template > < / el-dialog >
< el-dialog v-model = "createStorageDialog" title="从已完成订单创建寄存" width="min(520px, 94vw)"><el-form label-position="top"><el-form-item label="商品订单 ID"><el-input v-model="createStorageForm.orderId" /></el-form-item><el-form-item label="到期时间"><el-date-picker v-model="createStorageForm.expiresAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss.SSSZ" /></el-form-item></el-form><template #footer><el-button @click="createStorageDialog = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitCreateStorage" > 创建寄存 < / el -button > < / template > < / el-dialog >
< / section >
< / template >
< script setup lang = "ts" >
import { computed , reactive , ref , watch } from 'vue' ;
import { ElMessage , ElMessageBox } from 'element-plus' ;
import { RefreshCw } from '@lucide/vue' ;
import {
ApiError , createOrderProductStorage , getProductOrder , getProductStorage ,
listInventoryStocks , listManagedStores , listProductListings , listProductOrders ,
listProductStorages , mutateInventory , runProductOrderAction , runProductStorageAction ,
type ApiSession
} from '../api' ;
import { adminSessionState } from '../session' ;
import type { InventoryStock , ManagedStore , PageResult , ProductListing , ProductOrder , ProductStorage } from '../types' ;
const props = defineProps < { session : ApiSession } > ( ) ;
const stores = ref < ManagedStore [ ] > ( [ ] ) ; const storeId = ref ( '' ) ; const activeTab = ref ( 'catalog' ) ;
const listings = ref < ProductListing [ ] > ( [ ] ) ; const stocks = reactive < PageResult < InventoryStock > > ( { items : [ ] , total : 0 , page : 1 , pageSize : 100 } ) ;
const orders = reactive < PageResult < ProductOrder > > ( { items : [ ] , total : 0 , page : 1 , pageSize : 50 } ) ;
const storages = reactive < PageResult < ProductStorage > > ( { items : [ ] , total : 0 , page : 1 , pageSize : 50 } ) ;
const loading = ref ( false ) ; const saving = ref ( false ) ; const lastError = ref ( '' ) ; const stockSearch = ref ( '' ) ;
const orderStatus = ref ( '' ) ; const storageStatus = ref ( '' ) ; const detailDrawer = ref ( false ) ;
const selectedOrder = ref < ProductOrder | null > ( null ) ; const selectedStorage = ref < ProductStorage | null > ( null ) ;
const retrieveDialog = ref ( false ) ; const retrieveStorage = ref < ProductStorage | null > ( null ) ;
const retrieveForm = reactive ( { storageItemId : '' , quantity : 1 , credential : '' } ) ;
const createStorageDialog = ref ( false ) ; const createStorageForm = reactive ( { orderId : '' , expiresAt : '' } ) ;
const orderStatuses = [ 'PENDING_PAYMENT' , 'PAID' , 'ACCEPTED' , 'DELIVERING' , 'READY_FOR_SELF_SERVICE' , 'COMPLETED' , 'CANCELLED' , 'REFUNDING' , 'REFUNDED' , 'REFUND_FAILED' ] ;
const storageStatuses = [ 'STORED' , 'PARTIALLY_RETRIEVED' , 'RETRIEVED' , 'EXPIRED' , 'CANCELLED' ] ;
const lowStockCount = computed ( ( ) => stocks . items . filter ( ( item ) => item . lowStock ) . length ) ;
const pendingOrderCount = computed ( ( ) => orders . items . filter ( ( item ) => [ 'PAID' , 'ACCEPTED' , 'DELIVERING' , 'READY_FOR_SELF_SERVICE' , 'REFUNDING' ] . includes ( item . status ) ) . length ) ;
const activeStorageCount = computed ( ( ) => storages . items . filter ( ( item ) => isStorageActive ( item . status ) ) . length ) ;
const hasAccess = ( ... required : string [ ] ) => adminSessionState . access . roles . includes ( 'PLATFORM_ADMIN' )
|| adminSessionState . access . capabilities . some ( ( item ) => [ ... required , 'tenant.manage' , 'platform.manage' ] . includes ( item ) ) ;
const canCatalogRead = computed ( ( ) => hasAccess ( 'product.catalog.read' , 'product.catalog.write' ) ) ;
const canInventoryRead = computed ( ( ) => hasAccess ( 'inventory.read' , 'inventory.adjust' ) ) ;
const canInventoryWrite = computed ( ( ) => hasAccess ( 'inventory.adjust' ) ) ;
const canOrderRead = computed ( ( ) => hasAccess ( 'goods.order.read' , 'goods.order.manage' ) ) ;
const canOrderWrite = computed ( ( ) => hasAccess ( 'goods.order.manage' ) ) ;
const canStorageRead = computed ( ( ) => hasAccess ( 'goods.storage.read' , 'goods.storage.manage' ) ) ;
const canStorageWrite = computed ( ( ) => hasAccess ( 'goods.storage.manage' ) ) ;
const selectedRetrieveItem = computed ( ( ) => retrieveStorage . value ? . items ? . find ( ( item ) => item . id === retrieveForm . storageItemId ) ) ;
const detailTitle = computed ( ( ) => selectedOrder . value ? ` 商品订单 ${ selectedOrder . value . orderNo } ` : selectedStorage . value ? ` 寄存单 ${ selectedStorage . value . storageNo } ` : '业务详情' ) ;
async function capture < T > ( work : ( ) => Promise < T > ) { lastError . value = '' ; try { return await work ( ) ; } catch ( error ) { lastError . value = error instanceof ApiError ? ` ${ error . code } ${ error . traceId ? ` · ${ error . traceId } ` : '' } ` : error instanceof Error ? error . message : '操作失败' ; throw error ; } }
async function loadWorkspace ( ) { if ( ! props . session . token ) return ; loading . value = true ; try { stores . value = await capture ( ( ) => listManagedStores ( props . session ) ) ; storeId . value || = stores . value [ 0 ] ? . id || '' ; activeTab . value = canCatalogRead . value ? 'catalog' : canInventoryRead . value ? 'inventory' : canOrderRead . value ? 'orders' : 'storages' ; await loadAll ( ) ; } catch { /* displayed */ } finally { loading . value = false ; } }
async function changeStore ( ) { await loadAll ( ) ; }
async function loadAll ( ) { if ( ! storeId . value ) return ; loading . value = true ; try { const [ nextListings , nextStocks , nextOrders , nextStorages ] = await Promise . all ( [ canCatalogRead . value ? capture ( ( ) => listProductListings ( props . session , storeId . value ) ) : Promise . resolve ( [ ] as ProductListing [ ] ) , canInventoryRead . value ? capture ( ( ) => listInventoryStocks ( props . session , { storeId : storeId . value , pageSize : 100 } ) ) : Promise . resolve ( { items : [ ] , total : 0 , page : 1 , pageSize : 100 } as PageResult < InventoryStock > ) , canOrderRead . value ? capture ( ( ) => listProductOrders ( props . session , { storeId : storeId . value , pageSize : 50 } ) ) : Promise . resolve ( { items : [ ] , total : 0 , page : 1 , pageSize : 50 } as PageResult < ProductOrder > ) , canStorageRead . value ? capture ( ( ) => listProductStorages ( props . session , { storeId : storeId . value , pageSize : 50 } ) ) : Promise . resolve ( { items : [ ] , total : 0 , page : 1 , pageSize : 50 } as PageResult < ProductStorage > ) ] ) ; listings . value = nextListings ; Object . assign ( stocks , nextStocks ) ; Object . assign ( orders , nextOrders ) ; Object . assign ( storages , nextStorages ) ; } catch { /* displayed */ } finally { loading . value = false ; } }
async function loadStocks ( ) { if ( ! storeId . value || ! canInventoryRead . value ) return ; loading . value = true ; try { Object . assign ( stocks , await capture ( ( ) => listInventoryStocks ( props . session , { storeId : storeId . value , pageSize : 100 , search : stockSearch . value . trim ( ) || undefined } ) ) ) ; } catch { /* displayed */ } finally { loading . value = false ; } }
async function loadOrders ( ) { if ( ! storeId . value || ! canOrderRead . value ) return ; loading . value = true ; try { Object . assign ( orders , await capture ( ( ) => listProductOrders ( props . session , { storeId : storeId . value , pageSize : 50 , status : orderStatus . value || undefined } ) ) ) ; } catch { /* displayed */ } finally { loading . value = false ; } }
async function loadStorages ( ) { if ( ! storeId . value || ! canStorageRead . value ) return ; loading . value = true ; try { Object . assign ( storages , await capture ( ( ) => listProductStorages ( props . session , { storeId : storeId . value , pageSize : 50 , status : storageStatus . value || undefined } ) ) ) ; } catch { /* displayed */ } finally { loading . value = false ; } }
async function inventoryQuantity ( row : InventoryStock , action : 'inbound' | 'loss' ) { try { const { value } = await ElMessageBox . prompt ( ` 当前可用 ${ row . availableQuantity } ,请输入 ${ action === 'inbound' ? '入库' : '损耗' } 数量 ` , ` ${ row . productName } · ${ row . skuName } ` , { inputPattern : /^[1-9]\d{0,8}$/ , inputErrorMessage : '请输入正整数' } ) ; await capture ( ( ) => mutateInventory ( props . session , row . skuId , action , { storeId : storeId . value , requestId : requestId ( ` admin- ${ action } ` ) , reason : action === 'inbound' ? '运营后台商品入库' : '运营后台登记损耗' , expectedVersion : row . version , quantity : Number ( value ) } ) ) ; ElMessage . success ( '库存已更新' ) ; await loadStocks ( ) ; } catch ( error ) { if ( error !== 'cancel' && error !== 'close' ) { /* displayed */ } } }
async function stocktake ( row : InventoryStock ) { try { const { value } = await ElMessageBox . prompt ( ` 当前可用 ${ row . availableQuantity } ,请输入盘点后的可用数量 ` , ` ${ row . productName } · ${ row . skuName } ` , { inputPattern : /^\d{1,9}$/ , inputErrorMessage : '请输入非负整数' } ) ; await capture ( ( ) => mutateInventory ( props . session , row . skuId , 'stocktake' , { storeId : storeId . value , requestId : requestId ( 'admin-stocktake' ) , reason : '运营后台库存盘点' , expectedVersion : row . version , availableQuantity : Number ( value ) , lossQuantity : row . lossQuantity } ) ) ; ElMessage . success ( '盘点已完成' ) ; await loadStocks ( ) ; } catch ( error ) { if ( error !== 'cancel' && error !== 'close' ) { /* displayed */ } } }
async function showOrder ( row : ProductOrder ) { try { selectedStorage . value = null ; selectedOrder . value = await capture ( ( ) => getProductOrder ( props . session , row . id ) ) ; detailDrawer . value = true ; } catch { /* displayed */ } }
function orderActions ( row : ProductOrder ) { if ( row . status === 'PAID' ) return [ 'ACCEPT' , 'REFUND' ] ; if ( row . status === 'ACCEPTED' ) return row . fulfillmentMode === 'DELIVERY' ? [ 'START_DELIVERY' , 'CANCEL' ] : [ 'MARK_READY' , 'CANCEL' ] ; if ( [ 'DELIVERING' , 'READY_FOR_SELF_SERVICE' ] . includes ( row . status ) ) return [ 'COMPLETE' ] ; if ( row . status === 'COMPLETED' ) return [ 'REFUND' ] ; if ( row . status === 'PENDING_PAYMENT' ) return [ 'CANCEL' ] ; return [ ] ; }
async function actOnOrder ( row : ProductOrder , action : string ) { try { const destructive = [ 'CANCEL' , 'REFUND' ] . includes ( action ) ; let reason = '' ; if ( destructive ) { const result = await ElMessageBox . prompt ( ` 将对订单 ${ row . orderNo } 执行“ ${ actionLabel ( action ) } ”,库存与退款状态会按当前履约阶段处理。请输入原因。 ` , '确认商品订单操作' , { inputPattern : /\S{2,512}/ , inputErrorMessage : '请填写至少 2 个字符的原因' , type : 'warning' } ) ; reason = result . value . trim ( ) ; } await capture ( ( ) => runProductOrderAction ( props . session , row . id , { requestId : requestId ( ` order- ${ action . toLowerCase ( ) } ` ) , action , reason } ) ) ; ElMessage . success ( '订单状态已更新' ) ; await loadOrders ( ) ; } catch ( error ) { if ( error !== 'cancel' && error !== 'close' ) { /* displayed */ } } }
async function showStorage ( row : ProductStorage ) { try { selectedOrder . value = null ; selectedStorage . value = await capture ( ( ) => getProductStorage ( props . session , row . id ) ) ; detailDrawer . value = true ; } catch { /* displayed */ } }
async function openRetrieve ( row : ProductStorage ) { try { retrieveStorage . value = await capture ( ( ) => getProductStorage ( props . session , row . id ) ) ; const first = retrieveStorage . value . items ? . find ( ( item ) => item . remainingQuantity > 0 ) ; Object . assign ( retrieveForm , { storageItemId : first ? . id || '' , quantity : 1 , credential : '' } ) ; retrieveDialog . value = true ; } catch { /* displayed */ } }
async function submitRetrieve ( ) { if ( ! retrieveStorage . value || ! retrieveForm . storageItemId || ! retrieveForm . credential . trim ( ) ) return ElMessage . warning ( '请填写寄存明细、数量和领取码' ) ; saving . value = true ; try { const result = await capture ( ( ) => runProductStorageAction ( props . session , retrieveStorage . value ! . id , 'retrieve' , { requestId : requestId ( 'storage-retrieve' ) , claimCredential : retrieveForm . credential . trim ( ) , items : [ { storageItemId : retrieveForm . storageItemId , quantity : retrieveForm . quantity } ] } ) ) ; retrieveDialog . value = false ; retrieveForm . credential = '' ; if ( result . nextClaimCredential ) await showOneTimeCredential ( result . nextClaimCredential , '部分取出已完成,旧领取码已失效' ) ; else ElMessage . success ( '商品已取出' ) ; await loadStorages ( ) ; } catch { /* displayed */ } finally { saving . value = false ; } }
async function rotateStorage ( row : ProductStorage ) { try { await ElMessageBox . confirm ( ` 寄存单 ${ row . storageNo } 的当前领取码会立即失效,新码只显示一次。 ` , '确认轮换领取码' , { type : 'warning' } ) ; const result = await capture ( ( ) => runProductStorageAction ( props . session , row . id , 'credential/rotate' , { requestId : requestId ( 'storage-rotate' ) } ) ) ; if ( result . claimCredential ) await showOneTimeCredential ( result . claimCredential , '领取码已轮换' ) ; await loadStorages ( ) ; } catch ( error ) { if ( error !== 'cancel' && error !== 'close' ) { /* displayed */ } } }
async function cancelStorage ( row : ProductStorage ) { try { const { value } = await ElMessageBox . prompt ( ` 取消寄存单 ${ row . storageNo } 后不可继续领取,剩余 ${ row . remainingQuantity } 件不会自动回库存。请输入原因。 ` , '确认取消寄存' , { inputPattern : /\S{2,512}/ , inputErrorMessage : '请填写至少 2 个字符的原因' , type : 'warning' } ) ; await capture ( ( ) => runProductStorageAction ( props . session , row . id , 'cancel' , { requestId : requestId ( 'storage-cancel' ) , reason : value . trim ( ) } ) ) ; ElMessage . success ( '寄存已取消' ) ; await loadStorages ( ) ; } catch ( error ) { if ( error !== 'cancel' && error !== 'close' ) { /* displayed */ } } }
function openCreateStorage ( ) { Object . assign ( createStorageForm , { orderId : '' , expiresAt : new Date ( Date . now ( ) + 30 * 86400000 ) . toISOString ( ) } ) ; createStorageDialog . value = true ; }
async function submitCreateStorage ( ) { if ( ! /^\d+$/ . test ( createStorageForm . orderId ) || ! createStorageForm . expiresAt ) return ElMessage . warning ( '请填写已完成商品订单 ID 和到期时间' ) ; saving . value = true ; try { const result = await capture ( ( ) => createOrderProductStorage ( props . session , { requestId : requestId ( 'storage-create' ) , sourceType : 'ORDER' , sourceOrderId : createStorageForm . orderId , expiresAt : new Date ( createStorageForm . expiresAt ) . toISOString ( ) } ) ) ; createStorageDialog . value = false ; if ( result . claimCredential ) await showOneTimeCredential ( result . claimCredential , '寄存已创建' ) ; await loadStorages ( ) ; } catch { /* displayed */ } finally { saving . value = false ; } }
async function showOneTimeCredential ( value : string , title : string ) { await ElMessageBox . alert ( ` 领取码: ${ value } \ n \ n请立即交给寄存人安全保存。关闭后后台无法再次查看原码,可通过“换码”签发新码。 ` , title , { confirmButtonText : '我已安全记录' , type : 'success' , dangerouslyUseHTMLString : false } ) ; }
function requestId ( prefix : string ) { return ` ${ prefix } - ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ; }
function money ( value : number ) { return ` ¥ ${ ( Number ( value || 0 ) / 100 ) . toFixed ( 2 ) } ` ; }
function dateText ( value : string | null ) { return value ? new Date ( value ) . toLocaleString ( 'zh-CN' , { hour12 : false } ) : '-' ; }
function rangeText ( start : string | null , end : string | null ) { return start || end ? ` ${ dateText ( start ) } 至 ${ dateText ( end ) } ` : '长期有效' ; }
function fulfillmentLabel ( value : string ) { return ( { DELIVERY : '房间配送' , SELF _SERVICE : '自助领取' , BOTH : '配送 / 自助' } as Record < string , string > ) [ value ] || value ; }
function statusLabel ( value : string ) { return ( { PENDING _PAYMENT : '待支付' , PAID : '已支付' , ACCEPTED : '已接单' , DELIVERING : '配送中' , READY _FOR _SELF _SERVICE : '待自取' , COMPLETED : '已完成' , CANCELLED : '已取消' , REFUNDING : '退款中' , REFUNDED : '已退款' , REFUND _FAILED : '退款失败' } as Record < string , string > ) [ value ] || value ; }
function actionLabel ( value : string ) { return ( { ACCEPT : '接单' , START _DELIVERY : '开始配送' , MARK _READY : '标记待取' , COMPLETE : '完成' , CANCEL : '取消' , REFUND : '退款' } as Record < string , string > ) [ value ] || value ; }
function orderStatusType ( value : string ) { return [ 'COMPLETED' , 'REFUNDED' ] . includes ( value ) ? 'success' : [ 'CANCELLED' , 'REFUND_FAILED' ] . includes ( value ) ? 'danger' : [ 'PAID' , 'ACCEPTED' , 'DELIVERING' , 'READY_FOR_SELF_SERVICE' , 'REFUNDING' ] . includes ( value ) ? 'warning' : 'info' ; }
function storageStatusLabel ( value : string ) { return ( { STORED : '已寄存' , PARTIALLY _RETRIEVED : '部分取出' , RETRIEVED : '已取完' , EXPIRED : '已过期' , CANCELLED : '已取消' } as Record < string , string > ) [ value ] || value ; }
function storageStatusType ( value : string ) { return value === 'RETRIEVED' ? 'success' : value === 'PARTIALLY_RETRIEVED' ? 'warning' : [ 'EXPIRED' , 'CANCELLED' ] . includes ( value ) ? 'danger' : 'primary' ; }
function isStorageActive ( value : string ) { return [ 'STORED' , 'PARTIALLY_RETRIEVED' ] . includes ( value ) ; }
watch ( ( ) => props . session . token , ( token ) => { if ( token ) void loadWorkspace ( ) ; } , { immediate : true } ) ;
< / script >
< style scoped >
. goods - page { display : grid ; gap : 18 px ; }
. goods - metrics { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; gap : 14 px ; }
. goods - metrics span { display : grid ; gap : 5 px ; padding : 18 px ; border : 1 px solid var ( -- line ) ; border - radius : 16 px ; background : # fff ; }
. goods - metrics small { color : var ( -- muted ) ; }
. goods - metrics strong { font - size : 28 px ; }
. panel { padding : 20 px ; border : 1 px solid var ( -- line ) ; border - radius : 18 px ; background : # fff ; }
. goods - toolbar , . toolbar - actions , . tab - tools , . row - actions { display : flex ; align - items : center ; gap : 10 px ; flex - wrap : wrap ; }
. goods - toolbar { justify - content : space - between ; margin - bottom : 12 px ; }
. goods - toolbar h3 { margin : 2 px 0 0 ; }
. section - kicker { margin : 0 ; color : # 2563 eb ; font - size : 12 px ; font - weight : 700 ; }
. tab - tools { justify - content : flex - end ; margin - bottom : 14 px ; }
. tab - tools . el - input { width : min ( 320 px , 100 % ) ; }
. stack { display : grid ; gap : 4 px ; }
. stack span { color : var ( -- muted ) ; font - size : 12 px ; }
h4 { margin : 22 px 0 10 px ; }
@ media ( max - width : 760 px ) {
. goods - metrics { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; }
. panel { padding : 12 px ; }
. goods - toolbar { align - items : stretch ; }
. toolbar - actions , . toolbar - actions . el - select , . toolbar - actions . el - button { width : 100 % ; }
. tab - tools { justify - content : stretch ; }
. tab - tools > * { flex : 1 1 160 px ; }
}
< / style >