Files
admin-web/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue
T
2026-10-20 11:33:39 +08:00

312 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 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)
const quantityError = ref('')
function dimension(value: number | null | undefined) { return value == null ? '未提供' : String(value) }
function knownDimension(value: number, unknown: number, unit: string) { return Number(value) === 0 && Number(unknown) > 0 ? `${unit === '箱' ? '箱数' : '贴标件数'}未提供(${unknown}票)` : `已知 ${value} ${unit}${Number(unknown) > 0 ? `(${unknown}票未提供)` : ''}` }
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 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 createIdempotencyKey = ref('')
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
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') {
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(detail.application?.totalPackageCount ?? row.cartons),
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 || ''),
})
} catch (error) {
ElMessage.error(getApiErrorDetails(error, '换标单详情加载失败,请重试').message)
dialogOpen.value = false
} finally { detailLoading.value = false }
}
}
async function loadCustomers() {
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 = []; ElMessage.error(getApiErrorDetails(error, '客户列表加载失败,请重试').message) }
finally { customersLoading.value = false }
}
async function loadWarehouses() {
form.warehouseId = ''
warehouses.value = []
warehousesLoading.value = true
try {
const records = (await listRelabelWarehouseOptions()).records.filter((item) => item.status === 'ACTIVE')
warehouses.value = records
} catch (error) {
ElMessage.error(getApiErrorDetails(error, '公共海外收货仓加载失败,请重试').message)
} finally {
warehousesLoading.value = false
}
}
async function submitForm() {
if (submitting.value) return
try {
if (dialogMode.value === 'create') {
if (!form.customerId || !form.warehouseId) { ElMessage.warning('请选择客户和公共海外收货仓'); return }
if (!form.relatedOrderNo.trim() || !form.sku.trim() || !form.productName.trim() || !form.cartons || !form.relabelCost || form.relabelCost <= 0 || !form.labelDeadline || !form.contactName.trim() || !form.contactPhone.trim() || !form.shipmentNo.trim() || !form.newSkuCode.trim() || !form.labelRequirement) {
ElMessage.warning('请完整填写换标单必填信息')
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, items: [{ skuCode: form.sku.trim(), productNameZh: form.productName.trim(), totalPackageCount: Number(form.cartons), notifiedPackageCount: Number(form.cartons), shipmentNo: form.shipmentNo.trim(), newSkuCode: form.newSkuCode.trim(), labelRequirement: form.labelRequirement }] })
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) { ElMessage.error(getApiErrorDetails(error, '操作失败,请重试').message) }
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'
}
async function load(model: QueryModel = filters.value) {
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; quantityError.value = ''
const result = await listRelabelOrders(query)
try { quantities.value = await getRelabelQuantities(query) }
catch { quantityError.value = '数量汇总加载失败,请重试;未将当前页作为全部合计' }
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) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '换标单加载失败,请重试') }
finally { 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 class="quantity-summary" aria-label="全部筛选结果数量汇总"><template v-if="quantities"><strong>全部筛选结果</strong><span>{{ quantities.orderCount }} 票</span><span>{{ quantities.skuCount }} 种 SKU</span><span>{{ knownDimension(quantities.knownLabelPieceCount, quantities.unknownPieceOrders, '件') }}</span><span>{{ knownDimension(quantities.knownCartonCount, quantities.unknownCartonOrders, '箱') }}</span><span>原托盘/目标托盘数:未提供</span></template><span v-else-if="quantityError" role="alert">{{ quantityError }}</span></section>
<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' ? '新增换标单' : '审核换标单'" width="min(760px, calc(100vw - 28px))" :confirm-text="dialogMode === 'create' ? '保存换标单' : '提交审核'" :confirm-loading="submitting || detailLoading" :confirm-disabled="detailLoading" @confirm="submitForm">
<el-form class="density-form" label-position="top"><h3>订单信息</h3><el-row :gutter="16">
<el-col :span="12"><el-form-item label="换标单号"><el-input :model-value="dialogMode === 'create' ? '系统自动生成' : form.orderNo || '—'" readonly /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="关联订单" required><el-input v-model="form.relatedOrderNo" :disabled="dialogMode === 'edit'" placeholder="请输入关联订单号" /></el-form-item></el-col>
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="客户" required><el-select v-model="form.customerId" filterable :loading="customersLoading" placeholder="请选择 ACTIVE 客户"><el-option v-for="item in customers" :key="String(item.id)" :label="item.tenantName" :value="String(item.id)" /></el-select></el-form-item></el-col>
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="公共海外收货仓" required><el-select v-model="form.warehouseId" filterable :loading="warehousesLoading" placeholder="请选择 ACTIVE+RECEIVING 海外仓"><el-option v-for="item in warehouses" :key="item.id" :label="`${item.warehouseName} · ${item.warehouseCode}`" :value="String(item.id)" /></el-select></el-form-item></el-col>
<el-col :span="12"><el-form-item label="商品SKU" required><el-input v-model="form.sku" :type="dialogMode === 'edit' ? 'textarea' : 'text'" :autosize="{ minRows: 1, maxRows: 4 }" :disabled="dialogMode === 'edit'" placeholder="请输入商品SKU" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="中文品名" required><el-input v-model="form.productName" :disabled="dialogMode === 'edit'" placeholder="请输入中文品名" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="箱数" required><el-input-number v-model="form.cartons" :disabled="dialogMode === 'edit'" :min="1" :precision="0" controls-position="right" placeholder="请输入箱数" /></el-form-item></el-col>
<el-col v-if="dialogMode === 'create'" :span="12"><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></el-col>
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="联系人" required><el-input v-model="form.contactName" placeholder="请输入联系人" /></el-form-item></el-col>
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="联系电话" required><el-input v-model="form.contactPhone" placeholder="请输入联系电话" /></el-form-item></el-col>
<el-col v-if="dialogMode === 'edit'" :span="12"><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></el-col>
</el-row><h3>作业要求</h3><el-row :gutter="16">
<el-col :span="12"><el-form-item label="贴标要求" required><el-select v-model="form.labelRequirement" :disabled="dialogMode === 'edit'" placeholder="请选择贴标要求"><el-option v-for="value in ['换箱唛', '换SKU标', '换箱唛和SKU标']" :key="value" :label="value" :value="value" /></el-select></el-form-item></el-col>
<el-col :span="12"><el-form-item label="货件号" required><el-input v-model="form.shipmentNo" :disabled="dialogMode === 'edit'" placeholder="请输入货件号" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="新SKU" required><el-input v-model="form.newSkuCode" :type="dialogMode === 'edit' ? 'textarea' : 'text'" :autosize="{ minRows: 1, maxRows: 4 }" :disabled="dialogMode === 'edit'" placeholder="请输入新SKU" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="换标费用(CNY)" required><el-input-number v-model="form.relabelCost" :disabled="dialogMode === 'edit'" :min="0.01" :precision="2" controls-position="right" placeholder="请输入换标费用" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注(选填)" /></el-form-item></el-col>
</el-row></el-form>
</AppDialog>
</AdminPage>
</template>
<style scoped>
.quantity-summary { display: flex; flex-wrap: wrap; gap: 8px 18px; padding: 12px 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: var(--cc-bg-card, white); margin-bottom: 12px; font-size: 13px; }
h3 { margin: 0 0 var(--admin-space-5); color: var(--cc-text); font-size: var(--admin-font-section); }
h3:not(:first-child) { margin-top: var(--admin-space-1); padding-top: var(--admin-space-5); border-top: 1px solid var(--cc-border); }
.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%; }
.packing-plan-dialog { display: grid; gap: 14px; }
.packing-plan-summary { display: flex; gap: 24px; color: var(--cc-text-secondary); font-size: var(--admin-font-body); }
.packing-mode { margin-bottom: 2px; }
.packing-box { border: 1px solid var(--cc-border); border-radius: 6px; padding: 14px; display: grid; gap: 10px; }
.packing-box-head, .packing-item, .packing-dimensions { display: flex; align-items: center; gap: 10px; }
.packing-box-head { justify-content: space-between; }
.packing-item .el-input { flex: 1; }
.packing-item .el-input-number { width: 160px; }
.packing-dimensions .el-input-number { width: 150px; }
</style>