This commit is contained in:
2026-09-15 11:54:19 +08:00
parent a807071ed2
commit ff25847b02
19 changed files with 93 additions and 80 deletions
@@ -15,7 +15,7 @@ describe('DeliveryOrderDetailView', () => {
order: {
version: 3, allowedActions: ['COMPLETE_DELIVERY'],
id: '1', customerId: '100', deliveryNo: 'DEL/1', outboundNo: 'OUT-1', dispatchNo: 'PC-1',
status: 'DELIVERING', overseasWarehouseName: '洛杉矶仓', palletCount: 1, packageCount: 2,
status: 'DELIVERING', overseasWarehouseName: '洛杉矶仓', destinationWarehouseCode: 'ACY2', palletCount: 1, packageCount: 2,
createdAt: '2026-09-01T10:00:00',
},
skus: [], fees: [
@@ -32,6 +32,7 @@ describe('DeliveryOrderDetailView', () => {
expect(wrapper.text()).toContain('DEL/1')
expect(wrapper.text()).toContain('派送中')
expect(wrapper.text()).toContain('洛杉矶仓')
expect(wrapper.text()).toContain('ACY2')
await wrapper.find('.el-tabs__item:nth-child(3)').trigger('click')
expect(wrapper.text()).toContain('私人仓尾程派送费')
expect(wrapper.text()).toContain('派送费用')
@@ -68,10 +68,11 @@ watch([deliveryNo, customerId], () => { void load() }, { immediate: true })
<section class="tab-content" aria-label="订单信息">
<dl v-if="detail" class="info-grid density-fields">
<div class="info-item"><dt>派送单号</dt><dd>{{ show(detail.order.deliveryNo) }}</dd></div><div class="info-item"><dt>状态</dt><dd>{{ statusLabels[detail.order.status] || detail.order.status }}</dd></div>
<div class="info-item"><dt>仓库</dt><dd>{{ show(detail.order.overseasWarehouseName) }}</dd></div><div class="info-item"><dt>运单号</dt><dd>{{ show(detail.order.waybillNo) }}</dd></div>
<div class="info-item"><dt>派送人</dt><dd>{{ show(detail.order.carrierName) }}</dd></div><div class="info-item"><dt>派送时间</dt><dd>{{ formatOverseasDateTime(detail.order.dispatchedAt || detail.order.plannedDeliveryAt) }}</dd></div>
<div class="info-item"><dt>提货方式</dt><dd>{{ pickupLabels[detail.order.pickupMethod || ''] || show(detail.order.pickupMethod) }}</dd></div><div class="info-item"><dt>装车类型</dt><dd>{{ loadingLabels[detail.order.loadingType || ''] || show(detail.order.loadingType) }}</dd></div>
<div class="info-item"><dt>派送方式</dt><dd>{{ methodLabels[detail.order.deliveryMethod || ''] || show(detail.order.deliveryMethod) }}</dd></div><div class="info-item"><dt>司机/车队</dt><dd>{{ show(detail.order.driverName) }}</dd></div>
<div class="info-item"><dt>仓库</dt><dd>{{ show(detail.order.overseasWarehouseName) }}</dd></div><div class="info-item"><dt>目的 FBA 仓</dt><dd>{{ show(detail.order.destinationWarehouseCode || detail.order.destinationWarehouseName) }}</dd></div>
<div class="info-item"><dt>运单号</dt><dd>{{ show(detail.order.waybillNo) }}</dd></div><div class="info-item"><dt>派送人</dt><dd>{{ show(detail.order.carrierName) }}</dd></div>
<div class="info-item"><dt>派送时间</dt><dd>{{ formatOverseasDateTime(detail.order.dispatchedAt || detail.order.plannedDeliveryAt) }}</dd></div><div class="info-item"><dt>提货方式</dt><dd>{{ pickupLabels[detail.order.pickupMethod || ''] || show(detail.order.pickupMethod) }}</dd></div>
<div class="info-item"><dt>装车类型</dt><dd>{{ loadingLabels[detail.order.loadingType || ''] || show(detail.order.loadingType) }}</dd></div><div class="info-item"><dt>派送方式</dt><dd>{{ methodLabels[detail.order.deliveryMethod || ''] || show(detail.order.deliveryMethod) }}</dd></div>
<div class="info-item"><dt>司机/车队</dt><dd>{{ show(detail.order.driverName) }}</dd></div>
<div class="info-item"><dt>车牌号</dt><dd>{{ show(detail.order.vehiclePlate) }}</dd></div><div class="info-item"><dt>总箱数</dt><dd>{{ quantity(detail.order.packageCount, '箱') }}</dd></div>
<div class="info-item"><dt>总重量</dt><dd>{{ quantity(detail.order.totalWeightKg, 'kg') }}</dd></div><div class="info-item"><dt>总体积</dt><dd>{{ quantity(detail.order.totalVolumeCbm, 'm³') }}</dd></div>
<div class="info-item"><dt>预计到达时间</dt><dd>{{ formatOverseasDateTime(detail.order.expectedArrivalAt) }}</dd></div><div class="info-item"><dt>客户确认</dt><dd>{{ customerConfirmation() }}</dd></div>
@@ -1,6 +1,6 @@
import ElementPlus from 'element-plus'
import { defineComponent, h, type PropType } from 'vue'
import { DOMWrapper, flushPromises, mount } from '@vue/test-utils'
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getDeliveryOrder, listDeliveryOrders, listDeliveryReturnInboundOptions, markDeliveryReturn } from '../api/deliveryOrders'
import { updateDeliveryOrderStatus } from '../api/customerVisits'
@@ -54,10 +54,10 @@ describe('DeliveryOrdersView permissions and commands', () => {
await wrapper.findAll('button').find(button => button.text().includes('编辑状态'))!.trigger('click')
await flushPromises()
await wrapper.find('[data-dialog="编辑派送状态"] select').setValue('DELIVERED')
await wrapper.find('[data-dialog="编辑派送状态"] select').setValue('DELIVERING')
await wrapper.find('[data-dialog="编辑派送状态"] .dialog-confirm').trigger('click')
await flushPromises()
expect(updateDeliveryOrderStatus).toHaveBeenCalledWith('DEL/1', '100', { status: 'DELIVERED', expectedVersion: 7, remark: undefined })
expect(updateDeliveryOrderStatus).toHaveBeenCalledWith('DEL/1', '100', { status: 'DELIVERING', expectedVersion: 7, remark: undefined })
await wrapper.findAll('button').find(button => button.text().includes('标记退货'))!.trigger('click')
await flushPromises()
@@ -86,7 +86,7 @@ describe('DeliveryOrdersView permissions and commands', () => {
wrapper.unmount()
})
it('maps delivering separately from completion and blocks duplicate or conflicted submission', async () => {
it('excludes manual completion and blocks duplicate or conflicted status submission', async () => {
canPermission.mockReturnValue(true)
const wrapper = mountView()
await flushPromises()
@@ -94,7 +94,7 @@ describe('DeliveryOrdersView permissions and commands', () => {
await flushPromises()
const select = wrapper.find('[data-dialog="编辑派送状态"] select')
expect(select.find('option[value="DELIVERING"]').text()).toBe('派送中')
expect(select.find('option[value="DELIVERED"]').text()).toBe('派送完成')
expect(select.find('option[value="DELIVERED"]').exists()).toBe(false)
expect(select.find('option[value="WAIT_DELIVERY"]').exists()).toBe(false)
await select.setValue('DELIVERING')
await wrapper.find('[data-dialog="编辑派送状态"] textarea').setValue('123')
@@ -114,7 +114,7 @@ describe('DeliveryOrdersView permissions and commands', () => {
expect(updateDeliveryOrderStatus).toHaveBeenCalledTimes(1)
vi.mocked(getDeliveryOrder).mockResolvedValue({ order: { ...row, version: 8, allowedActions: ['COMPLETE_DELIVERY'] }, skus: [], fees: [], operations: [] })
await state.refreshStatus(); await flushPromises()
expect(select.findAll('option').map(option => option.attributes('value'))).toEqual(['DELIVERED'])
expect(select.findAll('option')).toHaveLength(0)
wrapper.unmount()
})
@@ -192,37 +192,27 @@ describe('DeliveryOrdersView permissions and commands', () => {
wrapper.unmount()
})
it('selects completion through the real dialog and select, then retries a conflict with the refreshed version', async () => {
it('hides status editing when completion is the only server action', async () => {
canPermission.mockImplementation(code => code.endsWith(':status'))
vi.mocked(getDeliveryOrder).mockResolvedValue({ order: { ...row, version: 7, status: 'DELIVERING', allowedActions: ['COMPLETE_DELIVERY'] }, skus: [], fees: [], operations: [] })
vi.mocked(updateDeliveryOrderStatus).mockRejectedValueOnce({ isAxiosError: true, response: { status: 409, data: { code: 'A0409', message: '版本冲突' } } })
const wrapper = mount(DeliveryOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus], stubs: { ...commonPhase5Stubs, AppDialog: false, ElInput: false, ElSelect: false, ElOption: false } } })
const body = new DOMWrapper(document.body)
try {
await flushPromises()
await wrapper.findAll('button').find(button => button.text().includes('编辑状态'))!.trigger('click')
await flushPromises()
const dialog = body.find('[role="dialog"][aria-label="编辑派送状态"]')
const save = () => dialog.findAll('button').find(button => button.text() === '保存状态')!
expect(save().attributes('disabled')).toBeDefined()
await dialog.find('.el-select__wrapper').trigger('click')
await flushPromises()
const options = body.findAll('.el-select-dropdown__item')
expect(options.map(option => option.text())).toEqual(['派送完成'])
await options[0]!.trigger('click')
await dialog.find('textarea').setValue('已交付')
await save().trigger('click'); await flushPromises()
expect(updateDeliveryOrderStatus).toHaveBeenLastCalledWith('DEL/1', '100', { status: 'DELIVERED', expectedVersion: 7, remark: '已交付' })
expect(save().attributes('disabled')).toBeDefined()
expect(dialog.text()).toContain('请刷新最新状态并重新确认')
vi.mocked(getDeliveryOrder).mockResolvedValue({ order: { ...row, version: 8, status: 'DELIVERING', allowedActions: ['COMPLETE_DELIVERY'] }, skus: [], fees: [], operations: [] })
await dialog.findAll('button').find(button => button.text().includes('重新加载'))!.trigger('click')
await flushPromises()
expect((dialog.find('textarea').element as HTMLTextAreaElement).value).toBe('已交付')
await save().trigger('click'); await flushPromises()
expect(updateDeliveryOrderStatus).toHaveBeenLastCalledWith('DEL/1', '100', { status: 'DELIVERED', expectedVersion: 8, remark: '已交付' })
expect(listDeliveryOrders).toHaveBeenCalledTimes(2)
} finally { wrapper.unmount() }
vi.mocked(listDeliveryOrders).mockResolvedValue({ records: [{ ...row, status: 'DELIVERING', allowedActions: ['COMPLETE_DELIVERY'] }], page: 1, size: 10, total: 1, pages: 1 })
const wrapper = mountView(); await flushPromises()
expect(wrapper.find('[data-testid="data-table"]').text()).not.toContain('编辑状态')
expect(getDeliveryOrder).not.toHaveBeenCalled()
expect(updateDeliveryOrderStatus).not.toHaveBeenCalled()
wrapper.unmount()
})
it('shows the destination FBA warehouse that identifies each split delivery', async () => {
vi.mocked(listDeliveryOrders).mockResolvedValue({ records: [{ ...row, destinationWarehouseCode: 'ACY2', destinationWarehouseName: 'Amazon ACY2' }], page: 1, size: 10, total: 1, pages: 1 })
const table = defineComponent({
props: { data: Array as PropType<object[]>, columns: Array as PropType<Array<{ key: string; formatter?: (row: object) => string }>> },
setup(props) { return () => h('div', props.columns?.find(column => column.key === 'destinationWarehouse')?.formatter?.(props.data?.[0] || {}) || '') },
})
const wrapper = mount(DeliveryOrdersView, { global: { plugins: [ElementPlus], stubs: { ...commonPhase5Stubs, DataTable: table } } })
await flushPromises()
expect(wrapper.text()).toContain('ACY2 · Amazon ACY2')
wrapper.unmount()
})
it('renders category counts from the list without per-row document requests', async () => {
File diff suppressed because one or more lines are too long
@@ -15,7 +15,10 @@ describe('OutboundDetailView', () => {
vi.mocked(getOutbound).mockReset().mockResolvedValue({
outbound: { id: '1', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'PICKED', palletCount: 2, packageCount: 20, createdAt: '2026-09-01T10:00:00' },
pallets: [{ palletNo: 'IP-ACY2-20260907-597057-P001', inboundCode: 'NYEXX2609060063', destinationWarehouseCode: 'ONT8', destinationWarehouseName: 'Amazon ONT8', skuCode: 'SKU-001', packageCount: 20 }],
delivery: { version: 2, allowedActions: ['START_DELIVERY'], id: '2', customerId: 'customer-1', deliveryNo: 'DEL-1', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', palletCount: 2, packageCount: 20, createdAt: '2026-09-01T10:00:00' },
deliveries: [
{ version: 2, allowedActions: ['START_DELIVERY'], id: '2', customerId: 'customer-1', deliveryNo: 'DEL-ONT8', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', destinationWarehouseCode: 'ONT8', palletCount: 1, packageCount: 10, createdAt: '2026-09-01T10:00:00' },
{ version: 2, allowedActions: ['START_DELIVERY'], id: '3', customerId: 'customer-1', deliveryNo: 'DEL-ACY2', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', destinationWarehouseCode: 'ACY2', palletCount: 1, packageCount: 10, createdAt: '2026-09-01T10:00:00' },
],
})
})
@@ -25,7 +28,9 @@ describe('OutboundDetailView', () => {
expect(getOutbound).toHaveBeenCalledWith('OUT/1')
expect(wrapper.text()).toContain('OUT/1')
expect(wrapper.text()).toContain('PC-1')
expect(wrapper.text()).toContain('DEL-1')
expect(wrapper.text()).toContain('DEL-ONT8')
expect(wrapper.text()).toContain('DEL-ACY2')
expect(wrapper.text()).toContain('ACY2')
expect(wrapper.text()).toContain('出库托盘')
expect(wrapper.text()).toContain('IP-ACY2-20260907-597057-P001')
expect(wrapper.text()).toContain('NYEXX2609060063')
@@ -40,7 +45,7 @@ describe('OutboundDetailView', () => {
vi.mocked(getOutbound).mockResolvedValueOnce({
outbound: { id: '1', outboundNo: 'OUT/1', status: 'OUTBOUNDED' },
pallets: [{ palletNo: null, inboundCode: null, destinationWarehouseCode: null, destinationWarehouseName: null, skuCode: null, packageCount: null }],
delivery: null,
deliveries: [],
})
const wrapper = mount(OutboundDetailView, { global: { plugins: [ElementPlus] } })
await flushPromises()
@@ -51,7 +56,7 @@ describe('OutboundDetailView', () => {
vi.mocked(getOutbound).mockResolvedValueOnce({
outbound: { id: '1', outboundNo: 'OUT/1', status: 'OUTBOUNDED' },
pallets: [],
delivery: null,
deliveries: [],
})
const emptyWrapper = mount(OutboundDetailView, { global: { plugins: [ElementPlus] } })
await flushPromises()
@@ -74,7 +79,7 @@ describe('OutboundDetailView', () => {
])('uses a matching warehouse name and never displays internal IDs (%s, %s)', async (deliveryWarehouseId, name, expected) => {
vi.mocked(getOutbound).mockResolvedValueOnce({
outbound: { id: '1', outboundNo: 'OUT/1', overseasWarehouseId: 'warehouse-internal-id' },
delivery: { id: '2', customerId: 'customer-1', deliveryNo: 'DEL-1', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', palletCount: 2, packageCount: 20, createdAt: '2026-09-01T10:00:00', version: 1, allowedActions: [], overseasWarehouseId: deliveryWarehouseId, overseasWarehouseName: name },
deliveries: [{ id: '2', customerId: 'customer-1', deliveryNo: 'DEL-1', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', palletCount: 2, packageCount: 20, createdAt: '2026-09-01T10:00:00', version: 1, allowedActions: [], overseasWarehouseId: deliveryWarehouseId, overseasWarehouseName: name }],
})
const wrapper = mount(OutboundDetailView, { global: { plugins: [ElementPlus] } })
await flushPromises()
@@ -37,7 +37,7 @@ function show(value: unknown) { const text = String(value ?? '').trim(); return
// A warehouse ID is only a join key; never render it as a warehouse name.
const warehouseLabel = computed(() => {
const outboundWarehouseId = detail.value?.outbound.overseasWarehouseId
const delivery = detail.value?.delivery
const delivery = detail.value?.deliveries?.find(item => item.overseasWarehouseId === outboundWarehouseId)
return outboundWarehouseId && delivery?.overseasWarehouseId === outboundWarehouseId ? show(delivery.overseasWarehouseName) : '—'
})
function statusLabel(value: unknown) { const code = String(value || ''); return statusLabels[code] || show(code) }
@@ -54,11 +54,11 @@ watch(outboundNo, () => { void load() }, { immediate: true })
<template #header><div class="detail-heading"><el-button class="density-back" link aria-label="返回出库单列表" @click="back"><ArrowLeft :size="16" />返回</el-button><div class="detail-title"><div><h1>出库单详情</h1><p>出库单号 {{ outboundNo }}</p></div><StatusBadge v-if="detail" :status="statusLabel(detail.outbound.status)" /></div><el-button v-if="canCancel" type="danger" plain :loading="cancelling" @click="cancel"><XCircle :size="16" />取消出库单</el-button></div></template>
<OperationErrorToast :details="mutationError" />
<LoadErrorState v-if="loadError" title="出库单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<template v-else-if="detail"><section class="surface-panel detail-section"><h2>出库信息</h2><el-descriptions class="density-descriptions" :column="4" border><el-descriptions-item label="出库单号">{{ show(detail.outbound.outboundNo) }}</el-descriptions-item><el-descriptions-item label="出库类型">{{ outboundTypeLabel(detail.outbound.outboundType) }}</el-descriptions-item><el-descriptions-item label="关联排车单">{{ show(detail.outbound.dispatchNo) }}</el-descriptions-item><el-descriptions-item label="状态">{{ statusLabel(detail.outbound.status) }}</el-descriptions-item><el-descriptions-item label="海外仓">{{ warehouseLabel }}</el-descriptions-item><el-descriptions-item label="托盘数">{{ detail.outbound.palletCount ?? '—' }}</el-descriptions-item><el-descriptions-item label="箱数">{{ detail.outbound.packageCount ?? '—' }}</el-descriptions-item><el-descriptions-item label="创建时间">{{ formatOverseasDateTime(detail.outbound.createdAt) }}</el-descriptions-item></el-descriptions></section><section class="surface-panel detail-section"><h2>出库托盘</h2><DataTable class="outbound-pallet-table" aria-label="出库托盘" :data="detail.pallets || []" :columns="palletColumns" stripe column-settings-mode="hide" empty-text="暂无出库托盘" /></section><section class="surface-panel detail-section"><h2>关联派送</h2><el-empty v-if="!detail.delivery" description="暂无关联派送单" :image-size="56" /><el-descriptions class="density-descriptions" v-else :column="4" border><el-descriptions-item label="派送单号">{{ show(detail.delivery.deliveryNo) }}</el-descriptions-item><el-descriptions-item label="派送状态">{{ show(detail.delivery.status) }}</el-descriptions-item><el-descriptions-item label="运单号">{{ show(detail.delivery.waybillNo) }}</el-descriptions-item><el-descriptions-item label="承运商">{{ show(detail.delivery.carrierName) }}</el-descriptions-item><el-descriptions-item label="司机">{{ show(detail.delivery.driverName) }}</el-descriptions-item><el-descriptions-item label="车牌">{{ show(detail.delivery.vehiclePlate) }}</el-descriptions-item><el-descriptions-item label="计划派送">{{ formatOverseasDateTime(detail.delivery.plannedDeliveryAt) }}</el-descriptions-item><el-descriptions-item label="备注">{{ show(detail.delivery.remark) }}</el-descriptions-item></el-descriptions></section></template>
<template v-else-if="detail"><section class="surface-panel detail-section"><h2>出库信息</h2><el-descriptions class="density-descriptions" :column="4" border><el-descriptions-item label="出库单号">{{ show(detail.outbound.outboundNo) }}</el-descriptions-item><el-descriptions-item label="出库类型">{{ outboundTypeLabel(detail.outbound.outboundType) }}</el-descriptions-item><el-descriptions-item label="关联排车单">{{ show(detail.outbound.dispatchNo) }}</el-descriptions-item><el-descriptions-item label="状态">{{ statusLabel(detail.outbound.status) }}</el-descriptions-item><el-descriptions-item label="海外仓">{{ warehouseLabel }}</el-descriptions-item><el-descriptions-item label="托盘数">{{ detail.outbound.palletCount ?? '—' }}</el-descriptions-item><el-descriptions-item label="箱数">{{ detail.outbound.packageCount ?? '—' }}</el-descriptions-item><el-descriptions-item label="创建时间">{{ formatOverseasDateTime(detail.outbound.createdAt) }}</el-descriptions-item></el-descriptions></section><section class="surface-panel detail-section"><h2>出库托盘</h2><DataTable class="outbound-pallet-table" aria-label="出库托盘" :data="detail.pallets || []" :columns="palletColumns" stripe column-settings-mode="hide" empty-text="暂无出库托盘" /></section><section class="surface-panel detail-section"><h2>关联派送</h2><el-empty v-if="!detail.deliveries?.length" description="暂无关联派送单" :image-size="56" /><div v-else class="delivery-list"><el-descriptions v-for="delivery in detail.deliveries" :key="delivery.deliveryNo" class="density-descriptions" :column="4" border><el-descriptions-item label="目的 FBA 仓">{{ show(delivery.destinationWarehouseCode || delivery.destinationWarehouseName) }}</el-descriptions-item><el-descriptions-item label="派送单号">{{ show(delivery.deliveryNo) }}</el-descriptions-item><el-descriptions-item label="派送状态">{{ statusLabel(delivery.status) }}</el-descriptions-item><el-descriptions-item label="运单号">{{ show(delivery.waybillNo) }}</el-descriptions-item><el-descriptions-item label="承运商">{{ show(delivery.carrierName) }}</el-descriptions-item><el-descriptions-item label="司机">{{ show(delivery.driverName) }}</el-descriptions-item><el-descriptions-item label="车牌">{{ show(delivery.vehiclePlate) }}</el-descriptions-item><el-descriptions-item label="计划派送">{{ formatOverseasDateTime(delivery.plannedDeliveryAt) }}</el-descriptions-item><el-descriptions-item label="托盘数">{{ delivery.palletCount }}</el-descriptions-item><el-descriptions-item label="箱数">{{ delivery.packageCount }}</el-descriptions-item><el-descriptions-item label="备注" :span="2">{{ show(delivery.remark) }}</el-descriptions-item></el-descriptions></div></section></template>
</DetailPageLayout></AdminPage>
</template>
<style scoped>.detail-heading{display:flex;min-width:0;align-items:center;gap:var(--admin-space-5)}.detail-section h2{margin:0 0 var(--admin-space-5);font-size:var(--admin-font-section)}.detail-section+.detail-section{margin-top:var(--admin-space-5)}@media(max-width:1200px){.detail-section :deep(.el-descriptions__body .el-descriptions__table){table-layout:auto}}@media(max-width:720px){.detail-heading,.detail-title{align-items:flex-start;flex-direction:column}}
<style scoped>.detail-heading{display:flex;min-width:0;align-items:center;gap:var(--admin-space-5)}.detail-section h2{margin:0 0 var(--admin-space-5);font-size:var(--admin-font-section)}.detail-section+.detail-section{margin-top:var(--admin-space-5)}.delivery-list{display:grid;gap:var(--admin-space-4)}@media(max-width:1200px){.detail-section :deep(.el-descriptions__body .el-descriptions__table){table-layout:auto}}@media(max-width:720px){.detail-heading,.detail-title{align-items:flex-start;flex-direction:column}}
:deep(.detail-page-layout) { gap: 0; background: var(--admin-surface); border: 1px solid var(--cc-border); }
:deep(.detail-page-layout__header) { padding: var(--admin-space-5); border-bottom: 1px solid var(--cc-border); }
.detail-title {display:flex;min-width:0;flex:1;align-items:center; justify-content: flex-start;gap:var(--admin-space-5); }