This commit is contained in:
2026-08-12 16:31:46 +08:00
parent c15d21b986
commit d8bbfe05f3
40 changed files with 1541 additions and 248 deletions
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const panel = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/ContainerFeesPanel.vue'), 'utf8')
const detail = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/LoadingListDetailDrawer.vue'), 'utf8')
describe('ContainerFeesPanel contract', () => {
it('is exposed from loading-list detail and derives customer context from real items', () => {
expect(detail).toContain('<ContainerFeesPanel :plan-id="summary.planId" :customers="feeCustomers" />')
expect(detail).toContain('item.accountId')
expect(panel).toContain('该柜次包含多个客户,请先选择费用归属客户')
expect(panel).toContain('装柜明细缺少客户归属,无法安全维护费用')
})
it('covers create, review, reject, approved adjustment and void actions', () => {
expect(panel).toContain('新增费用')
expect(panel).toContain("row.reviewStatus === 'PENDING'")
expect(panel).toContain('approveContainerFee')
expect(panel).toContain('rejectContainerFee')
expect(panel).toContain("row.reviewStatus === 'APPROVED'")
expect(panel).toContain('adjustContainerFee')
expect(panel).toContain('voidContainerFee')
expect(panel).toContain('调整后版本递增并重新进入待审核状态')
})
it('shows only server allocations and supplier snapshots', () => {
expect(panel).toContain('listContainerCostDetails')
expect(panel).toContain('仅展示 Finance 已持久化结果')
expect(panel).toContain("cost.supplierName || '服务端未返回'")
expect(panel).toContain('尚无与该柜次关联的财务分摊结果')
expect(panel).not.toMatch(/allocatedAmount\s*=|allocationRate\s*=/)
})
})
@@ -0,0 +1,143 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { Check, CircleX, Pencil, Plus, Trash2 } from '@lucide/vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { adjustContainerFee, approveContainerFee, createContainerFee, listApplicableFeeItems, listContainerCostDetails, listContainerFees, rejectContainerFee, voidContainerFee, type ApplicableFeeItem, type ContainerCostDetail, type ContainerFeeDirection, type ContainerFeeResource, type ContainerFeeSaveRequest } from '../api/containerFees'
const props = defineProps<{ planId: string; customers: Array<{ accountId: string; customerName: string | null }> }>()
const access = useAccessPreviewStore()
const accountId = ref('')
const fees = ref<ContainerFeeResource[]>([])
const feeItems = ref<ApplicableFeeItem[]>([])
const costs = ref<ContainerCostDetail[]>([])
const loading = ref(false)
const error = ref<ApiErrorDetails | null>(null)
const editorOpen = ref(false)
const rejectOpen = ref(false)
const saving = ref(false)
const target = ref<ContainerFeeResource | null>(null)
const formRef = ref<FormInstance>()
const form = reactive<ContainerFeeSaveRequest>({ feeItemId: '', direction: 'RECEIVABLE', businessDate: new Date().toISOString().slice(0, 10), amount: 0, currency: 'CNY', settlementIncluded: true, remark: null })
const rejection = ref('')
const canView = computed(() => access.can('firstleg:inbound:fee-detail:view'))
const canEdit = computed(() => access.can('firstleg:inbound:fee-detail:edit'))
const canReview = computed(() => access.can('firstleg:inbound:fee-detail:review'))
const customerOptions = computed(() => {
const unique = new Map<string, string>()
for (const item of props.customers) if (item.accountId) unique.set(item.accountId, item.customerName || '客户信息缺失')
return [...unique].map(([value, label]) => ({ value, label }))
})
const selectedCustomer = computed(() => customerOptions.value.find((item) => item.value === accountId.value))
const applicableItems = computed(() => feeItems.value.filter((item) => form.direction === 'RECEIVABLE'
? item.warehouseReceivable || item.pickupReceivable : item.warehousePayable || item.pickupPayable))
const containerId = computed(() => fees.value.find((item) => item.containerBookingId)?.containerBookingId || null)
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', fixed: 'left', width: 300, slotName: 'actions', hideable: false, movable: false },
{ key: 'feeItemName', prop: 'feeItemName', label: '费用项目', minWidth: 150 },
{ key: 'direction', label: '方向', width: 90, slotName: 'direction' },
{ key: 'amount', label: '金额(CNY)', width: 120, align: 'right', slotName: 'amount' },
{ key: 'businessDate', prop: 'businessDate', label: '业务日期', width: 120 },
{ key: 'reviewStatus', label: '审核状态', width: 110, slotName: 'reviewStatus' },
{ key: 'sourceStatus', label: '来源状态', width: 100, slotName: 'sourceStatus' },
{ key: 'settlementIncluded', label: '参与结算', width: 100, slotName: 'settlementIncluded' },
{ key: 'remark', prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
]
const allocationColumns: DataTableColumn[] = [
{ key: 'orderId', prop: 'orderId', label: '订单', fixed: 'left', minWidth: 150 },
{ key: 'inboundId', prop: 'inboundId', label: '入库单', minWidth: 150 },
{ key: 'volumeCbm', label: '订单体积(m³)', width: 125, align: 'right', slotName: 'volume' },
{ key: 'allocationRate', label: '分摊比例', width: 110, align: 'right', slotName: 'rate' },
{ key: 'allocatedAmount', label: '分摊金额(CNY)', width: 135, align: 'right', slotName: 'allocated' },
]
const rules: FormRules = { feeItemId: [{ required: true, message: '请选择费用项目', trigger: 'change' }], businessDate: [{ required: true, message: '请选择业务日期', trigger: 'change' }], amount: [{ type: 'number', min: 0, message: '金额不能小于 0', trigger: 'change' }] }
function resetForm(item?: ContainerFeeResource) {
target.value = item || null
Object.assign(form, item ? { feeItemId: item.feeItemId, direction: item.direction, businessDate: item.businessDate, amount: item.amount, currency: 'CNY', settlementIncluded: item.settlementIncluded, remark: item.remark }
: { feeItemId: '', direction: 'RECEIVABLE', businessDate: new Date().toISOString().slice(0, 10), amount: 0, currency: 'CNY', settlementIncluded: true, remark: null })
}
async function load() {
if (!accountId.value || !canView.value) return
loading.value = true; error.value = null
try {
const [feeRows, items] = await Promise.all([listContainerFees(props.planId, accountId.value), listApplicableFeeItems()])
fees.value = feeRows; feeItems.value = items
costs.value = []
const currentContainerId = feeRows.find((item) => item.containerBookingId)?.containerBookingId
if (currentContainerId) costs.value = await listContainerCostDetails(accountId.value, currentContainerId)
} catch (cause) { error.value = getApiErrorDetails(cause, '柜次费用加载失败') }
finally { loading.value = false }
}
function openCreate() { resetForm(); editorOpen.value = true }
function openAdjust(item: ContainerFeeResource) { resetForm(item); editorOpen.value = true }
async function save() {
if (!accountId.value || !await formRef.value?.validate().catch(() => false)) return
saving.value = true
try {
if (target.value) await adjustContainerFee(props.planId, accountId.value, target.value.id, target.value.sourceVersion, { ...form })
else await createContainerFee(props.planId, accountId.value, { ...form })
editorOpen.value = false; ElMessage.success(target.value ? '费用调整已提交审核' : '费用已新增'); await load()
} catch (cause) { ElMessage.error(getApiErrorDetails(cause, '费用保存失败').message) }
finally { saving.value = false }
}
async function approve(item: ContainerFeeResource) { await approveContainerFee(props.planId, accountId.value, item.id).then(() => ElMessage.success('费用已审核通过')).then(load).catch((cause) => ElMessage.error(getApiErrorDetails(cause, '审核失败').message)) }
function openReject(item: ContainerFeeResource) { target.value = item; rejection.value = ''; rejectOpen.value = true }
async function reject() {
if (!target.value || !rejection.value.trim()) return ElMessage.warning('请输入驳回原因')
saving.value = true
try { await rejectContainerFee(props.planId, accountId.value, target.value.id, rejection.value.trim()); rejectOpen.value = false; ElMessage.success('费用已驳回'); await load() }
catch (cause) { ElMessage.error(getApiErrorDetails(cause, '驳回失败').message) } finally { saving.value = false }
}
async function voidFee(item: ContainerFeeResource) {
await ElMessageBox.confirm(`确认作废“${item.feeItemName}”费用?作废后将同步财务来源状态。`, '作废费用', { type: 'warning', confirmButtonText: '确认作废' })
await voidContainerFee(props.planId, accountId.value, item.id, item.sourceVersion).then(() => ElMessage.success('费用已作废')).then(load).catch((cause) => ElMessage.error(getApiErrorDetails(cause, '作废失败').message))
}
function money(value: number) { return value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }
function initializeCustomer() { accountId.value = customerOptions.value.length === 1 ? customerOptions.value[0].value : '' }
watch(customerOptions, initializeCustomer, { immediate: true })
watch(accountId, () => { fees.value = []; costs.value = []; void load() })
watch(() => form.direction, () => { if (!applicableItems.value.some((item) => item.id === form.feeItemId)) form.feeItemId = '' })
onMounted(() => void load())
</script>
<template>
<section class="fees-panel surface-panel" aria-label="柜次费用维护">
<header class="fees-header"><div><h2>柜次费用</h2><p>费用与财务分摊均来自服务端真实记录</p></div><el-button v-if="canEdit && accountId" type="primary" @click="openCreate"><Plus :size="16" />新增费用</el-button></header>
<el-alert v-if="!canView" type="warning" :closable="false" show-icon title="当前账号没有查看柜次费用的权限" />
<template v-else>
<div v-if="customerOptions.length > 1" class="customer-context"><span>客户</span><el-select v-model="accountId" placeholder="请选择费用归属客户"><el-option v-for="item in customerOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></div>
<el-alert v-if="customerOptions.length === 0" type="warning" :closable="false" show-icon title="装柜明细缺少客户归属,无法安全维护费用" />
<el-alert v-else-if="!accountId" type="info" :closable="false" show-icon title="该柜次包含多个客户,请先选择费用归属客户" />
<LoadErrorState v-else-if="error" compact title="柜次费用加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<DataTable v-else :data="fees" :columns="columns" :loading="loading" preference-key="container-fees" row-key="id" empty-text="当前客户暂无柜次费用">
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canReview && row.reviewStatus === 'PENDING'" link type="success" @click="approve(row as ContainerFeeResource)"><Check :size="15" />通过</el-button><el-button v-if="canReview && row.reviewStatus === 'PENDING'" link type="danger" @click="openReject(row as ContainerFeeResource)"><CircleX :size="15" />驳回</el-button><el-button v-if="canEdit && row.reviewStatus === 'APPROVED' && row.sourceStatus === 'ACTIVE'" link type="primary" @click="openAdjust(row as ContainerFeeResource)"><Pencil :size="15" />调整</el-button><el-button v-if="canReview && row.reviewStatus === 'APPROVED' && row.sourceStatus === 'ACTIVE'" link type="danger" @click="voidFee(row as ContainerFeeResource)"><Trash2 :size="15" />作废</el-button></div></template>
<template #direction="{ row }"><el-tag :type="row.direction === 'RECEIVABLE' ? 'success' : 'warning'" effect="plain">{{ row.direction === 'RECEIVABLE' ? '应收' : '应付' }}</el-tag></template>
<template #amount="{ row }">{{ money(row.amount) }}</template><template #reviewStatus="{ row }"><StatusBadge :status="row.reviewStatus === 'PENDING' ? '待审核' : row.reviewStatus === 'APPROVED' ? '已通过' : '已驳回'" /></template><template #sourceStatus="{ row }"><StatusBadge :status="row.sourceStatus === 'ACTIVE' ? '有效' : '已作废'" /></template><template #settlementIncluded="{ row }">{{ row.settlementIncluded ? '是' : '否' }}</template>
</DataTable>
<div v-if="accountId" class="allocation-section"><header><div><h3>财务体积分摊</h3><p>{{ selectedCustomer?.label }} · 仅展示 Finance 已持久化结果</p></div></header><el-empty v-if="!loading && costs.length === 0" description="尚无与该柜次关联的财务分摊结果" :image-size="64" /><article v-for="cost in costs" :key="cost.id" class="cost-block"><div class="cost-summary"><strong>{{ cost.feeItemName || '柜次成本' }}</strong><span>往来单位:{{ cost.supplierName || '服务端未返回' }}</span><span>金额:¥{{ money(cost.amount) }}</span></div><DataTable :data="cost.allocations" :columns="allocationColumns" :preference-key="`container-cost-${cost.id}`" row-key="id" empty-text="服务端未返回分摊明细"><template #volume="{ row }">{{ row.volumeCbm.toFixed(3) }}</template><template #rate="{ row }">{{ (row.allocationRate * 100).toFixed(2) }}%</template><template #allocated="{ row }">{{ money(row.allocatedAmount) }}</template></DataTable></article></div>
</template>
<AppDialog v-model="editorOpen" :title="target ? '调整已审核费用' : '新增柜次费用'" confirm-text="保存" :confirm-loading="saving" @confirm="save"><el-alert v-if="target" type="info" :closable="false" show-icon title="调整后版本递增并重新进入待审核状态" /><el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="fee-form"><el-form-item label="方向" prop="direction"><el-segmented v-model="form.direction" :options="[{ label: '应收', value: 'RECEIVABLE' }, { label: '应付', value: 'PAYABLE' }]" /></el-form-item><el-form-item label="费用项目" prop="feeItemId"><el-select v-model="form.feeItemId" filterable placeholder="请选择启用且适用的费用项目"><el-option v-for="item in applicableItems" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item><el-form-item label="业务日期" prop="businessDate"><el-date-picker v-model="form.businessDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="金额(CNY)" prop="amount"><el-input-number v-model="form.amount" :min="0" :precision="2" controls-position="right" /></el-form-item><el-form-item label="参与结算"><el-switch v-model="form.settlementIncluded" /></el-form-item><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item></el-form></AppDialog>
<AppDialog v-model="rejectOpen" title="驳回柜次费用" confirm-text="确认驳回" :confirm-loading="saving" @confirm="reject"><el-input v-model="rejection" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入驳回原因" /></AppDialog>
</section>
</template>
<style scoped>
.fees-panel { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); background: #fff; }
.fees-header, .allocation-section > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 18px; }
.fees-header h2, .allocation-section h3 { margin: 0; font-size: 16px; }.fees-header p, .allocation-section p { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; }
.customer-context { display: flex; align-items: center; gap: 12px; padding: 0 18px 16px; }.customer-context > span { color: var(--cc-muted); font-size: 13px; }.customer-context :deep(.el-select) { width: min(320px, 100%); }
.row-actions { display: flex; flex-wrap: wrap; }.allocation-section { border-top: 1px solid var(--cc-border); }.cost-block { border-top: 1px solid var(--cc-border); }.cost-summary { display: flex; flex-wrap: wrap; gap: 12px 20px; padding: 14px 18px; background: #f8fafc; font-size: 13px; }.cost-summary span { color: var(--cc-muted); }.fee-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; margin-top: 14px; }.fee-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
@media (max-width: 640px) { .fees-header { align-items: stretch; flex-direction: column; }.fees-header .el-button { width: 100%; }.fee-form { grid-template-columns: 1fr; }.fee-form :deep(.el-form-item:last-child) { grid-column: auto; }.customer-context { align-items: stretch; flex-direction: column; } }
</style>
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Pencil } from '@lucide/vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import ContainerFeesPanel from './ContainerFeesPanel.vue'
import { loadingListStatusLabel } from '../status'
import type { ApiErrorDetails } from '@/shared/api/http'
import type { LoadingDetail, LoadingListMode, LoadingListSummary, PreloadSummary } from '../api/platformLoadingLists'
@@ -19,7 +19,6 @@ const props = defineProps<{
canEdit: boolean
}>()
const open = defineModel<boolean>({ default: false })
const emit = defineEmits<{ retry: []; edit: [] }>()
const itemColumns: DataTableColumn[] = [
@@ -43,6 +42,11 @@ const itemColumns: DataTableColumn[] = [
const summary = computed<LoadingListSummary | null>(() => props.detail?.summary || null)
const preloadSummary = computed(() => props.mode === 'preload' ? summary.value as PreloadSummary | null : null)
const feeCustomers = computed(() => {
const customers = new Map<string, string | null>()
for (const item of props.detail?.items || []) if (item.accountId) customers.set(item.accountId, item.customerName)
return [...customers].map(([accountId, customerName]) => ({ accountId, customerName }))
})
const title = computed(() => {
if (!summary.value) return props.mode === 'preload' ? '预装清单详情' : '装柜清单详情'
const number = props.mode === 'preload'
@@ -70,14 +74,7 @@ function formatNumber(value?: number | null, digits = 3) {
</script>
<template>
<AppDrawer
v-model="open"
:title="title"
size="min(1180px, 96vw)"
:loading="loading"
:show-footer="false"
destroy-on-close
>
<section v-loading="loading" class="loading-detail-content" :aria-label="title" :aria-busy="loading">
<LoadErrorState v-if="error" title="清单详情加载失败" :message="error.message" @retry="emit('retry')">
<template #details><ApiErrorMeta :details="error" /></template>
</LoadErrorState>
@@ -122,6 +119,8 @@ function formatNumber(value?: number | null, digits = 3) {
</DataTable>
</section>
<ContainerFeesPanel :plan-id="summary.planId" :customers="feeCustomers" />
<section class="history-section surface-panel" aria-label="操作历史">
<header><h2>操作历史</h2><span>{{ detail.histories.length }} 条</span></header>
<el-empty v-if="detail.histories.length === 0" description="暂无操作历史" :image-size="72" />
@@ -137,11 +136,11 @@ function formatNumber(value?: number | null, digits = 3) {
</section>
</DetailPageLayout>
</div>
</AppDrawer>
</section>
</template>
<style scoped>
.detail-layout { display: grid; gap: 16px; }
.loading-detail-content, .detail-layout { display: grid; min-width: 0; gap: 16px; }
.detail-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; overflow: hidden; border: 1px solid var(--cc-border); background: var(--cc-border); }
.detail-summary > div { display: flex; min-width: 0; min-height: 78px; flex-direction: column; justify-content: center; gap: 7px; padding: 14px 16px; background: #fff; }
.detail-summary span { color: var(--cc-muted); font-size: 12px; }
@@ -4,6 +4,7 @@ import { resolve } from 'node:path'
const source = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/LoadingListsView.vue'), 'utf8')
const detailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/LoadingListDetailDrawer.vue'), 'utf8')
const preloadDetailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/pages/PreloadPlanDetailView.vue'), 'utf8')
describe('LoadingListsView', () => {
it('keeps the operation column first and fixed left in both lists', () => {
@@ -24,13 +25,31 @@ describe('LoadingListsView', () => {
expect(source).toContain('MISSING_WAREHOUSE_CONTEXT')
})
it('uses the shared query, table, drawer, dialog and error components', () => {
it('uses the shared query, table, dialog and error components without a detail drawer', () => {
expect(source).toContain('QueryToolbar')
expect(source).toContain('DataTable')
expect(source).toContain('AppDialog')
expect(source).toContain('LoadErrorState')
expect(source).toContain('ApiErrorMeta')
expect(detailSource).toContain('AppDrawer')
expect(detailSource).not.toContain('AppDrawer')
expect(detailSource).toContain('ContainerFeesPanel')
})
it('opens preload detail as a route and confirmed detail inline on the list page', () => {
expect(source).toContain("name: 'preload-plan-detail'")
expect(source).toContain('v-if="!isPreload && (detailLoading || detailError || detail)"')
expect(source).toContain('class="confirmed-inline-detail surface-panel"')
expect(preloadDetailSource).toContain('返回列表')
expect(preloadDetailSource).toContain('router.back()')
})
it('keeps the real transport edit contract in a 620px centered modal', () => {
expect(source).toContain('class="loading-transport-dialog"')
expect(source).toContain('width="min(620px, calc(100vw - 28px))"')
expect(preloadDetailSource).toContain('width="min(620px, calc(100vw - 28px))"')
expect(source).not.toContain('备注信息(仅自己可见)')
expect(source).not.toContain('备注信息(客户可见)')
expect(source).not.toContain('备注信息(仓库可见)')
})
it('only exposes transport editing for LOADING preload details', () => {
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { Eye, Pencil } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
@@ -31,6 +31,7 @@ import {
const props = defineProps<{ mode: LoadingListMode }>()
const route = useRoute()
const router = useRouter()
const access = useAccessPreviewStore()
const rows = ref<LoadingListSummary[]>([])
const loading = ref(false)
@@ -39,7 +40,6 @@ const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const query = reactive<QueryModel>({ keyword: '', createdRange: [] })
const detailOpen = ref(false)
const detailLoading = ref(false)
const detailError = ref<ApiErrorDetails | null>(null)
const detail = ref<LoadingDetail | null>(null)
@@ -150,11 +150,14 @@ async function openDeepLinkedDetail() {
const key = `${props.mode}:${planId}:${warehouseId}`
if (handledDeepLink.value === key) return
handledDeepLink.value = key
if (isPreload.value) {
await router.replace({ name: 'preload-plan-detail', params: { planId }, query: { warehouseId } })
return
}
selectedPlanId.value = planId
selectedWarehouseId.value = warehouseId
detail.value = null
detailError.value = null
detailOpen.value = true
await loadDetail()
const expectedStatuses = isPreload.value ? ['EDITING', 'LOADING'] : ['COMPLETED']
const loadedDetail = detail.value as LoadingDetail | null
@@ -198,11 +201,14 @@ async function loadDetail() {
function openDetail(rowValue: object) {
const row = rowValue as LoadingListSummary
if (isPreload.value) {
void router.push({ name: 'preload-plan-detail', params: { planId: String(row.planId) }, query: { warehouseId: String(row.warehouseId || '') } })
return
}
selectedPlanId.value = String(row.planId)
selectedWarehouseId.value = String(row.warehouseId || '')
detail.value = null
detailError.value = null
detailOpen.value = true
void loadDetail()
}
@@ -213,7 +219,6 @@ async function openEditFromRow(rowValue: object) {
selectedWarehouseId.value = String(row.warehouseId || '')
detail.value = null
detailError.value = null
detailOpen.value = true
await loadDetail()
const loadedDetail = detail.value as LoadingDetail | null
if (loadedDetail && ['EDITING', 'LOADING'].includes(loadedDetail.summary.status)) openEdit()
@@ -317,7 +322,8 @@ onMounted(() => {
</ListPageLayout>
<LoadingListDetailDrawer
v-model="detailOpen"
v-if="!isPreload && (detailLoading || detailError || detail)"
class="confirmed-inline-detail surface-panel"
:mode="mode"
:loading="detailLoading"
:detail="detail"
@@ -329,7 +335,9 @@ onMounted(() => {
<AppDialog
v-model="editOpen"
class="loading-transport-dialog"
title="编辑预装运输信息"
width="min(620px, calc(100vw - 28px))"
confirm-text="保存"
:confirm-loading="editSaving"
:confirm-disabled="!canEdit"
@@ -352,7 +360,9 @@ onMounted(() => {
<style scoped>
.loading-lists-page { display: grid; min-width: 0; gap: 16px; }
.table-panel { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); background: #fff; }
.confirmed-inline-detail { padding: 20px; border: 1px solid var(--cc-border); background: #fff; }
.transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; margin-top: 16px; }
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
@media (max-width: 640px) { .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } }
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
@media (max-width: 720px) { .confirmed-inline-detail { padding: 12px; } .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } }
</style>