优化
This commit is contained in:
@@ -17,7 +17,7 @@ describe('SmartLoadingDialog', () => {
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findAll('.smart-loading-dialog__warehouses [role="tab"]').map((tab) => tab.text())).toEqual(['义乌仓', '东莞仓'])
|
||||
expect(wrapper.find('.smart-loading-dialog__warehouses').exists()).toBe(false)
|
||||
const expand = wrapper.get('button[aria-label="展开全屏"]')
|
||||
await expand.trigger('click')
|
||||
expect(document.querySelector('.smart-loading-dialog.is-fullscreen')).not.toBeNull()
|
||||
|
||||
@@ -3,23 +3,15 @@ import { Maximize2, Minimize2 } from '@lucide/vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import GlobalSmartLoadingView from '../pages/GlobalSmartLoadingView.vue'
|
||||
import { SMART_LOADING_WAREHOUSE_GROUPS } from '../smartLoadingChannelGroups'
|
||||
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const fullscreen = ref(false)
|
||||
const warehouseCode = ref('')
|
||||
const workbench = ref<InstanceType<typeof GlobalSmartLoadingView> | null>(null)
|
||||
|
||||
watch(open, (value) => {
|
||||
if (!value) {
|
||||
fullscreen.value = false
|
||||
warehouseCode.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
function selectWarehouse(code: string) {
|
||||
void workbench.value?.selectWarehouse(code)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,9 +29,6 @@ function selectWarehouse(code: string) {
|
||||
<div class="smart-loading-dialog__header">
|
||||
<div class="smart-loading-dialog__heading">
|
||||
<span>智能装柜</span>
|
||||
<el-tabs :model-value="warehouseCode" class="smart-loading-dialog__warehouses" aria-label="装柜仓库" @tab-change="selectWarehouse(String($event))">
|
||||
<el-tab-pane v-for="warehouse in SMART_LOADING_WAREHOUSE_GROUPS" :key="warehouse.code" :label="warehouse.name" :name="warehouse.code" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-tooltip :content="fullscreen ? '退出全屏' : '展开全屏'" placement="bottom">
|
||||
<el-button text :aria-label="fullscreen ? '退出全屏' : '展开全屏'" @click="fullscreen = !fullscreen">
|
||||
@@ -49,25 +38,21 @@ function selectWarehouse(code: string) {
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<GlobalSmartLoadingView ref="workbench" v-model:warehouse-code="warehouseCode" embedded />
|
||||
<GlobalSmartLoadingView embedded />
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(.smart-loading-dialog.app-dialog.el-dialog) { width: min(1600px, 98vw); max-height: 94vh; margin-block: 3vh !important; }
|
||||
:global(.smart-loading-dialog.app-dialog.el-dialog.is-fullscreen) { width: 100%; height: 100vh; max-height: 100vh; margin: 0 !important; border-radius: 0; }
|
||||
:global(.smart-loading-dialog .el-dialog__body) { padding: 0 16px 16px; }
|
||||
:global(.smart-loading-dialog .el-dialog__body) { padding: 0 }
|
||||
:global(.smart-loading-dialog .app-dialog__body) { min-width: 0; }
|
||||
.smart-loading-dialog__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; color: var(--cc-text); font-size: 18px; font-weight: 600; line-height: 24px; }
|
||||
.smart-loading-dialog__heading { display: flex; min-width: 0; align-items: center; gap: 24px; }
|
||||
.smart-loading-dialog__heading > span { flex: 0 0 auto; }
|
||||
.smart-loading-dialog__warehouses { min-width: 0; }
|
||||
.smart-loading-dialog__warehouses :deep(.el-tabs__header) { margin: 0; }
|
||||
.smart-loading-dialog__warehouses :deep(.el-tabs__nav-wrap::after), .smart-loading-dialog__warehouses :deep(.el-tabs__content) { display: none; }
|
||||
.smart-loading-dialog__warehouses :deep(.el-tabs__item) { height: 38px; padding-inline: 16px; font-size: 14px; font-weight: 600; }
|
||||
.smart-loading-dialog__header :deep(.el-button) { flex: 0 0 auto; margin-right: 24px; color: var(--cc-muted); }
|
||||
@media (max-width: 720px) {
|
||||
:global(.smart-loading-dialog.app-dialog.el-dialog) { width: calc(100vw - 12px); max-height: calc(100svh - 12px); margin-block: 6px !important; }
|
||||
:global(.smart-loading-dialog .el-dialog__body) { padding: 0 8px 8px; }
|
||||
:global(.smart-loading-dialog .el-dialog__body) { padding: 0 }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ElementPlus, { ElInputNumber, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import ElementPlus, { ElCheckboxButton, ElInputNumber, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { AxiosError } from 'axios'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
@@ -48,7 +48,7 @@ vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups:
|
||||
const container = {
|
||||
id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
|
||||
warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'EDITING' as const, version: 1,
|
||||
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null,
|
||||
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: '31', shippingCompanyName: '美森轮船',
|
||||
departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null,
|
||||
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', capacityCbm: 76, maxWeightKg: 28000,
|
||||
terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null,
|
||||
@@ -88,7 +88,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.id, container.warehouseId)
|
||||
expect(wrapper.text()).toContain('候选装柜明细')
|
||||
expect(wrapper.text()).toContain('美森正班')
|
||||
expect(wrapper.findAll('.channel-tabs [role="tab"]')).toHaveLength(9)
|
||||
expect(wrapper.findAll('.channel-tags .el-checkbox-button')).toHaveLength(9)
|
||||
expect(wrapper.text()).not.toContain('美森正班海卡包税')
|
||||
expect(wrapper.text()).toContain('另一企业')
|
||||
expect(wrapper.text()).toContain('MSCU-1 装柜方案')
|
||||
@@ -105,7 +105,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
|
||||
it('uses the top-level container channel snapshot without expecting a nested snapshot', async () => {
|
||||
vi.mocked(listContainers).mockResolvedValue({
|
||||
records: [{ ...container, channelCode: 'EXX_RAIL_NY', channelName: '纽约EXX海铁' }],
|
||||
records: [{ ...container, shippingCompanyId: '32', shippingCompanyName: 'EXX', channelCode: 'EXX_RAIL_NY', channelName: '纽约EXX海铁' }],
|
||||
page: 1,
|
||||
size: 100,
|
||||
total: 1,
|
||||
@@ -167,7 +167,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).not.toContain('真实柜号')
|
||||
})
|
||||
|
||||
it('groups containers by channel and reloads candidates when the channel changes', async () => {
|
||||
it('keeps nine fixed channel tags and allows selecting multiple channels', async () => {
|
||||
const other = {
|
||||
...container,
|
||||
id: '9007199254740998',
|
||||
@@ -185,17 +185,19 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.findAll('.container-option')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('MSCU-1')
|
||||
expect(wrapper.text()).not.toContain('MSCU-2')
|
||||
expect(wrapper.text()).not.toContain('个柜次')
|
||||
expect(wrapper.text()).toContain('柜子数量1 个')
|
||||
|
||||
const tab = wrapper.findAll('[role="tab"]').find((item) => item.text() === '合德')
|
||||
expect(tab).toBeDefined()
|
||||
await tab!.trigger('click')
|
||||
expect(wrapper.findAll('.channel-tags .el-checkbox-button').map((item) => item.text())).toEqual([
|
||||
'美森正班', '美森加班', 'EXX', '合德', '洛杉矶普船', '芝加哥普船', '休斯顿普船', '萨凡纳普船', '纽约普船海卡',
|
||||
])
|
||||
const hedeChannel = wrapper.findAllComponents(ElCheckboxButton).find((item) => item.text() === '合德')!
|
||||
await hedeChannel.find('input').setValue(true)
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findAll('.container-option')).toHaveLength(1)
|
||||
expect(wrapper.findAll('.container-option')).toHaveLength(2)
|
||||
expect(wrapper.text()).toContain('MSCU-1')
|
||||
expect(wrapper.text()).toContain('MSCU-2')
|
||||
expect(wrapper.text()).not.toContain('MSCU-1')
|
||||
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(other.id, other.warehouseId)
|
||||
expect(wrapper.text()).toContain('柜子数量2 个')
|
||||
})
|
||||
|
||||
it('merges detailed pricing channels into the same confirmed display group', async () => {
|
||||
@@ -219,7 +221,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).not.toContain('美森正班海派')
|
||||
})
|
||||
|
||||
it('isolates Yiwu and Dongguan containers when the host changes the warehouse', async () => {
|
||||
it('moves the shipping warehouse into the query filters', async () => {
|
||||
const dongguanContainer = {
|
||||
...container,
|
||||
id: '9007199254740998',
|
||||
@@ -235,9 +237,9 @@ describe('GlobalSmartLoadingView', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('MSCU-1')
|
||||
expect(wrapper.text()).not.toContain('MSCU-DG')
|
||||
expect(wrapper.text()).toContain('MSCU-DG')
|
||||
|
||||
await (wrapper.vm as unknown as { selectWarehouse: (code: string) => Promise<void> }).selectWarehouse('DONGGUAN')
|
||||
wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('MSCU-DG')
|
||||
@@ -261,10 +263,10 @@ describe('GlobalSmartLoadingView', () => {
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
await (wrapper.vm as unknown as { selectWarehouse: (code: string) => Promise<void> }).selectWarehouse('DONGGUAN')
|
||||
wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('.channel-tabs [role="tab"][aria-selected="true"]').text()).toBe('美森正班')
|
||||
expect(wrapper.find('.channel-tags .el-checkbox-button.is-checked').text()).toBe('美森正班')
|
||||
expect(wrapper.findAll('.container-option')).toHaveLength(0)
|
||||
expect(wrapper.text()).not.toContain('MSCU-DG-MAX')
|
||||
expect(listSmartLoadingCandidates).toHaveBeenCalledTimes(1)
|
||||
@@ -391,7 +393,8 @@ describe('GlobalSmartLoadingView', () => {
|
||||
await flushPromises()
|
||||
|
||||
const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }> }>
|
||||
expect(fields.map((field) => field.key)).toEqual(['customerId', 'inboundCode', 'expectedArrivalRange', 'orderStatus'])
|
||||
expect(fields.map((field) => field.key)).toEqual(['warehouseId', 'customerId', 'inboundCode', 'expectedArrivalRange', 'orderStatus'])
|
||||
expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([{ label: '义乌仓', value: '8' }])
|
||||
expect(source).not.toContain('目的仓')
|
||||
|
||||
expect(wrapper.text()).not.toContain('税率')
|
||||
@@ -659,20 +662,23 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(source).toContain(':page-sizes="[20, 50, 100]"')
|
||||
})
|
||||
|
||||
it('uses overflow-aware channel tabs with a compact right-aligned summary', () => {
|
||||
it('uses nine fixed multi-select channel tags with a compact right-aligned summary', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
|
||||
|
||||
expect(source).toContain('<el-tabs :model-value="activeChannelCode"')
|
||||
expect(source).toContain('<el-tab-pane v-for="channel in channelTabs"')
|
||||
expect(source).toContain(':label="channel.name"')
|
||||
expect(source).not.toContain('channel.containerCount')
|
||||
expect(source).toContain('class="compact-summary" aria-label="当前渠道装柜统计"')
|
||||
expect(source).toContain('<el-checkbox-group :model-value="selectedChannelCodes"')
|
||||
expect(source).toContain('<el-checkbox-button v-for="channel in channelOptions"')
|
||||
expect(source).toContain('{{ channel.name }}')
|
||||
expect(source).toContain("ElMessage.warning('至少选择一个渠道')")
|
||||
expect(source).toContain('class="compact-summary" aria-label="已选渠道装柜统计"')
|
||||
expect(source).toContain('<dt>候选明细</dt>')
|
||||
expect(source).toContain('<dt>柜次</dt>')
|
||||
expect(source).toContain('<dt>柜子数量</dt>')
|
||||
expect(source).toContain('<dt>已装</dt>')
|
||||
expect(source).toContain('<dt>总重量</dt>')
|
||||
expect(source).toContain('<dt>总体积</dt>')
|
||||
expect(source).toContain('.channel-tabs :deep(.el-tabs__content) { display: none; }')
|
||||
expect(source).toContain('.channel-tags :deep(.el-checkbox-button__inner)')
|
||||
expect(source).toContain('flex-wrap: nowrap; gap: 6px;')
|
||||
expect(source).toContain('height: 34px; padding: 0 10px;')
|
||||
expect(source).toContain('font-size: 13px; line-height: 32px;')
|
||||
expect(source).not.toContain('class="summary-band"')
|
||||
expect(source).toContain('class="capacity-grid"')
|
||||
expect(source).toContain('智能推荐')
|
||||
|
||||
@@ -32,13 +32,10 @@ import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLo
|
||||
import { loadSmartLoadingPlanCoLoadGroups, type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups'
|
||||
import {
|
||||
SMART_LOADING_CHANNEL_GROUPS,
|
||||
SMART_LOADING_WAREHOUSE_GROUPS,
|
||||
resolveSmartLoadingChannelGroup,
|
||||
resolveSmartLoadingWarehouseGroup,
|
||||
} from '../smartLoadingChannelGroups'
|
||||
|
||||
const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
|
||||
const activeWarehouseCode = defineModel<string>('warehouseCode', { default: '' })
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const router = useRouter()
|
||||
@@ -46,7 +43,7 @@ const containers = ref<ContainerBookingResource[]>([])
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
const customersLoading = ref(false)
|
||||
const selectedContainerId = ref('')
|
||||
const activeChannelCode = ref('')
|
||||
const selectedChannelCodes = ref<string[]>([])
|
||||
const plan = ref<SmartLoadingPlan | null>(null)
|
||||
const candidates = ref<SmartLoadingCandidate[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -61,7 +58,7 @@ const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
|
||||
const assignForm = reactive({ packageCount: 1 })
|
||||
const quantityDrafts = reactive<Record<string, number>>({})
|
||||
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
|
||||
const query = ref<QueryModel>({ customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' })
|
||||
const query = ref<QueryModel>({ warehouseId: '', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' })
|
||||
const appliedQuery = ref<QueryModel>({ ...query.value })
|
||||
const candidatePage = ref(1)
|
||||
const candidatePageSize = ref(20)
|
||||
@@ -77,16 +74,16 @@ const unmappedChannelContainerCount = computed(() => orchestratableContainers.va
|
||||
item.channelCode && !resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
|
||||
)).length)
|
||||
const mappedWorkbenchContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
|
||||
resolveSmartLoadingWarehouseGroup(item.warehouseName)
|
||||
&& resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
|
||||
resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
|
||||
)).length)
|
||||
const unmappedWarehouseContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
|
||||
!resolveSmartLoadingWarehouseGroup(item.warehouseName)
|
||||
)).length)
|
||||
const warehouseTabs = computed(() => SMART_LOADING_WAREHOUSE_GROUPS)
|
||||
const channelTabs = computed(() => SMART_LOADING_CHANNEL_GROUPS)
|
||||
const activeWarehouse = computed(() => warehouseTabs.value.find((item) => item.code === activeWarehouseCode.value) || null)
|
||||
const activeChannel = computed(() => channelTabs.value.find((item) => item.code === activeChannelCode.value) || null)
|
||||
const channelOptions = computed(() => SMART_LOADING_CHANNEL_GROUPS)
|
||||
const warehouseOptions = computed(() => {
|
||||
const options = new Map<string, string>()
|
||||
for (const item of orchestratableContainers.value) {
|
||||
if (item.warehouseId && !options.has(item.warehouseId)) options.set(item.warehouseId, item.warehouseName?.trim() || '未命名发货仓')
|
||||
}
|
||||
return [...options].map(([value, label]) => ({ value, label }))
|
||||
})
|
||||
const editable = computed(() => plan.value?.canEdit === true)
|
||||
const assignable = computed(() => plan.value?.canAssign === true)
|
||||
const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
|
||||
@@ -195,6 +192,7 @@ const candidateOrderStatusOptions = computed(() => [...new Set(candidates.value
|
||||
.filter(Boolean))]
|
||||
.map((status) => ({ value: status, label: candidateOrderStatusLabel(status) })))
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'warehouseId', type: 'select', label: '发货仓', placeholder: '全部发货仓', width: 180, options: warehouseOptions.value, props: { filterable: true } },
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||
{ key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 240 },
|
||||
{ key: 'expectedArrivalRange', type: 'daterange', label: '预计到货时间', placeholder: '请选择预计到货时间范围', width: 300 },
|
||||
@@ -203,9 +201,8 @@ const queryFields = computed<QueryField[]>(() => [
|
||||
|
||||
const filteredContainers = computed(() => containers.value.filter((item) => (
|
||||
['WAIT_LOADING', 'EDITING', 'LOADING'].includes(item.planStatus)
|
||||
&& resolveSmartLoadingWarehouseGroup(item.warehouseName)?.code === activeWarehouseCode.value
|
||||
&& Boolean(activeChannelCode.value)
|
||||
&& resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === activeChannelCode.value
|
||||
&& (!String(appliedQuery.value.warehouseId || '') || item.warehouseId === String(appliedQuery.value.warehouseId))
|
||||
&& selectedChannelCodes.value.includes(resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code || '')
|
||||
)))
|
||||
const filteredCandidates = computed(() => {
|
||||
const customerId = String(appliedQuery.value.customerId || '')
|
||||
@@ -298,12 +295,6 @@ function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> =
|
||||
}
|
||||
}
|
||||
|
||||
function preferredChannelCode(warehouseCode: string) {
|
||||
return channelTabs.value.find((group) => orchestratableContainers.value.some((item) => (
|
||||
resolveSmartLoadingWarehouseGroup(item.warehouseName)?.code === warehouseCode
|
||||
&& resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === group.code
|
||||
)))?.code || channelTabs.value[0]?.code || ''
|
||||
}
|
||||
function clearSelectionData() {
|
||||
plan.value = null
|
||||
candidates.value = []
|
||||
@@ -363,13 +354,12 @@ async function loadAll(preserved: Record<string, number> = {}) {
|
||||
? await Promise.all(Array.from({ length: firstPage.pages - 1 }, (_, index) => listContainers({ page: index + 2, size: 100 })))
|
||||
: []
|
||||
containers.value = [firstPage, ...remainingPages].flatMap((page) => page.records)
|
||||
if (!warehouseTabs.value.some((item) => item.code === activeWarehouseCode.value)) {
|
||||
activeWarehouseCode.value = warehouseTabs.value.find((group) => orchestratableContainers.value.some((item) => (
|
||||
resolveSmartLoadingWarehouseGroup(item.warehouseName)?.code === group.code
|
||||
)))?.code || warehouseTabs.value[0]?.code || ''
|
||||
}
|
||||
if (!channelTabs.value.some((item) => item.code === activeChannelCode.value)) {
|
||||
activeChannelCode.value = preferredChannelCode(activeWarehouseCode.value)
|
||||
const availableChannelCodes = new Set(channelOptions.value.map((item) => item.code))
|
||||
selectedChannelCodes.value = selectedChannelCodes.value.filter((code) => availableChannelCodes.has(code))
|
||||
if (!selectedChannelCodes.value.length) {
|
||||
selectedChannelCodes.value = [channelOptions.value.find((channel) => orchestratableContainers.value.some((item) => (
|
||||
resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === channel.code
|
||||
)))?.code || channelOptions.value[0]?.code || '']
|
||||
}
|
||||
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) selectedContainerId.value = preferredContainerId()
|
||||
await loadSelectedData(preserved)
|
||||
@@ -396,16 +386,12 @@ async function selectContainer(containerId: string) {
|
||||
selectedContainerId.value = containerId
|
||||
await loadSelectedData()
|
||||
}
|
||||
async function selectWarehouse(warehouseCode: string) {
|
||||
if (activeWarehouseCode.value === warehouseCode) return
|
||||
activeWarehouseCode.value = warehouseCode
|
||||
selectedContainerId.value = preferredContainerId()
|
||||
await loadSelectedData()
|
||||
}
|
||||
defineExpose({ selectWarehouse })
|
||||
async function selectChannel(channelCode: string) {
|
||||
if (activeChannelCode.value === channelCode) return
|
||||
activeChannelCode.value = channelCode
|
||||
async function selectChannels(codes: string[]) {
|
||||
if (!codes.length) {
|
||||
ElMessage.warning('至少选择一个渠道')
|
||||
return
|
||||
}
|
||||
selectedChannelCodes.value = codes
|
||||
selectedContainerId.value = preferredContainerId()
|
||||
await loadSelectedData()
|
||||
}
|
||||
@@ -575,13 +561,14 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<template>
|
||||
<component :is="props.embedded ? 'div' : AdminPage" :class="{ 'embedded-smart-loading': props.embedded }">
|
||||
<section class="global-loading-page">
|
||||
<div v-if="channelTabs.length" class="channel-summary-row">
|
||||
<el-tabs :model-value="activeChannelCode" class="channel-tabs" aria-label="物流渠道" @tab-change="selectChannel(String($event))">
|
||||
<el-tab-pane v-for="channel in channelTabs" :key="channel.code" :label="channel.name" :name="channel.code" />
|
||||
</el-tabs>
|
||||
<dl class="compact-summary" aria-label="当前渠道装柜统计">
|
||||
<div v-if="channelOptions.length" class="channel-summary-row">
|
||||
<el-checkbox-group :model-value="selectedChannelCodes" class="channel-tags" aria-label="渠道筛选,至少选择一项" @change="selectChannels($event as string[])">
|
||||
<span class="channel-tags__label">渠道</span>
|
||||
<el-checkbox-button v-for="channel in channelOptions" :key="channel.code" :label="channel.code" :value="channel.code">{{ channel.name }}</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
<dl class="compact-summary" aria-label="已选渠道装柜统计">
|
||||
<div><dt>候选明细</dt><dd><strong>{{ filteredCandidates.length }}</strong> 条</dd></div>
|
||||
<div><dt>柜次</dt><dd><strong>{{ filteredContainers.length }}</strong> 个</dd></div>
|
||||
<div><dt>柜子数量</dt><dd><strong>{{ filteredContainers.length }}</strong> 个</dd></div>
|
||||
<div><dt>已装</dt><dd><strong>{{ totalPackages }}</strong> 箱</dd></div>
|
||||
<div><dt>总重量</dt><dd><strong>{{ formatNumber(totalWeight, 1) }}</strong> kg</dd></div>
|
||||
<div><dt>总体积</dt><dd><strong>{{ formatNumber(totalVolume) }}</strong> m³</dd></div>
|
||||
@@ -589,7 +576,6 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
</div>
|
||||
<el-alert v-if="missingChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${missingChannelContainerCount} 个可编排柜次缺少渠道快照,已隔离;请补齐后再参与智能装柜`" />
|
||||
<el-alert v-if="unmappedChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${unmappedChannelContainerCount} 个可编排柜次的渠道未纳入固定 9 类,已隔离`" />
|
||||
<el-alert v-if="unmappedWarehouseContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${unmappedWarehouseContainerCount} 个可编排柜次不属于义乌仓或东莞仓,已隔离`" />
|
||||
<el-alert v-if="completedContainerCount && !orchestratableContainers.length" type="success" :closable="false" show-icon title="当前柜次已完成装柜,不可继续编辑" />
|
||||
<QueryToolbar
|
||||
v-model="query"
|
||||
@@ -599,6 +585,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
preference-key="global-smart-loading-query-v2"
|
||||
:search-on-reset="false"
|
||||
@search="applyFilters"
|
||||
@reset="applyFilters"
|
||||
/>
|
||||
|
||||
<LoadErrorState v-if="loadError" title="智能装柜工作台加载失败" :message="loadError.message" :loading="loading" @retry="load">
|
||||
@@ -610,7 +597,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
</LoadErrorState>
|
||||
<section class="dispatch-grid" aria-label="智能装柜调度台">
|
||||
<section class="dispatch-pane candidate-pane">
|
||||
<header class="pane-header"><div><strong>候选装柜明细</strong><small>{{ activeWarehouse?.name || '未选择仓库' }} · {{ activeChannel?.name || '未选择渠道' }}</small></div></header>
|
||||
<header class="pane-header"><div><strong>候选装柜明细</strong><small>{{ selectedContainer?.warehouseName || '未选择发货仓' }} · {{ resolveSmartLoadingChannelGroup(selectedContainer?.channelCode, selectedContainer?.channelName)?.name || '未选择渠道' }}</small></div></header>
|
||||
<DataTable
|
||||
v-model:page="candidatePage"
|
||||
v-model:page-size="candidatePageSize"
|
||||
@@ -642,7 +629,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
</section>
|
||||
|
||||
<aside class="dispatch-pane container-pane" aria-label="货柜列表">
|
||||
<header class="pane-header"><div><strong>当前渠道柜次</strong></div></header>
|
||||
<header class="pane-header"><div><strong>已选渠道柜次</strong></div></header>
|
||||
<div v-loading="loading" class="container-list">
|
||||
<el-empty v-if="!filteredContainers.length" description="暂无符合条件的货柜" :image-size="72" />
|
||||
<button
|
||||
@@ -741,13 +728,12 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<style scoped>
|
||||
.global-loading-page { display: grid; gap: 12px; min-width: 0; }
|
||||
.embedded-smart-loading { min-width: 0; }
|
||||
.channel-summary-row { display: grid; grid-template-columns: minmax(360px, 1fr) auto; min-width: 0; align-items: end; border-bottom: 2px solid var(--cc-border); background: #fff; }
|
||||
.channel-tabs { min-width: 0; }
|
||||
.channel-tabs :deep(.el-tabs__header) { margin: 0 0 -2px; }
|
||||
.channel-tabs :deep(.el-tabs__nav-wrap::after) { display: none; }
|
||||
.channel-tabs :deep(.el-tabs__item) { overflow: hidden; max-width: 240px; height: 48px; padding-inline: 18px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.channel-tabs :deep(.el-tabs__content) { display: none; }
|
||||
.compact-summary { display: flex; min-height: 48px; align-items: center; justify-content: flex-end; gap: 16px; margin: 0; padding: 6px 12px; white-space: nowrap; }
|
||||
.channel-summary-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; min-width: 0; align-items: center; border-bottom: 2px solid var(--cc-border); background: #fff; }
|
||||
.channel-tags { display: flex; min-width: 0; align-items: center; flex-wrap: nowrap; gap: 6px; padding: 6px 0; overflow-x: auto; }
|
||||
.channel-tags__label { flex: 0 0 auto; margin-right: 2px; color: var(--cc-text); font-size: 13px; font-weight: 600; }
|
||||
.channel-tags :deep(.el-checkbox-button) { flex: 0 0 auto; }
|
||||
.channel-tags :deep(.el-checkbox-button__inner) { height: 34px; padding: 0 10px; border: 1px solid var(--cc-border) !important; border-radius: 6px !important; box-shadow: none !important; font-size: 13px; line-height: 32px; }
|
||||
.compact-summary { display: flex; min-height: 46px; align-items: center; justify-content: flex-end; gap: 12px; margin: 0; padding: 5px 8px; white-space: nowrap; }
|
||||
.compact-summary > div { display: flex; align-items: baseline; gap: 4px; }
|
||||
.compact-summary dt { color: var(--cc-muted); font-size: 12px; }
|
||||
.compact-summary dd { margin: 0; color: var(--cc-text); font-size: 12px; }
|
||||
@@ -803,6 +789,6 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
.pane-header,.plan-actions,.meter-title { align-items: stretch; flex-direction: column; }
|
||||
.plan-actions :deep(.el-button) { width: 100%; min-height: 40px; margin-left: 0; }
|
||||
.container-list { grid-template-columns: 1fr; }
|
||||
.channel-tabs :deep(.el-tabs__item) { max-width: 180px; padding-inline: 12px; }
|
||||
.channel-tags { padding-inline: 8px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
listPlatformUnifiedOrders,
|
||||
listPlatformUnifiedOrderFeeDetails,
|
||||
updatePlatformUnifiedOrderDeclaration,
|
||||
updatePlatformUnifiedOrderPlanning,
|
||||
} from './unifiedOrders'
|
||||
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
@@ -72,11 +73,11 @@ describe('unified orders API contract', () => {
|
||||
|
||||
it('normalizes C02 row IDs and keeps inboundId for measurement routing', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ data: {
|
||||
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', channelCode: ' NY_EXX ', channelName: '纽约渠道', locationCodes: 'A0-01, B2-03', actualGrossWeightKg: '12.5', actualVolumeCbm: '0.45', totalPalletCount: '3', totalNetWeightKg: '11.2', chargeableVolumeCbm: '0.5', totalQuantity: '48', skuNos: ['SKU-1', ''], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }],
|
||||
records: [{ orderId: '90001', orderVersion: '7', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', channelCode: ' NY_EXX ', channelName: '纽约渠道', locationCodes: 'A0-01, B2-03', estimatedVolumeCbm: '0.4', expectedVolumeCbm: '0.45', actualGrossWeightKg: '12.5', actualVolumeCbm: '0.46', totalPalletCount: '3', totalNetWeightKg: '11.2', chargeableVolumeCbm: '0.5', totalQuantity: '48', skuNos: ['SKU-1', ''], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }],
|
||||
page: 1, size: 20, total: 1, pages: 1,
|
||||
} })
|
||||
await expect(listPlatformUnifiedOrders({ page: 1, size: 20 })).resolves.toMatchObject({
|
||||
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, channelCode: 'NY_EXX', locationCodes: 'A0-01, B2-03', actualGrossWeightKg: 12.5, actualVolumeCbm: 0.45, totalPalletCount: 3, totalNetWeightKg: 11.2, chargeableVolumeCbm: 0.5, totalQuantity: 48, skuNos: ['SKU-1'], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }],
|
||||
records: [{ orderId: '90001', orderVersion: 7, tenantId: '208', inboundId: '77', inboundVersion: 3, channelCode: 'NY_EXX', locationCodes: 'A0-01, B2-03', estimatedVolumeCbm: 0.4, expectedVolumeCbm: 0.45, actualGrossWeightKg: 12.5, actualVolumeCbm: 0.46, totalPalletCount: 3, totalNetWeightKg: 11.2, chargeableVolumeCbm: 0.5, totalQuantity: 48, skuNos: ['SKU-1'], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -160,6 +161,23 @@ describe('unified orders API contract', () => {
|
||||
expect(http.put).toHaveBeenCalledWith('/platform/v1/orders/90001/declaration', request, { params: { tenantId: '208' } })
|
||||
})
|
||||
|
||||
it('updates arrival and operator-estimated volume through the dedicated versioned contract', async () => {
|
||||
const request = {
|
||||
expectedVersion: 7,
|
||||
expectedArrivalAt: '2026-09-10T18:30:00',
|
||||
estimatedVolumeCbm: 0.625,
|
||||
}
|
||||
vi.mocked(http.put).mockResolvedValue({ data: { previousVersion: 7, version: 8, changes: [] } })
|
||||
|
||||
await expect(updatePlatformUnifiedOrderPlanning('2089176869176389634', '2089223263733518337', request))
|
||||
.resolves.toMatchObject({ version: 8 })
|
||||
expect(http.put).toHaveBeenCalledWith(
|
||||
'/platform/v1/orders/2089223263733518337/planning',
|
||||
request,
|
||||
{ params: { tenantId: '2089176869176389634' } },
|
||||
)
|
||||
})
|
||||
|
||||
it('loads fee detail rows from the inbound fee-detail contract', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ data: [{ id: '1', inboundId: '88', reviewStatus: 'APPROVED', amount: 100 }] })
|
||||
await expect(listPlatformUnifiedOrderFeeDetails('208', '88')).resolves.toHaveLength(1)
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface UnifiedOrderCoLoadGroupSummary {
|
||||
|
||||
export interface UnifiedOrderResource {
|
||||
orderId: number | string
|
||||
orderVersion?: number | null
|
||||
tenantId: number | string
|
||||
orderNo: string | null
|
||||
mainStatus: UnifiedOrderMainStatus
|
||||
@@ -103,6 +104,7 @@ export interface UnifiedOrderResource {
|
||||
inboundCode: string | null
|
||||
expectedShipDate: string | null
|
||||
totalPackageCount: number | null
|
||||
estimatedVolumeCbm?: number | null
|
||||
expectedVolumeCbm?: number | null
|
||||
locationCodes: string | null
|
||||
actualGrossWeightKg: number | null
|
||||
@@ -362,6 +364,7 @@ export interface UpdateUnifiedOrderDeclarationRequest {
|
||||
expectedVersion: number
|
||||
customerRemark?: string | null
|
||||
expectedArrivalAt?: string | null
|
||||
estimatedVolumeCbm?: number | null
|
||||
channelCode: string
|
||||
channelName: string
|
||||
cargoes: UnifiedOrderDeclarationCargo[]
|
||||
@@ -384,6 +387,12 @@ export interface UpdateUnifiedOrderDeclarationResult {
|
||||
changes: UnifiedOrderDeclarationChange[]
|
||||
}
|
||||
|
||||
export interface UpdateUnifiedOrderPlanningRequest {
|
||||
expectedVersion: number
|
||||
expectedArrivalAt?: string | null
|
||||
estimatedVolumeCbm?: number | null
|
||||
}
|
||||
|
||||
function optionalText(value: string | null | undefined) {
|
||||
const normalized = value?.trim()
|
||||
return normalized || null
|
||||
@@ -397,6 +406,7 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource {
|
||||
return {
|
||||
...raw,
|
||||
orderId: String(raw.orderId),
|
||||
orderVersion: nullableInteger(raw.orderVersion),
|
||||
tenantId: String(raw.tenantId),
|
||||
customerId: id(raw.customerId),
|
||||
channelCode: optionalText(raw.channelCode),
|
||||
@@ -412,6 +422,7 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource {
|
||||
childCount: integer(raw.relation.childCount, 0),
|
||||
} : null,
|
||||
totalPackageCount: nullableInteger(raw.totalPackageCount),
|
||||
estimatedVolumeCbm: numberValue(raw.estimatedVolumeCbm),
|
||||
expectedVolumeCbm: numberValue(raw.expectedVolumeCbm),
|
||||
channelChanged: Boolean(raw.channelChanged),
|
||||
actualGrossWeightKg: numberValue(raw.actualGrossWeightKg),
|
||||
@@ -492,6 +503,18 @@ export function updatePlatformUnifiedOrderDeclaration(
|
||||
).then(unwrapApiResponse)
|
||||
}
|
||||
|
||||
export function updatePlatformUnifiedOrderPlanning(
|
||||
tenantId: string,
|
||||
orderId: string,
|
||||
request: UpdateUnifiedOrderPlanningRequest,
|
||||
) {
|
||||
return http.put<ApiEnvelope<UpdateUnifiedOrderDeclarationResult>, ApiEnvelope<UpdateUnifiedOrderDeclarationResult>>(
|
||||
`${unifiedOrdersApiPath}/${orderId}/planning`,
|
||||
request,
|
||||
{ params: { tenantId } },
|
||||
).then(unwrapApiResponse)
|
||||
}
|
||||
|
||||
export function downloadPlatformProductImage(tenantId: string, fileId: string) {
|
||||
return http.get<Blob, Blob>(
|
||||
`/platform/v1/files/product-images/${encodeURIComponent(tenantId)}/${encodeURIComponent(fileId)}`,
|
||||
|
||||
@@ -13,7 +13,7 @@ const detail = {
|
||||
orderId: '90001', tenantId: '208', inboundCode: 'RK-001', mainStatus: 'WAIT_MEASURE',
|
||||
receivingWarehouseId: '700000000000000001', receivingWarehouseName: '深圳仓', destinationWarehouseName: '洛杉矶中转仓',
|
||||
channelCode: 'OLD', channelName: '原渠道', channelChanged: false,
|
||||
expectedArrivalAt: '2026-09-06T10:00:00', customerRemark: '原备注',
|
||||
expectedArrivalAt: '2026-09-06T10:00:00', estimatedVolumeCbm: 0.625, customerRemark: '原备注',
|
||||
},
|
||||
order: {
|
||||
order: { version: 7, receivingWarehouseId: '700000000000000001', channelCode: 'OLD', channelName: '原渠道', customerRemark: '原备注' },
|
||||
@@ -68,7 +68,7 @@ describe('OrderDeclarationEditorDialog', () => {
|
||||
|
||||
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('原箱码继续有效,无需更换已打印箱唛'), '确认变更渠道', expect.any(Object))
|
||||
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('纸面上的旧渠道文字不会自动变化'), '确认变更渠道', expect.any(Object))
|
||||
expect(updatePlatformUnifiedOrderDeclaration).toHaveBeenCalledWith('208', '90001', expect.objectContaining({ expectedVersion: 7, channelCode: 'NEW', channelName: '新渠道' }))
|
||||
expect(updatePlatformUnifiedOrderDeclaration).toHaveBeenCalledWith('208', '90001', expect.objectContaining({ expectedVersion: 7, estimatedVolumeCbm: 0.625, channelCode: 'NEW', channelName: '新渠道' }))
|
||||
expect(ElMessage.success).toHaveBeenCalledWith(expect.stringContaining('订单已标记为“曾变更渠道”'))
|
||||
expect(wrapper.emitted('updated')).toHaveLength(1)
|
||||
expect(wrapper.emitted('update:modelValue')).toContainEqual([false])
|
||||
|
||||
@@ -36,6 +36,7 @@ const form = reactive({
|
||||
expectedVersion: 0,
|
||||
customerRemark: '',
|
||||
expectedArrivalAt: '',
|
||||
estimatedVolumeCbm: null as number | null,
|
||||
channelCode: '',
|
||||
channelName: '',
|
||||
cargoes: [] as EditableCargo[],
|
||||
@@ -67,6 +68,7 @@ function resetForm() {
|
||||
form.expectedVersion = Number(snapshot?.version ?? 0)
|
||||
form.customerRemark = snapshot?.customerRemark || props.detail.summary.customerRemark || ''
|
||||
form.expectedArrivalAt = props.detail.summary.expectedArrivalAt || ''
|
||||
form.estimatedVolumeCbm = props.detail.summary.estimatedVolumeCbm ?? null
|
||||
form.channelCode = snapshot?.channelCode || props.detail.summary.channelCode || ''
|
||||
form.channelName = snapshot?.channelName || props.detail.summary.channelName || ''
|
||||
originalChannel.code = form.channelCode.trim()
|
||||
@@ -191,6 +193,7 @@ async function save() {
|
||||
expectedVersion: form.expectedVersion,
|
||||
customerRemark: nullable(form.customerRemark),
|
||||
expectedArrivalAt: nullable(form.expectedArrivalAt),
|
||||
estimatedVolumeCbm: form.estimatedVolumeCbm,
|
||||
channelCode: form.channelCode.trim(),
|
||||
channelName: form.channelName.trim(),
|
||||
cargoes: form.cargoes.map(({ key: _key, ...cargo }) => ({
|
||||
@@ -245,10 +248,11 @@ async function save() {
|
||||
|
||||
<el-form label-position="top" class="order-fields">
|
||||
<el-row :gutter="14">
|
||||
<el-col :span="8"><el-form-item label="渠道名称" required><el-select v-model="form.channelCode" aria-label="渠道名称" filterable clearable :loading="channelsLoading" placeholder="请选择正式渠道" @change="selectChannel"><el-option v-for="channel in channels" :key="channel.channelCode" :label="`${channel.channelName} · ${channel.channelCode}`" :value="channel.channelCode" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="7"><el-form-item label="渠道名称" required><el-select v-model="form.channelCode" aria-label="渠道名称" filterable clearable :loading="channelsLoading" placeholder="请选择正式渠道" @change="selectChannel"><el-option v-for="channel in channels" :key="channel.channelCode" :label="`${channel.channelName} · ${channel.channelCode}`" :value="channel.channelCode" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item label="渠道编码"><el-input :model-value="form.channelCode" readonly aria-label="渠道编码" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="预计到货时间"><el-date-picker v-model="form.expectedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" clearable /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="客户备注"><el-input v-model="form.customerRemark" maxlength="1000" show-word-limit /></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item label="预估体积(m³)"><el-input-number v-model="form.estimatedVolumeCbm" :min="0.0001" :precision="4" :controls="false" /></el-form-item></el-col>
|
||||
<el-col :span="5"><el-form-item label="预计到货时间"><el-date-picker v-model="form.expectedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" clearable /></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item label="客户备注"><el-input v-model="form.customerRemark" maxlength="1000" show-word-limit /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import { listPlatformUnifiedOrders, updatePlatformUnifiedOrderPlanning } from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import { createPlatformExportTask } from '@/modules/downloads'
|
||||
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import UnifiedOrdersView from './UnifiedOrdersView.vue'
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
@@ -27,6 +28,7 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
getPlatformUnifiedOrder: vi.fn(),
|
||||
listPlatformUnifiedOrderFeeDetails: vi.fn(),
|
||||
listPlatformUnifiedOrders: vi.fn(),
|
||||
updatePlatformUnifiedOrderPlanning: vi.fn(),
|
||||
unifiedOrderStatusOptions: [
|
||||
{ value: 'PENDING_REVIEW', label: '待审核' },
|
||||
{ value: 'WAIT_MEASURE', label: '待测量数据' },
|
||||
@@ -49,8 +51,11 @@ function mountView() {
|
||||
AdminPage: { template: '<main><slot /></main>' },
|
||||
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
|
||||
QueryToolbar: { name: 'QueryToolbar', props: ['modelValue'], emits: ['update:modelValue', 'search', 'reset'], template: '<div data-testid="query-toolbar" />' },
|
||||
DataTable: { name: 'DataTable', props: ['page', 'pageSize'], emits: ['update:page', 'update:pageSize', 'selection-change'], template: '<div data-testid="data-table"><slot name="toolbar-actions" /></div>' },
|
||||
DataTable: { name: 'DataTable', props: ['page', 'pageSize', 'data'], emits: ['update:page', 'update:pageSize', 'selection-change'], template: '<div data-testid="data-table"><slot name="toolbar-actions" /><div v-for="row in data" :key="row.orderId" class="test-row"><slot name="estimatedVolume" :row="row" /><slot name="expectedArrival" :row="row" /></div></div>' },
|
||||
'el-button': { template: '<button><slot /></button>' },
|
||||
'el-input-number': { name: 'ElInputNumber', props: ['modelValue', 'disabled'], emits: ['update:modelValue', 'change'], template: '<input class="number-input" :value="modelValue" :disabled="disabled" @change="$emit(\'change\')" />' },
|
||||
'el-date-picker': { name: 'ElDatePicker', props: ['modelValue', 'disabled'], emits: ['update:modelValue', 'change'], template: '<input class="date-input" :value="modelValue" :disabled="disabled" @change="$emit(\'change\')" />' },
|
||||
ApiErrorMeta: { props: ['details'], template: '<code class="api-meta">{{ details.code }} {{ details.requestId }} {{ details.traceId }}</code>' },
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -59,6 +64,7 @@ function mountView() {
|
||||
describe('UnifiedOrdersView list request coordination', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(listPlatformUnifiedOrders).mockReset().mockResolvedValue(emptyPage())
|
||||
vi.mocked(updatePlatformUnifiedOrderPlanning).mockReset()
|
||||
vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ taskName: '国内仓订单', taskNo: 'EXP-1' } as never)
|
||||
vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false)
|
||||
})
|
||||
@@ -185,4 +191,41 @@ describe('UnifiedOrdersView list request coordination', () => {
|
||||
parameters: expect.objectContaining({ page: expect.anything(), size: expect.anything() }),
|
||||
}))
|
||||
})
|
||||
|
||||
it('edits arrival and estimated volume with the row version and preserves evidence on failure', async () => {
|
||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({
|
||||
records: [{
|
||||
orderId: '2089223263733518337', orderVersion: 7, tenantId: '2089176869176389634',
|
||||
inboundCode: 'NYEXX2609030001', expectedArrivalAt: '2026-09-10T08:30:00', estimatedVolumeCbm: 0.25,
|
||||
}], page: 1, size: 20, total: 1, pages: 1,
|
||||
} as never)
|
||||
vi.mocked(updatePlatformUnifiedOrderPlanning)
|
||||
.mockResolvedValueOnce({ version: 8 } as never)
|
||||
.mockRejectedValueOnce(new ApiResponseError('订单版本已变化', 'A0409', 200, undefined, 'req-planning', 'trace-planning'))
|
||||
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
const volume = wrapper.getComponent({ name: 'ElInputNumber' })
|
||||
volume.vm.$emit('update:modelValue', 0.5)
|
||||
volume.vm.$emit('change', 0.5)
|
||||
await flushPromises()
|
||||
|
||||
expect(updatePlatformUnifiedOrderPlanning).toHaveBeenNthCalledWith(1,
|
||||
'2089176869176389634', '2089223263733518337', {
|
||||
expectedVersion: 7, expectedArrivalAt: '2026-09-10T08:30:00', estimatedVolumeCbm: 0.5,
|
||||
})
|
||||
|
||||
const arrival = wrapper.getComponent({ name: 'ElDatePicker' })
|
||||
arrival.vm.$emit('update:modelValue', '2026-09-12T16:00:00')
|
||||
arrival.vm.$emit('change', '2026-09-12T16:00:00')
|
||||
await flushPromises()
|
||||
|
||||
expect(updatePlatformUnifiedOrderPlanning).toHaveBeenNthCalledWith(2,
|
||||
'2089176869176389634', '2089223263733518337', {
|
||||
expectedVersion: 8, expectedArrivalAt: '2026-09-12T16:00:00', estimatedVolumeCbm: 0.5,
|
||||
})
|
||||
expect(wrapper.getComponent({ name: 'ElDatePicker' }).props('modelValue')).toBe('2026-09-12T16:00:00')
|
||||
expect(wrapper.text()).toContain('订单版本已变化')
|
||||
expect(wrapper.text()).toContain('A0409 req-planning trace-planning')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
||||
'destinationWarehouseName', 'channelName', 'totalPackageCount', 'totalPalletCount', 'requestedPickupAt', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg',
|
||||
'actualVolumeCbm', 'totalNetWeightKg', 'chargeableWeightKg', 'chargeableVolumeCbm', 'expectedShipDate',
|
||||
'expectedVolumeCbm', 'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
|
||||
'estimatedVolumeCbm', 'expectedVolumeCbm', 'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
|
||||
'shipmentId', 'referenceId', 'containerNo', 'cabinetNo',
|
||||
'effectiveSailingAt', 'effectivePortArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
|
||||
]
|
||||
@@ -118,8 +118,20 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(columnSource).not.toContain("key: 'recommendedPalletType'")
|
||||
expect(columnSource).toContain("key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间'")
|
||||
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(13)
|
||||
expect(columnSource).toContain("key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 160, slotName: 'field' }")
|
||||
expect(columnSource).toContain("key: 'expectedVolumeCbm', prop: 'expectedVolumeCbm', label: '预计体积(m³)', minWidth: 130, slotName: 'field' }")
|
||||
expect(columnSource).toContain("key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 220, slotName: 'expectedArrival' }")
|
||||
expect(columnSource).toContain("key: 'estimatedVolumeCbm', prop: 'estimatedVolumeCbm', label: '预估体积(m³)', minWidth: 180, slotName: 'estimatedVolume' }")
|
||||
expect(columnSource).toContain("key: 'expectedVolumeCbm', prop: 'expectedVolumeCbm', label: '下单体积(m³)', minWidth: 130, slotName: 'field' }")
|
||||
const planningColumnPositions = [
|
||||
columnSource.indexOf("key: 'expectedArrivalAt'"),
|
||||
columnSource.indexOf("key: 'estimatedVolumeCbm'"),
|
||||
columnSource.indexOf("key: 'expectedVolumeCbm'"),
|
||||
columnSource.indexOf("key: 'actualVolumeCbm'"),
|
||||
]
|
||||
expect(planningColumnPositions).toEqual([...planningColumnPositions].sort((left, right) => left - right))
|
||||
expect(source).toContain("can('platform:order:declaration:edit')")
|
||||
expect(source).toContain('updatePlatformUnifiedOrderPlanning(String(row.tenantId), String(row.orderId)')
|
||||
expect(source).toContain('expectedVersion: row.orderVersion!')
|
||||
expect(source).toContain('<ApiErrorMeta :details="planningDraft(row as UnifiedOrderResource).error!" />')
|
||||
expect(columnSource).toContain("key: 'effectiveSailingAt', prop: 'effectiveSailingAt', label: '开船时间', minWidth: 180, slotName: 'field' }")
|
||||
expect(columnSource).toContain("key: 'effectivePortArrivalAt', prop: 'effectivePortArrivalAt', label: '到港时间', minWidth: 180, slotName: 'field' }")
|
||||
expect(source).toContain('preference-key="unified-orders-table-v2"')
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
batchApprovePlatformUnifiedOrders,
|
||||
addPlatformUnifiedOrderNote,
|
||||
listPlatformUnifiedOrders,
|
||||
updatePlatformUnifiedOrderPlanning,
|
||||
unifiedOrderStatusOptions,
|
||||
type UnifiedOrderDetailResource,
|
||||
type UnifiedOrderCoLoadGroupSummary,
|
||||
@@ -110,6 +111,15 @@ const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
type PlanningDraft = {
|
||||
expectedArrivalAt: string
|
||||
estimatedVolumeCbm: number | null
|
||||
savedExpectedArrivalAt: string
|
||||
savedEstimatedVolumeCbm: number | null
|
||||
saving: boolean
|
||||
error: ApiErrorDetails | null
|
||||
}
|
||||
const planningDrafts = reactive<Record<string, PlanningDraft | undefined>>({})
|
||||
const customerLoading = ref(false)
|
||||
const customerLoadError = ref<ApiErrorDetails | null>(null)
|
||||
const warehouseLoading = ref(false)
|
||||
@@ -455,18 +465,19 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '目的仓', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'channelName', prop: 'channelName', label: '渠道', minWidth: 180, slotName: 'channel' },
|
||||
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', minWidth: 100, slotName: 'field' },
|
||||
{ key: 'expectedVolumeCbm', prop: 'expectedVolumeCbm', label: '预计体积(m³)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 220, slotName: 'expectedArrival' },
|
||||
{ key: 'estimatedVolumeCbm', prop: 'estimatedVolumeCbm', label: '预估体积(m³)', minWidth: 180, slotName: 'estimatedVolume' },
|
||||
{ key: 'expectedVolumeCbm', prop: 'expectedVolumeCbm', label: '下单体积(m³)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'actualVolumeCbm', prop: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'totalPalletCount', prop: 'totalPalletCount', label: '总托数', minWidth: 100, slotName: 'field' },
|
||||
{ key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间', minWidth: 160, slotName: 'field' },
|
||||
{ key: 'locationCodes', prop: 'locationCodes', label: '库位位置', minWidth: 160, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'measurementReviewStatus', prop: 'measurementReviewStatus', label: '测量数据审核状态', minWidth: 170, slotName: 'measurementReviewStatus' },
|
||||
{ key: 'actualGrossWeightKg', prop: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'actualVolumeCbm', prop: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'totalNetWeightKg', prop: 'totalNetWeightKg', label: '货物净重(kg)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'chargeableWeightKg', prop: 'chargeableWeightKg', label: '计费毛重', minWidth: 120, slotName: 'field' },
|
||||
{ key: 'chargeableVolumeCbm', prop: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'expectedShipDate', prop: 'expectedShipDate', label: '预计出运日期', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 160, slotName: 'field' },
|
||||
{ key: 'feeReviewStatus', prop: 'feeReviewStatus', label: '费用审核状态', minWidth: 140, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'skuNos', prop: 'skuNos', label: 'SKU 编码', minWidth: 180, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'totalQuantity', prop: 'totalQuantity', label: '货物总数量', minWidth: 120, slotName: 'field', defaultVisible: false },
|
||||
@@ -533,6 +544,60 @@ function can(permission: string) {
|
||||
return access.can(permission)
|
||||
}
|
||||
|
||||
function planningDraft(row: UnifiedOrderResource) {
|
||||
const key = String(row.orderId)
|
||||
let draft = planningDrafts[key]
|
||||
if (!draft) {
|
||||
draft = {
|
||||
expectedArrivalAt: row.expectedArrivalAt || '',
|
||||
estimatedVolumeCbm: row.estimatedVolumeCbm ?? null,
|
||||
savedExpectedArrivalAt: row.expectedArrivalAt || '',
|
||||
savedEstimatedVolumeCbm: row.estimatedVolumeCbm ?? null,
|
||||
saving: false,
|
||||
error: null,
|
||||
}
|
||||
planningDrafts[key] = draft
|
||||
}
|
||||
return draft
|
||||
}
|
||||
|
||||
async function savePlanning(row: UnifiedOrderResource) {
|
||||
const draft = planningDraft(row)
|
||||
if (!can('platform:order:declaration:edit') || draft.saving) return
|
||||
const estimatedVolumeCbm = draft.estimatedVolumeCbm ?? null
|
||||
draft.estimatedVolumeCbm = estimatedVolumeCbm
|
||||
if (!Number.isInteger(row.orderVersion)) {
|
||||
draft.error = { message: '订单版本缺失,请刷新列表后重试' }
|
||||
return
|
||||
}
|
||||
if (estimatedVolumeCbm !== null && (!Number.isFinite(estimatedVolumeCbm) || estimatedVolumeCbm <= 0)) {
|
||||
draft.error = { message: '预估体积必须大于 0,或清空该值' }
|
||||
return
|
||||
}
|
||||
if (draft.expectedArrivalAt === draft.savedExpectedArrivalAt
|
||||
&& estimatedVolumeCbm === draft.savedEstimatedVolumeCbm) return
|
||||
|
||||
draft.saving = true
|
||||
draft.error = null
|
||||
try {
|
||||
const result = await updatePlatformUnifiedOrderPlanning(String(row.tenantId), String(row.orderId), {
|
||||
expectedVersion: row.orderVersion!,
|
||||
expectedArrivalAt: draft.expectedArrivalAt || null,
|
||||
estimatedVolumeCbm,
|
||||
})
|
||||
row.orderVersion = result.version
|
||||
row.expectedArrivalAt = draft.expectedArrivalAt || null
|
||||
row.estimatedVolumeCbm = estimatedVolumeCbm
|
||||
draft.savedExpectedArrivalAt = draft.expectedArrivalAt
|
||||
draft.savedEstimatedVolumeCbm = estimatedVolumeCbm
|
||||
ElMessage.success('订单运营预估已更新')
|
||||
} catch (error) {
|
||||
draft.error = getApiErrorDetails(error, '订单运营预估保存失败')
|
||||
} finally {
|
||||
draft.saving = false
|
||||
}
|
||||
}
|
||||
|
||||
const canEditDeclaration = computed(() => Boolean(
|
||||
detail.value
|
||||
&& detail.value.summary.mainStatus === 'WAIT_MEASURE'
|
||||
@@ -766,7 +831,7 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
|
||||
version: measurement.version,
|
||||
actualGoodsName: measurement.actualGoodsName,
|
||||
items: measurement.items.map((item) => ({
|
||||
inboundDetailId: item.inboundDetailId,
|
||||
inboundDetailId: item.inboundDetailId || undefined,
|
||||
boxCode: item.boxCode,
|
||||
itemNo: item.itemNo,
|
||||
boxSpecification: item.boxSpecification,
|
||||
@@ -827,7 +892,7 @@ async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
|
||||
version: measurement.version,
|
||||
actualGoodsName: measurement.actualGoodsName,
|
||||
items: measurement.items.map((item) => ({
|
||||
inboundDetailId: item.inboundDetailId,
|
||||
inboundDetailId: item.inboundDetailId || undefined,
|
||||
boxCode: item.boxCode,
|
||||
itemNo: item.itemNo,
|
||||
boxSpecification: item.boxSpecification,
|
||||
@@ -1142,6 +1207,7 @@ async function loadOrders() {
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
coLoadError.value = null
|
||||
Object.keys(planningDrafts).forEach((key) => delete planningDrafts[key])
|
||||
try {
|
||||
const activeFilters = appliedFilters.value
|
||||
const range = activeFilters.createdRange
|
||||
@@ -2075,6 +2141,51 @@ onBeforeUnmount(() => {
|
||||
<template #orderStatus="{ row }"><StatusBadge :status="orderStatusLabel((row as UnifiedOrderResource).internalOrderStatus || (row as UnifiedOrderResource).mainStatus)" /></template>
|
||||
<template #measurementReviewStatus="{ row }"><StatusBadge :status="measurementStatusLabel((row as UnifiedOrderResource).measurement?.reviewStatus)" /></template>
|
||||
<template #channel="{ row }"><div class="channel-cell"><span>{{ (row as UnifiedOrderResource).channelName || '—' }}</span><el-tag v-if="(row as UnifiedOrderResource).channelChanged" size="small" type="warning">曾变更渠道</el-tag></div></template>
|
||||
<template #estimatedVolume="{ row }">
|
||||
<div class="planning-cell">
|
||||
<el-input-number
|
||||
v-if="can('platform:order:declaration:edit')"
|
||||
v-model="planningDraft(row as UnifiedOrderResource).estimatedVolumeCbm"
|
||||
class="planning-volume-input"
|
||||
:min="0.0001"
|
||||
:precision="4"
|
||||
:controls="false"
|
||||
:disabled="planningDraft(row as UnifiedOrderResource).saving"
|
||||
placeholder="填写预估体积"
|
||||
:aria-label="`修改 ${(row as UnifiedOrderResource).inboundCode || '当前订单'} 预估体积`"
|
||||
@change="savePlanning(row as UnifiedOrderResource)"
|
||||
/>
|
||||
<span v-else>{{ formatNumber((row as UnifiedOrderResource).estimatedVolumeCbm, 4) }}</span>
|
||||
<small v-if="planningDraft(row as UnifiedOrderResource).saving">保存中…</small>
|
||||
<div v-if="planningDraft(row as UnifiedOrderResource).error" class="planning-error" role="alert">
|
||||
<span>{{ planningDraft(row as UnifiedOrderResource).error?.message }}</span>
|
||||
<ApiErrorMeta :details="planningDraft(row as UnifiedOrderResource).error!" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #expectedArrival="{ row }">
|
||||
<div class="planning-cell">
|
||||
<el-date-picker
|
||||
v-if="can('platform:order:declaration:edit')"
|
||||
v-model="planningDraft(row as UnifiedOrderResource).expectedArrivalAt"
|
||||
class="planning-arrival-input"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
clearable
|
||||
:disabled="planningDraft(row as UnifiedOrderResource).saving"
|
||||
placeholder="选择预计到货时间"
|
||||
:aria-label="`修改 ${(row as UnifiedOrderResource).inboundCode || '当前订单'} 预计到货时间`"
|
||||
@change="savePlanning(row as UnifiedOrderResource)"
|
||||
/>
|
||||
<span v-else>{{ formatDate((row as UnifiedOrderResource).expectedArrivalAt) }}</span>
|
||||
<small v-if="planningDraft(row as UnifiedOrderResource).saving">保存中…</small>
|
||||
<div v-if="planningDraft(row as UnifiedOrderResource).error" class="planning-error" role="alert">
|
||||
<span>{{ planningDraft(row as UnifiedOrderResource).error?.message }}</span>
|
||||
<ApiErrorMeta :details="planningDraft(row as UnifiedOrderResource).error!" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #field="{ row, column }"><LineClampText v-if="['customerRemark', 'latestInternalNote'].includes(String(column.property))" :content="displayField(row as UnifiedOrderResource, column.property)" :lines="2" /><template v-else>{{ displayField(row as UnifiedOrderResource, column.property) }}</template></template>
|
||||
<template #mainStatus="{ row }"><StatusBadge :status="statusLabel((row as UnifiedOrderResource).mainStatus)" /></template>
|
||||
<template #customer="{ row }"><div class="primary-cell"><strong>{{ customerName(row as UnifiedOrderResource) }}</strong></div></template>
|
||||
@@ -2218,6 +2329,13 @@ onBeforeUnmount(() => {
|
||||
.table-card :deep(.el-table__body tr:hover > td.el-table__cell) { background: #f3f7fb; }
|
||||
.row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
|
||||
.row-actions :deep(.el-button) { margin-left: 0; }
|
||||
.planning-cell { display: grid; min-width: 0; gap: 4px; }
|
||||
.planning-cell > small { color: var(--cc-muted); font-size: 11px; }
|
||||
.planning-volume-input,.planning-arrival-input { width: 100%; max-width: 100%; }
|
||||
.planning-cell :deep(.el-input__wrapper) { min-height: 32px; padding-right: 9px; padding-left: 9px; }
|
||||
.planning-cell :deep(.el-input__inner) { min-width: 0; text-align: left; }
|
||||
.planning-error { min-width: 0; color: var(--el-color-danger); font-size: 11px; line-height: 1.35; white-space: normal; }
|
||||
.planning-error :deep(.api-error-meta) { margin-top: 3px; }
|
||||
.inbound-relation-cell { display: grid; min-width: 0; gap: 5px; }
|
||||
.inbound-code-value { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.order-relation-link { width: fit-content; max-width: 100%; height: auto; justify-content: flex-start; padding: 0; font-size: 12px; }
|
||||
|
||||
Reference in New Issue
Block a user