2026-08-26 15:48:34 +08:00
< script setup lang = "ts" >
import { onMounted , reactive , ref } from 'vue'
2026-09-03 17:17:11 +08:00
import { Eye , Plus } from '@lucide/vue'
2026-08-26 15:48:34 +08:00
import { ElMessage } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { formatOverseasDateTime } from '../lastMilePresentation'
import { overseasWarehousePermissions as permission } from '../permissions'
import { createExceptionOrder , listExceptionOrders , resolveExceptionRelation , type ExceptionOrderRow , type ExceptionRelation } from '../api/exceptionOrders'
2026-08-27 15:56:13 +08:00
import OrderReferenceText from '../components/OrderReferenceText.vue'
2026-08-26 15:48:34 +08:00
const rows = ref < ExceptionOrderRow [] >([]); const page = ref ( 1 ); const pageSize = ref ( 10 ); const total = ref ( 0 ); const loading = ref ( false ); const error = ref < ApiErrorDetails | null >( null )
const access = useAccessPreviewStore ()
const filters = ref < QueryModel >({ keyword : '' , exceptionType : '' , status : '' })
const fields : QueryField [] = [
{ key : 'keyword' , type : 'input' , label : '关键词' , placeholder : '异常单号 / 关联单号 / 客户 / 原因' },
{ key : 'exceptionType' , type : 'select' , label : '异常类型' , placeholder : '全部类型' , options : [{ label : '退货' , value : 'RETURN' }, { label : '拒收' , value : 'REJECT' }, { label : '破损' , value : 'DAMAGED' }, { label : '其他' , value : 'OTHER' }] },
{ key : 'status' , type : 'select' , label : '处理状态' , placeholder : '全部状态' , options : [{ label : '待处理' , value : 'PENDING' }, { label : '处理中' , value : 'PROCESSING' }, { label : '已完成' , value : 'COMPLETED' }] },
]
const columns : DataTableColumn [] = [
2026-09-03 17:17:11 +08:00
{ key : 'actions' , label : '操作' , width : 100 , fixed : 'left' , slotName : 'actions' },
2026-08-26 15:48:34 +08:00
{ key : 'exceptionNo' , prop : 'exceptionNo' , label : '异常订单号' , minWidth : 180 , fixed : 'left' },
{ key : 'related' , label : '关联订单号' , minWidth : 230 , slotName : 'related' },
{ key : 'exceptionType' , label : '异常类型' , width : 100 , formatter : ( row : ExceptionOrderRow ) => typeLabels [ row . exceptionType ] || row . exceptionType },
{ key : 'customerName' , prop : 'customerName' , label : '客户' , minWidth : 160 },
{ key : 'overseasWarehouseName' , prop : 'overseasWarehouseName' , label : '仓库' , minWidth : 140 },
{ key : 'reason' , prop : 'reason' , label : '异常原因' , minWidth : 200 },
{ key : 'skuCode' , prop : 'skuCode' , label : 'SKU' , minWidth : 120 },
{ key : 'packageCount' , prop : 'packageCount' , label : '箱数' , width : 80 },
{ key : 'exceptionAt' , label : '异常时间' , minWidth : 170 , formatter : ( row : ExceptionOrderRow ) => formatOverseasDateTime ( row . exceptionAt ) },
{ key : 'status' , label : '处理状态' , width : 100 , slotName : 'status' },
{ key : 'handlingAction' , prop : 'handlingAction' , label : '处理动作' , minWidth : 140 },
{ key : 'returnLogisticsNo' , prop : 'returnLogisticsNo' , label : '退回物流单号' , minWidth : 160 },
2026-09-03 17:17:11 +08:00
{ key : 'handlerName' , prop : 'handlerName' , label : '处理人' , minWidth : 110 , slotName : 'handlerName' },
2026-08-26 15:48:34 +08:00
{ key : 'updatedAt' , label : '更新时间' , minWidth : 170 , formatter : ( row : ExceptionOrderRow ) => formatOverseasDateTime ( row . updatedAt ) },
]
const typeLabels : Record < string , string > = { RETURN : '退货' , REJECT : '拒收' , DAMAGED : '破损' , OTHER : '其他' }
const statusLabels : Record < string , string > = { PENDING : '待处理' , PROCESSING : '处理中' , COMPLETED : '已完成' }
2026-09-03 17:17:11 +08:00
const detailOpen = ref ( false ); const detailRow = ref < ExceptionOrderRow | null >( null )
function openDetail ( row : ExceptionOrderRow ) { detailRow . value = row ; detailOpen . value = true }
function displayHandler ( value : unknown ) {
const normalized = String ( value ?? '' ). trim ()
return normalized && ! /^\d+$/ . test ( normalized ) ? normalized : '—'
}
2026-08-26 15:48:34 +08:00
const createOpen = ref ( false ); const saving = ref ( false ); const resolving = ref ( false ); const relation = ref < ExceptionRelation | null >( null ); const relationError = ref ( '' )
const form = reactive ({ customerId : '' , inboundCode : '' , lastMileOrderNo : '' , exceptionType : '' , reason : '' , packageCount : undefined as number | undefined , skuCode : '' })
function openCreate () { Object . assign ( form , { customerId : '' , inboundCode : '' , lastMileOrderNo : '' , exceptionType : '' , reason : '' , packageCount : undefined , skuCode : '' }); relation . value = null ; relationError . value = '' ; createOpen . value = true }
async function resolveRelation () {
if ( ! form . inboundCode . trim () && ! form . lastMileOrderNo . trim ()) { relation . value = null ; relationError . value = '入仓码和尾程订单号至少填写一个' ; return }
resolving . value = true ; relationError . value = ''
try { relation . value = await resolveExceptionRelation ({ customerId : form . customerId . trim () || undefined , inboundCode : form . inboundCode . trim () || undefined , lastMileOrderNo : form . lastMileOrderNo . trim () || undefined }); if ( ! form . inboundCode && relation . value . inboundCode ) form . inboundCode = relation . value . inboundCode ; if ( ! form . lastMileOrderNo && relation . value . lastMileOrderNo ) form . lastMileOrderNo = relation . value . lastMileOrderNo }
catch ( cause ) { relation . value = null ; relationError . value = getApiErrorDetails ( cause , '关联单据校验失败' ). message }
finally { resolving . value = false }
}
async function submit () {
await resolveRelation (); if ( ! relation . value ) return
if ( ! form . exceptionType || ! form . reason . trim ()) { ElMessage . warning ( '请选择异常类型并填写异常原因' ); return }
saving . value = true
try { const result = await createExceptionOrder ({ customerId : form . customerId . trim () || undefined , inboundCode : form . inboundCode . trim () || undefined , lastMileOrderNo : form . lastMileOrderNo . trim () || undefined , exceptionType : form . exceptionType , reason : form . reason . trim (), packageCount : form . packageCount , skuCode : form . skuCode . trim () || undefined }); createOpen . value = false ; ElMessage . success ( `异常单 ${ result . exceptionNo } 创建成功` ); page . value = 1 ; await load () }
catch ( cause ) { ElMessage . error ( getApiErrorDetails ( cause , '新增异常订单失败' ). message ) } finally { saving . value = false }
}
async function load ( model : QueryModel = filters . value ) { Object . assign ( filters . value , model ); loading . value = true ; error . value = null ; try { const result = await listExceptionOrders ({ page : page . value , size : pageSize . value , keyword : text ( 'keyword' ), exceptionType : text ( 'exceptionType' ), status : text ( 'status' ) }); rows . value = result . records ; total . value = result . total } catch ( cause ) { rows . value = []; total . value = 0 ; error . value = getApiErrorDetails ( cause , '异常订单列表加载失败' ) } finally { loading . value = false } }
function text ( key : string ) { return String ( filters . value [ key ] || '' ). trim () || undefined }
function search ( model : QueryModel ) { page . value = 1 ; void load ( model ) }
function reset ( model : QueryModel ) { page . value = 1 ; void load ( model ) }
onMounted ( load )
</ script >
< template >
< AdminPage >< ListPageLayout aria -label =" 异常订单列表 " >
< template # toolbar >< QueryToolbar v-model = "filters" class="surface-panel" :fields="fields" :loading="loading" ariaLabel="异常订单筛选" preference-key="overseas-exception-orders-query" @search="search" @reset="reset" /></ template >
< template v-if = "error" #state><LoadErrorState title="异常订单列表加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></ template ></ LoadErrorState ></ template >
< section v-if = "!error" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-exception-orders" row-key="id" empty-text="暂无异常订单" @update:page="load()" @update:page-size="page = 1; load()" >
< template # toolbar -actions >< el-button v-if = "access.can(permission.exceptionOrderCreate)" type="primary" @click="openCreate"><Plus :size="16" /> 新增异常订单 </ el-button ></ template >
2026-09-03 17:17:11 +08:00
< template # actions = "{ row }" >< el-button size = "small" type = "primary" plain @click ="openDetail(row as ExceptionOrderRow)" >< Eye :size = "15" /> 查看 </ el-button ></ template >
2026-08-27 15:56:13 +08:00
< template # related = "{ row }" >< div class = "related-orders" >< span v-if = "row.lastMileOrderNo" class="related-order-reference">尾程 <OrderReferenceText :value="String(row.lastMileOrderNo)" /></span><span v-if="row.deliveryNo">派送 {{ row.deliveryNo }}</span><span v-if="row.inboundCode">入仓 {{ row.inboundCode }}</span><span v-if="row.outboundNo" > 出库 {{ row.outboundNo }}</ span ></ div ></ template >
2026-09-03 17:17:11 +08:00
< template # handlerName = "{ row }" >{{ displayHandler ( row . handlerName ) }}</ template >
2026-08-26 15:48:34 +08:00
< template # status = "{ row }" >< StatusBadge : status = "statusLabels[String(row.status)] || String(row.status)" /></ template >< template # pagination -left > 共 {{ total }} 条异常订单 </ template >
</ DataTable ></ section >
</ ListPageLayout ></ AdminPage >
< AppDialog v-model = "createOpen" title="新增异常订单" width="min(760px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="saving" :loading="resolving" @confirm="submit" >
< el-alert title = "入仓码和尾程订单号至少填写一个;两项都填写时必须属于同一订单。" type = "warning" :closable = "false" />
< el-form label -position =" top " class = "create-form" >< div class = "form-grid" >
< el-form-item label = "入仓码" >< el-input v-model = "form.inboundCode" placeholder="请输入入仓码" @blur="resolveRelation" /></ el-form-item >
< el-form-item label = "尾程订单号" >< el-input v-model = "form.lastMileOrderNo" placeholder="请输入尾程订单号" @blur="resolveRelation" /></ el-form-item >
< el-form-item label = "异常类型" required >< el-select v-model = "form.exceptionType" placeholder="请选择异常类型"><el-option v-for="(label, value) in typeLabels" :key="value" :label="label" :value="value" /></ el-select ></ el-form-item >
< el-form-item label = "处理状态" >< el-input model -value =" 待处理 " readonly /></ el-form-item >
2026-09-03 19:29:38 +08:00
< el-form-item label = "箱数" >< el-input-number v-model = "form.packageCount" :min="1" :max="relation?.availablePackages || 999999" placeholder="请输入箱数" /></ el-form-item >
< el-form-item label = "SKU(可自由填写)" >< el-input v-model = "form.skuCode" maxlength="128" placeholder="请输入 SKU(选填)" /></ el-form-item >
</ div >< el-form-item label = "异常原因" required >< el-input v-model = "form.reason" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="请输入异常原因" /></ el-form-item >
2026-08-30 17:43:03 +08:00
< el-alert v-if = "relation" type="success" :closable="false"><template #title><span class="relation-alert-title">已关联:<OrderReferenceText :value="relation.lastMileOrderNo || '无尾程单'" /> / {{ relation . inboundCode || '无入仓单' }} , 客户 {{ relation . customerName || '客户信息待补充' }}</ span ></ template ></ el-alert >
2026-08-26 15:48:34 +08:00
< el-alert v-else-if = "relationError" :title="relationError" type="error" :closable="false" />
</ el-form >
</ AppDialog >
2026-09-03 17:17:11 +08:00
< AppDialog v-model = "detailOpen" title="异常订单详情" width="min(760px, calc(100vw - 28px))" :show-confirm="false" cancel-text="关闭" >
< el-descriptions v-if = "detailRow" :column="2" border >
< el-descriptions-item label = "异常订单号" >{{ detailRow . exceptionNo }}</ el-descriptions-item >
< el-descriptions-item label = "处理状态" >{{ statusLabels [ String ( detailRow . status )] || detailRow . status }}</ el-descriptions-item >
< el-descriptions-item label = "异常类型" >{{ typeLabels [ detailRow . exceptionType ] || detailRow . exceptionType }}</ el-descriptions-item >
< el-descriptions-item label = "客户" >{{ detailRow . customerName || '—' }}</ el-descriptions-item >
< el-descriptions-item label = "海外仓" >{{ detailRow . overseasWarehouseName || '—' }}</ el-descriptions-item >
< el-descriptions-item label = "处理人" >{{ displayHandler ( detailRow . handlerName ) }}</ el-descriptions-item >
< el-descriptions-item label = "异常时间" >{{ formatOverseasDateTime ( detailRow . exceptionAt ) }}</ el-descriptions-item >
< el-descriptions-item label = "更新时间" >{{ formatOverseasDateTime ( detailRow . updatedAt ) }}</ el-descriptions-item >
< el-descriptions-item label = "异常原因" :span = "2" >{{ detailRow . reason || '—' }}</ el-descriptions-item >
< el-descriptions-item label = "处理动作" :span = "2" >{{ detailRow . handlingAction || '—' }}</ el-descriptions-item >
</ el-descriptions >
</ AppDialog >
2026-08-26 15:48:34 +08:00
</ template >
< style scoped >
2026-08-27 15:56:13 +08:00
. related - orders { display : grid ; gap : 2 px ; font - size : 12 px ; }. related - order - reference ,. relation - alert - title { display : flex ; min - width : 0 ; align - items : baseline ; gap : 3 px ; }. create - form { margin - top : 18 px ; }. form - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 0 18 px ; }
2026-08-26 15:48:34 +08:00
@ media ( max - width : 720 px ) { . form - grid { grid - template - columns : 1 fr ; } }
</ style >