Files
admin-web/src/modules/overseas-warehouse/pages/RelabelOrdersView.vue
T
2026-09-29 20:52:21 +08:00

298 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, onMounted, reactive, ref, watch } from 'vue'
import { Plus, PackagePlus } from '@lucide/vue'
import { ElMessage } 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, createReturnPackingPlan, listRelabelOrders, listRelabelWarehouseOptions, type RelabelWarehouseOptionDto, type ReturnPackingPlanBox } 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()
const rows = ref<RelabelOrderListItem[]>([])
const loading = ref(false)
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 filters = ref<QueryModel>({ relabelNo: '', sku: '', status: '', reviewedDates: [], measuredDates: [], outboundDates: [] })
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: 'COMPLETED' }] },
{ key: 'reviewedDates', type: 'daterange', label: '审核时间', width: 240 },
{ key: 'measuredDates', type: 'daterange', label: '测量时间', width: 240 },
{ key: 'outboundDates', type: 'daterange', label: '出库时间', width: 240 },
])
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: '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: 'reviewedAt', label: '审核时间', minWidth: 170 },
{ key: 'measuredAt', label: '测量时间', minWidth: 170 },
{ key: 'outboundAt', label: '出库时间', minWidth: 170 },
{ key: 'createdAt', label: '创建时间', minWidth: 170 },
]
const dialogOpen = ref(false)
const dialogMode = ref<'create' | 'edit'>('create')
const submitting = 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<RelabelOrderListItem[]>([])
const packingDialogOpen = ref(false)
const packingSubmitting = ref(false)
const packingMode = ref<'CUSTOMER_INSTRUCTION' | 'WAREHOUSE_RECOMMENDATION'>('CUSTOMER_INSTRUCTION')
const packingRemark = ref('')
const packingBoxes = ref<ReturnPackingPlanBox[]>([])
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 selectedSkus() {
const values = new Map<string, number>()
for (const row of selectedRows.value) {
const skus = String(row.sku || '').split(',').map((value) => value.trim()).filter(Boolean)
for (const sku of skus.length ? skus : ['未维护 SKU']) values.set(sku, (values.get(sku) || 0) + Math.max(0, Number(row.cartons || 0)))
}
return [...values.entries()].map(([skuCode, quantity]) => ({ skuCode, quantity }))
}
function openPackingPlan() {
if (!selectedRows.value.length) { ElMessage.warning('请先勾选已入库退货订单'); return }
packingMode.value = 'CUSTOMER_INSTRUCTION'
packingRemark.value = ''
packingBoxes.value = selectedSkus().map((item, index) => ({ boxNo: index + 1, items: [{ ...item }] }))
packingDialogOpen.value = true
}
function addPackingBox() { packingBoxes.value.push({ boxNo: packingBoxes.value.length + 1, items: [{ skuCode: '', quantity: 0 }] }) }
function removePackingBox(index: number) { if (packingBoxes.value.length > 1) packingBoxes.value.splice(index, 1) }
function addBoxItem(box: ReturnPackingPlanBox) { box.items.push({ skuCode: '', quantity: 0 }) }
function removeBoxItem(box: ReturnPackingPlanBox, index: number) { if (box.items.length > 1) box.items.splice(index, 1) }
async function submitPackingPlan() {
if (packingSubmitting.value) return
const boxes = packingBoxes.value.map((box, index) => ({ ...box, boxNo: index + 1, items: box.items.filter((item) => item.skuCode.trim() && Number(item.quantity) > 0).map((item) => ({ skuCode: item.skuCode.trim(), quantity: Number(item.quantity) })) })).filter((box) => box.items.length)
if (!boxes.length) { ElMessage.warning('至少配置一个箱和一条 SKU 数量'); return }
packingSubmitting.value = true
try {
const result = await createReturnPackingPlan({ returnNos: selectedRows.value.map((row) => row.relatedOrderNo).filter((value) => value && value !== '—'), mode: packingMode.value, boxes, remark: packingRemark.value.trim() || undefined, idempotencyKey: globalThis.crypto?.randomUUID?.() || `packing-${Date.now()}` })
ElMessage.success(`合箱计划 ${result.planNo} 已推送 PDA`)
packingDialogOpen.value = false
selectedRows.value = []
await load()
} catch (error) { ElMessage.error(getApiErrorDetails(error, '合箱计划推送失败,请重试').message) }
finally { packingSubmitting.value = false }
}
function openForm(mode: 'create' | 'edit', source?: object) {
const row = source as RelabelOrderListItem | 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
}
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 RelabelOrderListItem
void router.push({ name: 'overseas-relabel-order-detail', params: { orderId: String(row.orderNo) } })
}
function relabelStatus(value: unknown) {
return relabelBusinessStatusLabel(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 RelabelOrderListItem
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 reviewedDates = Array.isArray(filters.value.reviewedDates) ? filters.value.reviewedDates : []
const measuredDates = Array.isArray(filters.value.measuredDates) ? filters.value.measuredDates : []
const outboundDates = Array.isArray(filters.value.outboundDates) ? filters.value.outboundDates : []
const result = await listRelabelOrders({ 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, reviewedFrom: String(reviewedDates[0] || '') || undefined, reviewedTo: String(reviewedDates[1] || '') || undefined, measuredFrom: String(measuredDates[0] || '') || undefined, measuredTo: String(measuredDates[1] || '') || undefined, outboundFrom: String(outboundDates[0] || '') || undefined, outboundTo: String(outboundDates[1] || '') || undefined })
rows.value = result.records.map((item) => ({
customerId: '', orderNo: String(item.relabelNo), relatedOrderNo: String(item.lastlegOrderNo || item.customerOrderNo || '—'), sku: String(item.skuCodes || '—'),
productName: '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '', reviewedAt: item.reviewedAt, measuredAt: item.measuredAt, outboundAt: item.outboundAt,
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)
watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
</script>
<template>
<AdminPage class="admin-density"><ListPageLayout aria-label="换标单列表">
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading" ariaLabel="换标单筛选" preference-key="overseas-relabel-orders-query" :search-on-reset="false" @search="search" @reset="reset" /></template>
<template v-if="loadError" #state><LoadErrorState title="换标单加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
<section v-if="!loadError" 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-relabel-orders" row-key="orderNo" empty-text="暂无换标单" @selection-change="selectedRows = $event" @update:page="load()" @update:page-size="page = 1; load()">
<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"><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></div></template>
<template #pagination-left>共 {{ total }} 条换标单</template>
</DataTable></section>
</ListPageLayout>
<AppDialog class="admin-density-dialog" v-model="packingDialogOpen" title="配置退货合箱计划" width="min(980px, calc(100vw - 28px))" confirm-text="推送 PDA 合箱计划" :confirm-loading="packingSubmitting" @confirm="submitPackingPlan">
<div class="packing-plan-dialog">
<div class="packing-plan-summary"><span>已选订单:{{ selectedRows.length }} 个</span><span>SKU:{{ selectedSkus().length }} 种</span><span>默认:单 SKU 一箱,可调整为混箱</span></div>
<el-radio-group v-model="packingMode" class="packing-mode"><el-radio-button label="CUSTOMER_INSTRUCTION">客户合箱指令</el-radio-button><el-radio-button label="WAREHOUSE_RECOMMENDATION">仓库自主合箱</el-radio-button></el-radio-group>
<div v-for="(box, boxIndex) in packingBoxes" :key="boxIndex" class="packing-box"><div class="packing-box-head"><strong>第 {{ boxIndex + 1 }} 箱</strong><el-button text type="danger" :disabled="packingBoxes.length <= 1" @click="removePackingBox(boxIndex)">删除箱</el-button></div><div v-for="(item, itemIndex) in box.items" :key="itemIndex" class="packing-item"><el-input v-model="item.skuCode" placeholder="SKU" /><el-input-number v-model="item.quantity" :min="0" :precision="0" controls-position="right" placeholder="数量" /><el-button text type="danger" :disabled="box.items.length <= 1" @click="removeBoxItem(box, itemIndex)">移除</el-button></div><el-button text type="primary" @click="addBoxItem(box)">+ 添加 SKU</el-button><div class="packing-dimensions"><el-input-number v-model="box.length" :min="0" :precision="2" placeholder="长 cm" /><el-input-number v-model="box.width" :min="0" :precision="2" placeholder="宽 cm" /><el-input-number v-model="box.height" :min="0" :precision="2" placeholder="高 cm" /><el-input-number v-model="box.weight" :min="0" :precision="2" placeholder="重量 kg" /></div></div>
<el-button plain type="primary" @click="addPackingBox">+ 添加箱</el-button>
<el-input v-model="packingRemark" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="合箱备注(选填)" />
</div>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="dialogOpen" :title="dialogMode === 'create' ? '新增换标单' : '审核换标单'" width="min(760px, calc(100vw - 28px))" :confirm-text="dialogMode === 'create' ? '保存换标单' : '提交审核'" :confirm-loading="submitting" @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" :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" :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>
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: 13px; }
.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>