101 lines
11 KiB
Vue
101 lines
11 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, reactive, ref } from 'vue'
|
||
import { Plus } from '@lucide/vue'
|
||
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'
|
||
import OrderReferenceText from '../components/OrderReferenceText.vue'
|
||
|
||
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[] = [
|
||
{ 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 },
|
||
{ key: 'handlerName', prop: 'handlerName', label: '处理人', minWidth: 110 },
|
||
{ 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: '已完成' }
|
||
|
||
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>
|
||
<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>
|
||
<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>
|
||
<el-form-item label="箱数"><el-input-number v-model="form.packageCount" :min="1" :max="relation?.availablePackages || 999999" /></el-form-item>
|
||
<el-form-item label="SKU(可自由填写)"><el-input v-model="form.skuCode" maxlength="128" /></el-form-item>
|
||
</div><el-form-item label="异常原因" required><el-input v-model="form.reason" type="textarea" :rows="4" maxlength="1000" show-word-limit /></el-form-item>
|
||
<el-alert v-if="relation" type="success" :closable="false"><template #title><span class="relation-alert-title">已关联:<OrderReferenceText :value="relation.lastMileOrderNo || '无尾程单'" /> / {{ relation.inboundCode || '无入仓单' }},客户 {{ relation.customerName || relation.customerId }}</span></template></el-alert>
|
||
<el-alert v-else-if="relationError" :title="relationError" type="error" :closable="false" />
|
||
</el-form>
|
||
</AppDialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.related-orders { display: grid; gap: 2px; font-size: 12px; }.related-order-reference,.relation-alert-title { display: flex; min-width: 0; align-items: baseline; gap: 3px; }.create-form { margin-top: 18px; }.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 18px; }
|
||
@media (max-width: 720px) { .form-grid { grid-template-columns: 1fr; } }
|
||
</style>
|