331 lines
28 KiB
Vue
331 lines
28 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminDensity.css'
|
||
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'
|
||
import { relabelSkuCodes } from '../utils/relabelSkuDisplay'
|
||
import { Plus, PackagePlus } from '@lucide/vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { useRouter } from 'vue-router'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import AdminPage from '@/shared/components/AdminPage.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 AppDialog from '@/shared/components/AppDialog.vue'
|
||
import { changeRelabelOrderStatus, createRelabelOrder, getRelabelOrder, listRelabelOrders, listRelabelWarehouseOptions, getRelabelQuantities, type RelabelQuantityTotals, type RelabelWarehouseOptionDto } from '../api/relabelOrders'
|
||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||
import { formatOverseasDateTime, relabelBusinessStatusLabel } from '../lastMilePresentation'
|
||
import RelabelInventoryItemsEditor from '../components/RelabelInventoryItemsEditor.vue'
|
||
import type { CreateRelabelItemPayload } from '../api/relabelOrders'
|
||
import type { RelabelOrderListItem } from '../types'
|
||
|
||
const access = useAccessPreviewStore()
|
||
const router = useRouter()
|
||
type RelabelRow = Omit<RelabelOrderListItem, 'cartons'> & { cartons: string; labelPieces: string; skuCount: string }
|
||
const rows = ref<RelabelRow[]>([])
|
||
const loading = ref(false)
|
||
const voiding = ref('')
|
||
const loadError = ref<ApiErrorDetails | null>(null)
|
||
const customers = ref<PlatformTenantResource[]>([])
|
||
const customersLoading = ref(false)
|
||
const warehouses = ref<RelabelWarehouseOptionDto[]>([])
|
||
const warehousesLoading = ref(false)
|
||
const page = ref(1)
|
||
const pageSize = ref(10)
|
||
const total = ref(0)
|
||
const quantities = ref<RelabelQuantityTotals | null>(null)
|
||
function dimension(value: number | string | null | undefined) { return value == null ? '—' : String(value) }
|
||
const filters = ref<QueryModel>({ relabelNo: '', sku: '', status: '' })
|
||
const fields = computed<QueryField[]>(() => [
|
||
{ key: 'relabelNo', type: 'input', label: '换标单号', placeholder: '请输入换标单号', width: 220 },
|
||
{ key: 'sku', type: 'input', label: '商品SKU', placeholder: '请输入商品SKU', width: 180 },
|
||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '待客户确认', value: 'CUSTOMER_CONFIRM_PENDING' }, { label: '已驳回', value: 'REJECTED' }, { label: '换标完成', value: 'COMPLETED' }, { label: '已作废', value: 'CANCELLED' }] },
|
||
])
|
||
const columns: DataTableColumn[] = [
|
||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left' },
|
||
{ key: 'orderNo', label: '换标单号', minWidth: 170 },
|
||
{ key: 'sku', label: '商品SKU', minWidth: 150 },
|
||
{ key: 'productName', label: '中文品名', minWidth: 180 },
|
||
{ key: 'cartons', label: '箱数', width: 90, align: 'right' },
|
||
{ key: 'labelPieces', label: '贴标件数', width: 110, align: 'right' },
|
||
{ key: 'skuCount', label: 'SKU种数', width: 100, align: 'right' },
|
||
{ key: 'budgetTotal', label: '预算总价', width: 120, align: 'right' },
|
||
{ key: 'relabelCost', label: '换标费用', width: 120, align: 'right' },
|
||
{ key: 'walletStatus', label: '资金状态', width: 120 },
|
||
{ key: 'labelRequirement', label: '贴标要求', minWidth: 150 },
|
||
{ key: 'fileName', label: '上传文件', minWidth: 160 },
|
||
{ key: 'status', label: '状态', width: 110 },
|
||
{ key: 'createdAt', label: '创建时间', minWidth: 170 },
|
||
]
|
||
const dialogOpen = ref(false)
|
||
const dialogMode = ref<'create' | 'edit'>('create')
|
||
const submitting = ref(false)
|
||
const detailLoading = ref(false)
|
||
const editSourceStatus = ref('')
|
||
const auditLabelPieces = ref('—')
|
||
const form = reactive({ orderNo: '', customerId: '', relatedOrderNo: '', sku: '', productName: '', cartons: undefined as number | undefined, budgetTotal: undefined as number | undefined, status: 'PENDING_PROCESSING', labelRequirement: '', relabelCost: undefined as number | undefined, remark: '', warehouseId: '', labelDeadline: '', contactName: '', contactPhone: '', shipmentNo: '', newSkuCode: '' })
|
||
const createItems = ref<CreateRelabelItemPayload[]>([])
|
||
const createOperation = ref<CreateRelabelItemPayload['operationType']>('CARTON_LABEL')
|
||
const createDestination = ref('')
|
||
const createDelivery = ref<'TRUCK' | 'EXPRESS' | 'PICKUP' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP'>('PICKUP')
|
||
const createIdempotencyKey = ref('')
|
||
const dialogError = ref<ApiErrorDetails | null>(null), customersError = ref<ApiErrorDetails | null>(null), warehousesError = ref<ApiErrorDetails | null>(null)
|
||
const formLocked = computed(() => submitting.value || detailLoading.value || dialogMode.value === 'create' && (customersLoading.value || warehousesLoading.value) || !access.can(dialogMode.value === 'create' ? permission.relabelOrderCreate : permission.relabelOrderStatus))
|
||
const selectedRows = ref<RelabelRow[]>([])
|
||
const allowedStatusOptions = computed(() => {
|
||
if (editSourceStatus.value === 'PENDING_REVIEW') return ['PENDING_PROCESSING', 'REJECTED']
|
||
if (editSourceStatus.value === 'PENDING_PROCESSING') return ['PROCESSING', 'REJECTED', 'CANCELLED']
|
||
if (editSourceStatus.value === 'PROCESSING') return ['COMPLETED', 'REJECTED', 'CANCELLED']
|
||
return []
|
||
})
|
||
|
||
function optionalNumber(value: unknown) { const number = Number(value); return value === '' || value == null || !Number.isFinite(number) ? undefined : number }
|
||
function openPackingPlan() {
|
||
if (!selectedRows.value.length) { ElMessage.warning('请先勾选订单'); return }
|
||
const customerIds = [...new Set(selectedRows.value.map(row => row.customerId).filter(Boolean))]
|
||
if (customerIds.length > 1) { ElMessage.warning('合箱请选择同一客户的退货库存'); return }
|
||
void router.push({ name: 'overseas-return-inventory', query: { packing: '1', ...(customerIds[0] ? { customerId: customerIds[0] } : {}) } })
|
||
}
|
||
async function openForm(mode: 'create' | 'edit', source?: object) {
|
||
const row = source as RelabelRow | undefined
|
||
if (dialogOpen.value || submitting.value || detailLoading.value || !access.can(mode === 'create' ? permission.relabelOrderCreate : permission.relabelOrderStatus) || mode === 'edit' && (!row || !canAudit(row))) return
|
||
dialogError.value = null; customersError.value = null; warehousesError.value = null
|
||
|
||
dialogMode.value = mode
|
||
Object.assign(form, { orderNo: '', customerId: '', relatedOrderNo: '', sku: '', productName: '', cartons: undefined, budgetTotal: undefined, status: 'PENDING_PROCESSING', labelRequirement: '', relabelCost: undefined, remark: '', warehouseId: '', labelDeadline: '', contactName: '', contactPhone: '', shipmentNo: '', newSkuCode: '' })
|
||
editSourceStatus.value = ''
|
||
if (mode === 'edit' && row) {
|
||
editSourceStatus.value = row.rawStatus || relabelStatusCode(row.status)
|
||
Object.assign(form, { orderNo: String(row.orderNo || ''), relatedOrderNo: String(row.relatedOrderNo || ''), sku: String(row.sku || ''), productName: String(row.productName || ''), cartons: optionalNumber(row.cartons), budgetTotal: optionalNumber(row.budgetTotal), status: '', labelRequirement: String(row.labelRequirement || ''), relabelCost: optionalNumber(row.relabelCost), remark: String(row.remark || '') })
|
||
}
|
||
if (mode === 'create') {
|
||
createItems.value = []; createOperation.value = 'CARTON_LABEL'; createDestination.value = ''; createDelivery.value = 'PICKUP'
|
||
createIdempotencyKey.value = globalThis.crypto?.randomUUID?.() || `admin-relabel-${Date.now()}`
|
||
warehouses.value = []
|
||
void loadCustomers()
|
||
void loadWarehouses()
|
||
}
|
||
dialogOpen.value = true
|
||
if (mode === 'edit' && row?.orderNo) {
|
||
detailLoading.value = true
|
||
try {
|
||
const detail = await getRelabelOrder(String(row.orderNo))
|
||
Object.assign(form, {
|
||
orderNo: String(detail.application?.relabelNo || row.orderNo || ''),
|
||
relatedOrderNo: String(detail.application?.lastlegOrderNo || detail.application?.customerOrderNo || row.relatedOrderNo || ''),
|
||
sku: [...new Set(detail.items.flatMap(relabelSkuCodes))].join('、') || row.sku || '',
|
||
productName: [...new Set(detail.items.map(line => line.productNameZh?.trim()).filter(Boolean))].join('、') || row.productName || '',
|
||
cartons: optionalNumber(quantities.value?.orders[String(row.orderNo)]?.cartonCount),
|
||
labelRequirement: [...new Set(detail.items.map(line => line.labelRequirement?.trim()).filter(Boolean))].join(';') || row.labelRequirement || '',
|
||
shipmentNo: [...new Set(detail.items.map(line => line.shipmentNo?.trim()).filter(Boolean))].join('、'),
|
||
newSkuCode: detail.items.flatMap(line => line.skuItems?.length ? line.skuItems.map(sku => `${sku.newSkuCode} × ${sku.quantity} 件`) : [line.newSkuCode || '']).filter(Boolean).join(';'),
|
||
relabelCost: optionalNumber(detail.application?.chargeAmount ?? row.relabelCost),
|
||
remark: String(detail.remark || detail.application?.reviewRemark || row.remark || ''),
|
||
})
|
||
auditLabelPieces.value = dimension(quantities.value?.orders[String(row.orderNo)]?.labelPieceCount)
|
||
} catch (error) {
|
||
ElMessage.error(getApiErrorDetails(error, '换标单详情加载失败,请重试').message)
|
||
dialogOpen.value = false
|
||
} finally { detailLoading.value = false }
|
||
}
|
||
}
|
||
|
||
async function loadCustomers() {
|
||
customersError.value = null; customersLoading.value = true
|
||
try {
|
||
const records: PlatformTenantResource[] = []
|
||
let currentPage = 1
|
||
let pages = 1
|
||
do {
|
||
const result = await listPlatformTenants({ page: currentPage, size: 200, status: 'ACTIVE' })
|
||
records.push(...result.records.filter((item) => item.status === 'ACTIVE'))
|
||
pages = Math.max(1, result.pages)
|
||
currentPage += 1
|
||
} while (currentPage <= pages)
|
||
customers.value = records
|
||
} catch (error) { customers.value = []; customersError.value = getApiErrorDetails(error, '客户列表加载失败,请重试') }
|
||
finally { customersLoading.value = false }
|
||
}
|
||
|
||
async function loadWarehouses() {
|
||
warehousesError.value = null
|
||
form.warehouseId = ''
|
||
warehouses.value = []
|
||
warehousesLoading.value = true
|
||
try {
|
||
const records = (await listRelabelWarehouseOptions()).records.filter((item) => item.status === 'ACTIVE')
|
||
warehouses.value = records
|
||
} catch (error) {
|
||
warehousesError.value = getApiErrorDetails(error, '公共海外收货仓加载失败,请重试')
|
||
} finally {
|
||
warehousesLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function submitForm() {
|
||
if (!dialogOpen.value || formLocked.value || dialogMode.value === 'create' && (customersError.value || warehousesError.value)) return
|
||
dialogError.value = null
|
||
try {
|
||
if (dialogMode.value === 'create') {
|
||
if (!form.customerId || !form.warehouseId) { ElMessage.warning('请选择客户和公共海外收货仓'); return }
|
||
if (!form.relatedOrderNo.trim() || !createItems.value.length || !form.relabelCost || form.relabelCost <= 0 || !form.labelDeadline || !form.contactName.trim() || !form.contactPhone.trim() || !form.shipmentNo.trim()) {
|
||
ElMessage.warning('请完整填写换标单必填信息')
|
||
return
|
||
}
|
||
const marks = createItems.value.map(item => item.cartonMark.trim())
|
||
if (marks.some(mark => !mark) || new Set(marks).size !== marks.length) { ElMessage.warning('请填写不重复的我方箱唛'); return }
|
||
if (createOperation.value !== 'SKU_LABEL' && (!createDestination.value.trim() || createItems.value.some(item => !item.fbaCartonMark?.trim()))) { ElMessage.warning('请填写 FBA 目的仓和每箱的新箱唛'); return }
|
||
if (createOperation.value !== 'CARTON_LABEL' && createItems.value.some(item => !item.skuItems.length || item.skuItems.some(sku => !sku.skuCode.trim() || !sku.newSkuCode.trim() || !Number.isInteger(sku.quantity) || sku.quantity <= 0))) { ElMessage.warning('请完整填写每箱 SKU 换标及件数'); return }
|
||
submitting.value = true
|
||
await createRelabelOrder(form.customerId, { customerOrderNo: form.relatedOrderNo.trim(), operationWarehouseId: form.warehouseId, labelDeadline: form.labelDeadline, contactName: form.contactName.trim(), contactPhone: form.contactPhone.trim(), chargeAmount: Number(form.relabelCost), remark: form.remark.trim() || undefined, idempotencyKey: createIdempotencyKey.value, deliveryMethod: createOperation.value === 'SKU_LABEL' ? undefined : createDelivery.value, printShippingLabel: false, items: createItems.value })
|
||
ElMessage.success('换标单已创建')
|
||
} else {
|
||
if (!allowedStatusOptions.value.includes(form.status)) { ElMessage.warning('当前状态没有可提交的审核流转'); return }
|
||
if (form.status === 'REJECTED' && !form.remark.trim()) { ElMessage.warning('审核驳回请填写原因'); return }
|
||
submitting.value = true
|
||
await changeRelabelOrderStatus(form.orderNo, form.status, form.remark || undefined)
|
||
ElMessage.success('状态已更新')
|
||
}
|
||
dialogOpen.value = false
|
||
createIdempotencyKey.value = ''
|
||
await load()
|
||
} catch (error) { dialogError.value = getApiErrorDetails(error, '操作失败,请重试') }
|
||
finally { submitting.value = false }
|
||
}
|
||
|
||
function openDetail(source: object) {
|
||
const row = source as RelabelRow
|
||
void router.push({ name: 'overseas-relabel-order-detail', params: { orderId: String(row.orderNo) } })
|
||
}
|
||
|
||
function relabelStatus(value: unknown) {
|
||
if (value === 'REJECTED') return '已驳回'
|
||
if (value === 'PENDING_REVIEW') return '待审核'
|
||
if (value === 'CANCELLED') return '已作废'
|
||
if (value === 'CUSTOMER_CONFIRM_PENDING') return '待客户确认'
|
||
return relabelBusinessStatusLabel(value)
|
||
}
|
||
function canVoid(source: object) {
|
||
const row = source as RelabelRow
|
||
return !['READY_FOR_OUTBOUND', 'COMPLETED', 'CANCELLED'].includes(row.rawStatus || relabelStatusCode(row.status))
|
||
}
|
||
async function voidOrder(source: object) {
|
||
const row = source as RelabelRow
|
||
if (voiding.value || !canVoid(row) || !access.can(permission.relabelOrderStatus)) return
|
||
voiding.value = String(row.orderNo)
|
||
try {
|
||
await ElMessageBox.confirm(`确认作废换标单 ${row.orderNo}?作废后停止换标作业,保留原单和操作记录。`, '作废换标单', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认作废', cancelButtonText: '取消' })
|
||
await changeRelabelOrderStatus(String(row.orderNo), 'CANCELLED', '平台作废换标单')
|
||
ElMessage.success('换标单已作废')
|
||
await load()
|
||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '换标单作废失败,请重试').message) }
|
||
finally { voiding.value = '' }
|
||
}
|
||
function relabelAuditStatusLabel(value: string) {
|
||
return ({
|
||
PENDING_PROCESSING: '审核通过',
|
||
PROCESSING: '处理中',
|
||
REJECTED: '已驳回',
|
||
CANCELLED: '已取消',
|
||
COMPLETED: '已完成',
|
||
} as Record<string, string>)[value] || relabelStatus(value)
|
||
}
|
||
function relabelStatusCode(value: unknown) {
|
||
const codes: Record<string, string> = { 待审核: 'PENDING_REVIEW', 待处理: 'PENDING_PROCESSING', 处理中: 'PROCESSING', 换标完成: 'COMPLETED' }
|
||
return codes[String(value || '')] || String(value || 'PENDING_PROCESSING')
|
||
}
|
||
function walletStatus(value: unknown) { return ({ FREEZE_PENDING: '冻结中', FROZEN: '已冻结', SETTLE_PENDING: '结算中', SETTLED: '已结算', RELEASE_PENDING: '释放中', RELEASED: '已释放', FREEZE_FAILED: '冻结失败', SETTLE_FAILED: '结算失败', RELEASE_FAILED: '释放失败' } as Record<string,string>)[String(value || '')] || String(value || '—') }
|
||
function canAudit(source: object) {
|
||
const row = source as RelabelRow
|
||
return row.reviewChannel !== 'TAIL_ORDER'
|
||
&& (row.rawStatus || relabelStatusCode(row.status)) === 'PENDING_REVIEW'
|
||
}
|
||
let loadGeneration = 0
|
||
async function load(model: QueryModel = filters.value) {
|
||
const current = ++loadGeneration
|
||
Object.assign(filters.value, model)
|
||
loading.value = true; loadError.value = null
|
||
try {
|
||
const query = { page: page.value, size: pageSize.value, relabelNo: String(filters.value.relabelNo || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined }
|
||
quantities.value = null
|
||
const result = await listRelabelOrders(query)
|
||
const measured = await getRelabelQuantities(query).catch(() => null)
|
||
if (current !== loadGeneration) return
|
||
quantities.value = measured
|
||
rows.value = result.records.map((item) => ({
|
||
customerId: '', orderNo: String(item.relabelNo), relatedOrderNo: String(item.lastlegOrderNo || item.customerOrderNo || '—'), sku: String(item.skuCodes || '—'),
|
||
productName: item.productNameZh?.trim() || '—', cartons: dimension(quantities.value?.orders[item.relabelNo]?.cartonCount), labelPieces: dimension(quantities.value?.orders[item.relabelNo]?.labelPieceCount), skuCount: dimension(quantities.value?.orders[item.relabelNo]?.skuCount), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: item.labelRequirement?.trim() || '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '',
|
||
reviewChannel: item.reviewChannel || undefined,
|
||
createdAt: formatOverseasDateTime(item.createdAt),
|
||
}))
|
||
total.value = result.total
|
||
} catch (error) { if (current === loadGeneration) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '换标单加载失败,请重试') } }
|
||
finally { if (current === loadGeneration) loading.value = false }
|
||
}
|
||
function search(model: QueryModel) { page.value = 1; void load(model) }
|
||
function reset(model: QueryModel) { page.value = 1; void load(model) }
|
||
onMounted(load)
|
||
onActivated(() => { if (!loading.value) void load() })
|
||
watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
|
||
</script>
|
||
|
||
<template>
|
||
<AdminPage class="admin-density"><ListPageLayout aria-label="换标单列表">
|
||
|
||
|
||
<section><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-relabel-orders" row-key="orderNo" empty-text="暂无换标单" @selection-change="selectedRows = $event" @update:page="load()" @update:page-size="page = 1; load()">
|
||
<template #toolbar-start><h1>换标单</h1></template>
|
||
<template #filters><QueryToolbar v-model="filters" :fields="fields" :loading="loading" ariaLabel="换标单筛选" preference-key="overseas-relabel-orders-query" :search-on-reset="false" @search="search" @reset="reset" /></template>
|
||
<template #notices><template v-if="loadError"><LoadErrorState title="换标单加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template></template>
|
||
|
||
|
||
<template #toolbar-actions><el-button v-if="selectedRows.length" type="warning" @click="openPackingPlan"><PackagePlus :size="16" />创建合箱计划({{ selectedRows.length }})</el-button><el-button v-if="access.can(permission.relabelOrderCreate)" type="primary" @click="openForm('create')"><Plus :size="16" />新增换标单</el-button></template>
|
||
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button><el-button class="density-action" data-tone="warning" v-if="access.can(permission.relabelOrderStatus) && canAudit(row)" size="small" @click="openForm('edit', row)">审核</el-button><el-button v-if="access.can(permission.relabelOrderStatus) && canVoid(row)" class="density-action" data-tone="danger" size="small" :loading="voiding === row.orderNo" :disabled="!!voiding" @click="voidOrder(row)">作废</el-button></div></template>
|
||
<template #pagination-left>共 {{ total }} 条换标单</template>
|
||
</DataTable></section>
|
||
</ListPageLayout>
|
||
|
||
<AppDialog class="admin-density-dialog" v-model="dialogOpen" :title="dialogMode === 'create' ? '新增换标单' : '审核换标单 · ' + (form.orderNo || '—')" width="min(760px, calc(100vw - 28px))" fullscreenable :confirm-text="dialogMode === 'create' ? '保存换标单' : '提交审核'" :confirm-loading="submitting || detailLoading || dialogMode === 'create' && (customersLoading || warehousesLoading)" :confirm-disabled="formLocked || dialogMode === 'create' && Boolean(customersError || warehousesError)" @confirm="submitForm">
|
||
<div class="density-dialog-sections">
|
||
<LoadErrorState v-if="dialogMode === 'create' && customersError" compact title="客户候选读取失败" :message="customersError.message" :loading="customersLoading || submitting" @retry="loadCustomers"><template #details><ApiErrorMeta :details="customersError" /></template></LoadErrorState>
|
||
<LoadErrorState v-if="dialogMode === 'create' && warehousesError" compact title="公共海外收货仓读取失败" :message="warehousesError.message" :loading="warehousesLoading || submitting" @retry="loadWarehouses"><template #details><ApiErrorMeta :details="warehousesError" /></template></LoadErrorState>
|
||
<LoadErrorState v-if="dialogError" compact title="换标操作未完成" :message="dialogError.message" :show-retry="false"><template #details><ApiErrorMeta :details="dialogError" /></template></LoadErrorState>
|
||
<el-form class="density-form density-dialog-sections" label-position="left" label-width="120px" :disabled="formLocked">
|
||
<template v-if="dialogMode === 'create'">
|
||
<section class="density-dialog-section" data-tone="summary" aria-label="换标单生成方式"><table class="density-table"><tbody><tr><th scope="row">换标单号</th><td>系统自动生成</td></tr></tbody></table></section>
|
||
<section class="density-dialog-section" data-tone="work" aria-label="换标基础资料"><h3 class="density-dialog-section__title">订单信息</h3><table class="density-table density-table--editable relabel-editor-table"><tbody><tr><td><el-form-item label="关联订单" required><el-input v-model="form.relatedOrderNo" /></el-form-item></td><td><el-form-item label="客户" required><el-select v-model="form.customerId" filterable :loading="customersLoading" placeholder="请选择客户"><el-option v-for="item in customers" :key="String(item.id)" :label="item.tenantName" :value="String(item.id)" /></el-select></el-form-item></td></tr><tr><td><el-form-item label="公共海外收货仓" required><el-select v-model="form.warehouseId" filterable :loading="warehousesLoading" placeholder="请选择公共海外收货仓"><el-option v-for="item in warehouses" :key="item.id" :label="`${item.warehouseName} · ${item.warehouseCode}`" :value="String(item.id)" /></el-select></el-form-item></td><td><el-form-item label="截止时间" required><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></el-form-item></td></tr><tr><td colspan="2"><el-form-item label="换标费用(CNY)" required><el-input-number v-model="form.relabelCost" :min="0.01" :precision="2" controls-position="right" /></el-form-item></td></tr><tr><td><el-form-item label="联系人" required><el-input v-model="form.contactName" /></el-form-item></td><td><el-form-item label="联系电话" required><el-input v-model="form.contactPhone" /></el-form-item></td></tr></tbody></table></section>
|
||
<section class="density-dialog-section" data-tone="work" aria-label="换标作业要求"><h3 class="density-dialog-section__title">作业要求</h3><table class="density-table density-table--editable relabel-editor-table"><tbody><tr><td><el-form-item label="贴标要求" required><el-input v-model="form.labelRequirement" /></el-form-item></td><td><el-form-item label="货件号" required><el-input v-model="form.shipmentNo" /></el-form-item></td></tr></tbody></table></section>
|
||
<section class="density-dialog-section" data-tone="work" aria-label="换标类型与去向">
|
||
<el-form-item label="换标类型" required><el-select v-model="createOperation" placeholder="请选择换标类型"><el-option label="只换箱唛" value="CARTON_LABEL" /><el-option label="只换 SKU" value="SKU_LABEL" /><el-option label="箱唛和 SKU 都换" value="CARTON_AND_SKU" /></el-select></el-form-item>
|
||
<template v-if="createOperation !== 'SKU_LABEL'"><el-form-item label="FBA 目的仓" required><el-input v-model="createDestination" placeholder="填写正式 FBA 仓码" /></el-form-item><el-form-item label="配送方式" required><el-select v-model="createDelivery" placeholder="请选择配送方式"><el-option label="普通自提" value="PICKUP" /><el-option label="平台卡车" value="TRUCK" /><el-option label="快递" value="EXPRESS" /><el-option label="亚马逊自提" value="AMAZON_PICKUP" /><el-option label="客户自提" value="CUSTOMER_PICKUP" /></el-select></el-form-item></template>
|
||
</section>
|
||
<RelabelInventoryItemsEditor :customer-id="form.customerId" :warehouse-id="form.warehouseId" :operation-type="createOperation" :shipment-no="form.shipmentNo" :destination="createDestination" :requirement="form.labelRequirement" @update:items="createItems = $event" />
|
||
</template>
|
||
<template v-else>
|
||
<section class="density-dialog-section" data-tone="source" aria-label="换标审核原始资料"><h3 class="density-dialog-section__title">原始资料</h3><table class="density-table"><tbody>
|
||
<tr><th scope="row">关联订单</th><td colspan="3">{{ form.relatedOrderNo || '—' }}</td></tr><tr><th scope="row">商品SKU</th><td colspan="3">{{ form.sku || '—' }}</td></tr><tr><th scope="row">中文品名</th><td colspan="3">{{ form.productName || '—' }}</td></tr>
|
||
<tr><th scope="row">箱数</th><td>{{ form.cartons ?? '—' }}</td><th scope="row">换标费用(CNY)</th><td>{{ form.relabelCost ?? '—' }}</td></tr>
|
||
<tr><th scope="row">贴标件数</th><td colspan="3">{{ auditLabelPieces }}</td></tr>
|
||
<tr><th scope="row">贴标要求</th><td colspan="3">{{ form.labelRequirement || '—' }}</td></tr><tr><th scope="row">货件号</th><td colspan="3">{{ form.shipmentNo || '—' }}</td></tr><tr><th scope="row">新SKU</th><td colspan="3">{{ form.newSkuCode || '—' }}</td></tr>
|
||
</tbody></table></section>
|
||
<section class="density-dialog-section" data-tone="work" aria-label="换标审核结论"><h3 class="density-dialog-section__title">审核结论</h3><table class="density-table density-table--editable relabel-editor-table"><tbody><tr><td colspan="2"><el-form-item label="审核动作"><el-select v-model="form.status" placeholder="请选择审核动作"><el-option v-for="value in allowedStatusOptions" :key="value" :label="relabelAuditStatusLabel(value)" :value="value" /></el-select></el-form-item></td></tr></tbody></table></section>
|
||
</template>
|
||
<section class="density-dialog-section" data-tone="work" aria-label="换标备注与原因"><h3 class="density-dialog-section__title">{{ dialogMode === 'edit' ? '审核备注' : '备注' }}</h3><table class="density-table density-table--editable relabel-editor-table"><tbody><tr><td colspan="2"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item></td></tr></tbody></table><p v-if="dialogMode === 'edit'" class="density-dialog-note">审核驳回时必须填写原因。</p></section>
|
||
</el-form>
|
||
</div>
|
||
</AppDialog>
|
||
</AdminPage>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.el-form :deep(.el-row) { margin-left: 0 !important; margin-right: 0 !important; }
|
||
.el-form :deep(.el-input), .el-form :deep(.el-select), .el-form :deep(.el-input-number), .el-form :deep(.el-date-editor) { width: 100%; }
|
||
.relabel-editor-table { table-layout: fixed; }
|
||
.relabel-editor-table td:not([colspan]) { width: 50%; }
|
||
@media (max-width: 720px) { .relabel-editor-table, .relabel-editor-table tbody { display: block; }.relabel-editor-table tr { display: grid; grid-template-columns: 1fr; }.relabel-editor-table td:not([colspan]) { width: auto; } }
|
||
</style>
|