diff --git a/src/modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue b/src/modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue new file mode 100644 index 00000000..0f425f15 --- /dev/null +++ b/src/modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue @@ -0,0 +1,596 @@ + + + + + diff --git a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts index 613b886d..2f2d2b21 100644 --- a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts +++ b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts @@ -3,6 +3,8 @@ import { join } from 'node:path' import { describe, expect, it } from 'vitest' const source = readFileSync(join(process.cwd(), 'src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue'), 'utf8') +const detailSource = readFileSync(join(process.cwd(), 'src/modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue'), 'utf8') +const routeSource = readFileSync(join(process.cwd(), 'src/modules/fba-pricing/routes.ts'), 'utf8') const queryToolbarSource = readFileSync(join(process.cwd(), 'src/shared/components/QueryToolbar.vue'), 'utf8') describe('CarrierRatesView price-book source contract', () => { @@ -19,13 +21,14 @@ describe('CarrierRatesView price-book source contract', () => { }) it('uses only the parsed price-book workflow instead of legacy carrier-rate editing', () => { + const workflowSource = `${source}\n${detailSource}` for (const capability of [ 'listPlatformPriceBooks', 'getPlatformPriceBook', 'uploadPlatformPriceBook', 'publishPlatformPriceBook', 'downloadPlatformPriceBookSource', - ]) expect(source).toContain(capability) + ]) expect(workflowSource).toContain(capability) for (const legacyCapability of [ 'createPlatformCarrierRateVersion', @@ -35,43 +38,47 @@ describe('CarrierRatesView price-book source contract', () => { '编辑价格', '有效期', '生效日期', - ]) expect(source).not.toContain(legacyCapability) + ]) expect(workflowSource).not.toContain(legacyCapability) }) - it('keeps uploaded prices immutable and makes confirmation the only edit action', () => { + it('opens a formal detail route and keeps uploaded prices immutable', () => { expect(source).toContain('上传并解析') expect(source).toContain('查看并确认') - expect(source).toContain('确认并立即生效') - expect(source).toContain('价格行不可在页面内修改') - expect(source).toContain('confirmPriceBook') - expect(source).toContain('publishPlatformPriceBook') - expect(source).not.toContain('草稿') + expect(source).toContain("name: 'fba-carrier-rate-detail'") + expect(routeSource).toContain("path: 'first-leg-pricing/carrier-rates/:versionId'") + expect(routeSource).toContain("import('./carrier-rates/pages/CarrierRateDetailView.vue')") + expect(detailSource).toContain('返回列表') + expect(detailSource).toContain('确认并立即生效') + expect(detailSource).toContain('价格行不可在页面内修改') + expect(detailSource).toContain('confirmPriceBook') + expect(detailSource).toContain('publishPlatformPriceBook') + expect(`${source}\n${detailSource}`).not.toContain('草稿') }) it('shows all source-backed prototype price metadata without inventing cost breakdowns', () => { for (const label of ['渠道', '参考时效', '最晚进仓时间', '船期', '附加费', '备注', '生效状态']) { - expect(source).toContain(`label="${label}"`) + expect(detailSource).toContain(`label="${label}"`) } for (const field of ['referenceTransit', 'latestWarehouseEntry', 'estimatedDepartureText', 'surchargeDescription', 'remark']) { - expect(source).toContain(`selectedChannel?.${field}`) + expect(detailSource).toContain(`selectedChannel?.${field}`) } - expect(source).not.toContain('AMS+换单费') - expect(source).not.toContain('建议报价') + expect(detailSource).not.toContain('AMS+换单费') + expect(detailSource).not.toContain('建议报价') }) it('maps only active platform public domestic and overseas warehouses before confirmation', () => { - expect(source).toContain('listAllPlatformWarehouses()') - expect(source).toContain("warehouse.warehouseType === 'DOMESTIC'") - expect(source).toContain("warehouse.warehouseType === 'OVERSEAS'") - expect(source).toContain('起运仓映射') - expect(source).toContain('海外仓映射') - expect(source).toContain('originWarehouseCode') - expect(source).toContain('sourceDestinationName') - expect(source).toContain('autoSelectExactWarehouseMappings') - expect(source).toContain("'DUPLICATE_RATE'") - expect(source).toContain('.channel-tabs { width: 100%; min-width: 0;') - expect(source).toContain('.channel-preview :deep(.el-table) { width: 100%; max-width: 100%; min-width: 0; }') - expect(source).not.toContain('tenantId') + expect(detailSource).toContain('listAllPlatformWarehouses()') + expect(detailSource).toContain("warehouse.warehouseType === 'DOMESTIC'") + expect(detailSource).toContain("warehouse.warehouseType === 'OVERSEAS'") + expect(detailSource).toContain('起运仓映射') + expect(detailSource).toContain('海外仓映射') + expect(detailSource).toContain('originWarehouseCode') + expect(detailSource).toContain('sourceDestinationName') + expect(detailSource).toContain('autoSelectExactWarehouseMappings') + expect(detailSource).toContain("'DUPLICATE_RATE'") + expect(detailSource).toContain('.channel-tabs { width: 100%; min-width: 0;') + expect(detailSource).toContain('.channel-preview :deep(.el-table) { width: 100%; max-width: 100%; min-width: 0; }') + expect(detailSource).not.toContain('tenantId') }) it('supports public and customer-specific price-book scopes without exposing technical ids', () => { @@ -81,19 +88,19 @@ describe('CarrierRatesView price-book source contract', () => { expect(source).toContain('客户专属') expect(source).toContain('uploadPlatformPriceBook(file, uploadCustomerId.value)') expect(source).toContain('只替换同一报价范围') - expect(source).not.toContain('{{ detail.version.customerId }}') + expect(detailSource).not.toContain('{{ detail.version.customerId }}') }) it('shows parsing blockers and a channel-tabbed, read-only preview', () => { - expect(source).toContain('nonMappingBlockingIssues') - expect(source).toContain('解析与校验') - expect(source).toContain('按渠道预览') - expect(source).toContain('') - expect(source).toContain('') - expect(source).not.toContain('detail.issues" size="small" max-height="280" border stripe') + expect(detailSource).toContain('nonMappingBlockingIssues') + expect(detailSource).toContain('解析与校验') + expect(detailSource).toContain('按渠道预览') + expect(detailSource).toContain('') + expect(detailSource).toContain('') + expect(detailSource).not.toContain('detail.issues" size="small" max-height="280" border stripe') }) it('accepts only xlsx uploads and retains original-file download', () => { @@ -111,20 +118,20 @@ describe('CarrierRatesView price-book source contract', () => { expect(queryToolbarSource).toContain('flex-wrap: wrap') expect(source).toContain('@media (max-width: 720px)') expect(source).toContain('.table-actions :deep(.el-button) { min-height: 40px; }') - expect(source).toContain('@media (max-width: 420px)') - expect(source).toContain('.detail-grid { grid-template-columns: 1fr; }') + expect(detailSource).toContain('@media (max-width: 720px)') + expect(detailSource).toContain('.detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }') }) it('uses bordered striped tables and explicit semantic business buttons', () => { expect(source).toContain('{{ detailActionLabel') expect(source).toContain('原文件') - expect(source).toContain('确认并立即生效') - expect(source).toContain('.price-book-footer :deep(.el-button) { min-height: 44px; }') + expect(detailSource).toContain('确认并立即生效') + expect(detailSource).toContain("import PageToolbar from '@/shared/components/PageToolbar.vue'") expect(source).not.toContain('{{ detailActionLabel') expect(source).toContain('原文件') + expect(source).toContain('作废') expect(source).not.toContain('MoreHorizontal') }) }) diff --git a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue index 2703a6f6..16d22d0e 100644 --- a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue +++ b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue @@ -44,8 +44,6 @@ const downloadingIds = ref(new Set()) const voidingIds = ref(new Set()) const customerOptions = ref([]) const customersLoading = ref(false) -const originSelections = ref>({}) -const overseasSelections = ref>({}) const filters = ref({ customerId: '0', status: '' }) const appliedFilters = ref({ ...filters.value }) let loadSequence = 0 @@ -74,13 +72,14 @@ const queryFields = computed(() => [ { label: '待确认', value: 'PENDING_CONFIRMATION' }, { label: '生效中', value: 'ACTIVE' }, { label: '已替换', value: 'SUPERSEDED' }, + { label: '已作废', value: 'VOIDED' }, { label: '解析失败', value: 'FAILED' }, ], }, ]) const columns: DataTableColumn[] = [ - { key: 'actions', label: '操作', width: 210, fixed: 'left', slotName: 'actions' }, + { key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' }, { key: 'scope', label: '报价范围', minWidth: 190, slotName: 'scope' }, { key: 'version', label: '价格表版本', minWidth: 180, slotName: 'version' }, { key: 'originalFileName', prop: 'originalFileName', label: '原始文件', minWidth: 250, showOverflowTooltip: true, defaultVisible: false }, @@ -95,7 +94,6 @@ const canEdit = computed(() => access.can('firstleg:fba-pricing:rate:edit')) const pagedRows = computed(() => rows.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value)) const activeCount = computed(() => rows.value.filter((row) => row.status === 'ACTIVE').length) const pendingCount = computed(() => rows.value.filter((row) => row.status === 'PENDING_CONFIRMATION').length) -const selectedScopeLabel = computed(() => priceBookScopeLabel(appliedFilters.value.customerId)) const loadErrorMessage = computed(() => loadError.value?.message || '') function formatDateTime(value: string | null | undefined) { @@ -127,46 +125,17 @@ async function loadCustomerOptions() { } } -function formatMoney(value: number, currency: string) { - return `${currency} ${value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 4 })}` -} - function priceBookStatusMeta(status: PriceBookStatus) { const values = { PENDING_CONFIRMATION: { label: '待确认', type: 'warning' as const }, ACTIVE: { label: '生效中', type: 'success' as const }, SUPERSEDED: { label: '已替换', type: 'info' as const }, + VOIDED: { label: '已作废', type: 'info' as const }, FAILED: { label: '解析失败', type: 'danger' as const }, } return values[status] } -function priceModelLabel(value: string) { - return value === 'REGION_WEIGHT_TIER' ? '海派区域阶梯价' : '按仓价格' -} - -function pricingTaxModeLabel(value: string | null) { - if (value === 'TAX_INCLUDED') return '包税' - if (value === 'TAX_EXCLUDED') return '不包税' - return '-' -} - -function rateDestinationLabel(rate: PlatformPriceBookParsedRate) { - if (rate.destinationType === 'OVERSEAS') return rate.destinationName || rate.destinationCode || '-' - if (rate.destinationType === 'FBA') return rate.destinationCode || rate.destinationName || '-' - return rate.regionCode || '-' -} - -function issueLocation(issue: PlatformPriceBookParseIssue) { - return [issue.sourceSheetName, issue.sourceRowNumber ? `第 ${issue.sourceRowNumber} 行` : null, issue.sourceCell] - .filter(Boolean) - .join(' · ') || '-' -} - -function sourceLocation(rate: PlatformPriceBookParsedRate) { - return `${rate.sourceSheetName} · 第 ${rate.sourceRowNumber} 行` -} - function apiErrorMessage(error: unknown, fallback: string) { const details = getApiErrorDetails(error, fallback) return details.requestId ? `${details.message}(requestId: ${details.requestId})` : details.message @@ -248,134 +217,8 @@ function selectUploadFile(event: Event) { uploadFile.value = file } -function resetMappings(value: PlatformPriceBookDetail) { - const origins: Record = {} - const overseas: Record = {} - const originByKey = new Map() - const overseasByKey = new Map() - - for (const rate of value.rates) { - const originCode = rate.originWarehouseCode.trim() - if (originCode) { - const key = mappingKey(originCode) - if (!originByKey.has(key)) originByKey.set(key, { - key, - sourceCode: originCode, - sourceName: rate.originWarehouseName || originCode, - warehouseId: rate.originWarehouseId, - }) - } - if (rate.destinationType === 'OVERSEAS' && rate.destinationName?.trim()) { - const sourceName = rate.destinationName.trim() - const key = mappingKey(sourceName) - if (!overseasByKey.has(key)) overseasByKey.set(key, { - key, - sourceName, - destinationCode: rate.destinationCode, - warehouseId: rate.destinationWarehouseId, - }) - } - } - - for (const row of originByKey.values()) { - if (row.warehouseId) origins[row.key] = row.warehouseId - } - for (const row of overseasByKey.values()) { - if (row.warehouseId) overseas[row.key] = row.warehouseId - } - originSelections.value = origins - overseasSelections.value = overseas -} - -function autoSelectExactWarehouseMappings() { - const origins = { ...originSelections.value } - const overseas = { ...overseasSelections.value } - const usedOrigins = new Set(Object.values(origins)) - const usedOverseas = new Set(Object.values(overseas)) - - for (const row of originMappingRows.value) { - if (origins[row.key]) continue - const matches = domesticWarehouses.value.filter((warehouse) => originWarehouseMatches(row, warehouse)) - if (matches.length !== 1) continue - const warehouseId = String(matches[0].id) - if (!usedOrigins.has(warehouseId)) { - origins[row.key] = warehouseId - usedOrigins.add(warehouseId) - } - } - for (const row of overseasMappingRows.value) { - if (overseas[row.key]) continue - const matches = overseasWarehouses.value.filter((warehouse) => overseasWarehouseMatches(row, warehouse)) - if (matches.length !== 1) continue - const warehouseId = String(matches[0].id) - if (!usedOverseas.has(warehouseId)) { - overseas[row.key] = warehouseId - usedOverseas.add(warehouseId) - } - } - originSelections.value = origins - overseasSelections.value = overseas -} - -function applyDetail(value: PlatformPriceBookDetail) { - detail.value = value - selectedVersionId.value = value.version.id - selectedChannelCode.value = value.channels[0]?.channelCode || '' - resetMappings(value) - if (mappingWarehousesLoaded.value) autoSelectExactWarehouseMappings() -} - -async function loadMappingWarehouses(force = false) { - if (mappingWarehousesLoaded.value && !force) return true - const sequence = ++warehouseSequence - mappingWarehousesLoading.value = true - mappingWarehousesError.value = null - try { - const result = await listAllPlatformWarehouses() - if (sequence !== warehouseSequence) return false - mappingWarehouses.value = result - mappingWarehousesLoaded.value = true - autoSelectExactWarehouseMappings() - return true - } catch (error) { - if (sequence === warehouseSequence) { - mappingWarehouses.value = [] - mappingWarehousesLoaded.value = false - mappingWarehousesError.value = getApiErrorDetails(error, '平台公共仓库加载失败,请稍后重试') - } - return false - } finally { - if (sequence === warehouseSequence) mappingWarehousesLoading.value = false - } -} - -async function openDetail(row: PlatformPriceBookVersion) { - const sequence = ++detailSequence - selectedVersionId.value = row.id - detailOpen.value = true - detailLoading.value = true - detailError.value = null - detail.value = null - try { - const result = await getPlatformPriceBook(row.id) - if (sequence !== detailSequence) return - applyDetail(result) - if (result.version.status === 'PENDING_CONFIRMATION' && canEdit.value) await loadMappingWarehouses() - } catch (error) { - if (sequence !== detailSequence) return - detailError.value = getApiErrorDetails(error, '船期价格表详情加载失败,请稍后重试') - } finally { - if (sequence === detailSequence) detailLoading.value = false - } -} - -async function showUploadedDetail(value: PlatformPriceBookDetail) { - detailSequence += 1 - detailOpen.value = true - detailLoading.value = false - detailError.value = null - applyDetail(value) - if (value.version.status === 'PENDING_CONFIRMATION' && canEdit.value) await loadMappingWarehouses() +function openDetail(row: PlatformPriceBookVersion) { + void router.push({ name: 'fba-carrier-rate-detail', params: { versionId: row.id } }) } async function submitUpload() { @@ -389,9 +232,9 @@ async function submitUpload() { const result = await uploadPlatformPriceBook(file, uploadCustomerId.value) uploadOpen.value = false resetUpload() - await showUploadedDetail(result) - ElMessage.success('价格表已解析,请核对结果并确认仓库映射。') await loadPriceBooks() + ElMessage.success('价格表已解析,请在详情页核对结果并确认仓库映射。') + await router.push({ name: 'fba-carrier-rate-detail', params: { versionId: result.version.id } }) } catch (error) { ElMessage.error(apiErrorMessage(error, '价格表上传或解析失败,请检查文件后重试')) await loadPriceBooks() @@ -400,69 +243,6 @@ async function submitUpload() { } } -function originWarehouseMatches(row: OriginMappingRow, warehouse: PlatformWarehouseResource) { - return mappingKey(warehouse.warehouseCode) === mappingKey(row.sourceCode) -} - -function overseasWarehouseMatches(row: OverseasMappingRow, warehouse: PlatformWarehouseResource) { - return !row.destinationCode || mappingKey(warehouse.warehouseCode) === mappingKey(row.destinationCode) -} - -function selectedByOther(selections: Record, currentKey: string, warehouseId: string) { - return Object.entries(selections).some(([key, selected]) => key !== currentKey && selected === warehouseId) -} - -function originWarehouseDisabled(row: OriginMappingRow, warehouse: PlatformWarehouseResource) { - return !originWarehouseMatches(row, warehouse) - || selectedByOther(originSelections.value, row.key, String(warehouse.id)) -} - -function overseasWarehouseDisabled(row: OverseasMappingRow, warehouse: PlatformWarehouseResource) { - return !overseasWarehouseMatches(row, warehouse) - || selectedByOther(overseasSelections.value, row.key, String(warehouse.id)) -} - -function buildConfirmationRequest(): PriceBookConfirmationRequest { - return { - originMappings: originMappingRows.value.map((row) => ({ - originWarehouseCode: row.sourceCode, - warehouseId: originSelections.value[row.key], - })), - overseasMappings: overseasMappingRows.value.map((row) => ({ - sourceDestinationName: row.sourceName, - warehouseId: overseasSelections.value[row.key], - })), - } -} - -async function confirmPriceBook() { - if (!detail.value || !canConfirmDetail.value) return - try { - await ElMessageBox.confirm( - `确认后,该价格表会立即成为“${priceBookScopeLabel(detail.value.version.customerId)}”的唯一生效版本,并仅替换同一报价范围的当前生效版本。历史版本和原始文件将继续保留。`, - '确认并生效船期价格表', - { type: 'warning', confirmButtonText: '确认并立即生效', cancelButtonText: '取消' }, - ) - } catch (error) { - if (error === 'cancel' || error === 'close') return - ElMessage.error('确认操作未完成,请重试。') - return - } - - publishing.value = true - try { - const result = await publishPlatformPriceBook(detail.value.version.id, buildConfirmationRequest()) - applyDetail(result) - ElMessage.success('船期价格表已生效,上一生效版本已保留为历史版本。') - await loadPriceBooks() - } catch (error) { - ElMessage.error(apiErrorMessage(error, '确认生效失败,请根据校验结果修正后重试')) - if (selectedVersionId.value) await openDetail({ id: selectedVersionId.value } as PlatformPriceBookVersion) - } finally { - publishing.value = false - } -} - async function downloadSource(row: PlatformPriceBookVersion) { const next = new Set(downloadingIds.value) next.add(row.id) @@ -480,26 +260,54 @@ async function downloadSource(row: PlatformPriceBookVersion) { } } +async function voidPending(row: PlatformPriceBookVersion) { + if (row.status !== 'PENDING_CONFIRMATION' || !canEdit.value) return + let reason = '' + try { + const result = await ElMessageBox.prompt( + `作废后,版本 V${row.versionNo} 将不能再确认生效;版本记录和原始文件会继续保留。`, + '作废待确认价格表', + { + type: 'warning', + confirmButtonText: '确认作废', + cancelButtonText: '取消', + inputPlaceholder: '请输入作废原因', + inputValidator: (value) => value.trim() ? true : '请输入作废原因', + inputPattern: /^.{1,500}$/s, + inputErrorMessage: '作废原因不能超过 500 个字符', + }, + ) + reason = result.value.trim() + } catch (error) { + if (error === 'cancel' || error === 'close') return + ElMessage.error('作废操作未完成,请重试。') + return + } + + const next = new Set(voidingIds.value) + next.add(row.id) + voidingIds.value = next + try { + await voidPlatformPriceBook(row.id, reason) + ElMessage.success(`价格表 V${row.versionNo} 已作废`) + await loadPriceBooks() + } catch (error) { + ElMessage.error(apiErrorMessage(error, '价格表作废失败,请稍后重试')) + } finally { + const current = new Set(voidingIds.value) + current.delete(row.id) + voidingIds.value = current + } +} + function detailActionLabel(row: PlatformPriceBookVersion) { return row.status === 'PENDING_CONFIRMATION' && canEdit.value ? '查看并确认' : '查看详情' } -function closeDetail() { - if (publishing.value) return - detailOpen.value = false -} - watch(uploadOpen, (open) => { if (!open && !uploading.value) resetUpload() }) -watch(detailOpen, (open) => { - if (open || publishing.value) return - detailSequence += 1 - detailLoading.value = false - detailError.value = null -}) - onMounted(() => { void loadCustomerOptions() void loadPriceBooks() @@ -557,6 +365,14 @@ onMounted(() => { :loading="downloadingIds.has((row as PlatformPriceBookVersion).id)" @click="downloadSource(row as PlatformPriceBookVersion)" >原文件 + 作废 @@ -595,136 +411,6 @@ onMounted(() => { - - - - - - - - - -
-

仓库映射

来源编码必须匹配启用的平台公共仓库编码。

- - - - -
-
-

起运仓映射

-
-
{{ row.sourceName }}来源编码:{{ row.sourceCode }}
- - - -
-
-
-

海外仓映射

-
-
{{ row.sourceName }}目的仓编码:{{ row.destinationCode || '未提供' }}
- - - -
-
- - -
-
- -
-

解析与校验

阻断错误未解决时不能生效;价格行不可在页面内修改。

- - - - - - - -
- -
-

按渠道预览

显示上传 Excel 的解析结果,确认页不提供在线改价。

- - -
-
-
@@ -732,8 +418,6 @@ onMounted(() => { .price-book-page { min-width: 0; } .table-actions { display: flex; min-height: 32px; align-items: center; gap: 8px; } .table-actions :deep(.el-button + .el-button) { margin-left: 0; } -.price-book-footer { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; } -.price-book-footer :deep(.el-button) { margin-left: 0; } .version-cell { display: flex; min-width: 0; flex-direction: column; gap: 3px; } .version-cell strong { color: var(--cc-text); } .version-cell span, .metric-cell span { color: var(--cc-muted); font-size: 11px; } @@ -751,59 +435,8 @@ onMounted(() => { .upload-file-name span { color: var(--cc-muted); font-size: 12px; } .upload-file-name.is-empty strong { color: var(--cc-muted); font-weight: 400; } .upload-file-error { margin: -8px 0 0; color: var(--el-color-danger); font-size: 12px; } -.detail-load-error { display: flex; min-height: 220px; align-items: flex-start; justify-content: center; flex-direction: column; } -.detail-load-error strong { color: var(--cc-danger); font-size: 15px; } -.detail-load-error p { margin: 6px 0; color: var(--cc-text); font-size: 13px; } -.detail-load-error :deep(.el-button) { margin-top: 16px; } -.detail-content { display: grid; min-width: 0; gap: 20px; } -.detail-summary { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 18px; border-bottom: 1px solid var(--cc-border); } -.detail-summary__title, .detail-summary__actions { display: flex; min-width: 0; align-items: flex-start; gap: 10px; } -.detail-summary__title > svg { flex: 0 0 22px; margin-top: 2px; color: var(--cc-primary); } -.detail-summary__title h3 { margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 17px; line-height: 1.5; } -.detail-summary__title p { margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; } -.detail-summary__actions { flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; } -.detail-section { display: grid; gap: 14px; min-width: 0; } -.detail-section__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } -.detail-section__heading h3 { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--cc-text); font-size: 14px; } -.detail-section__heading h3 svg { color: var(--cc-primary); } -.detail-section__heading p { margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.55; } -.version-facts { padding-bottom: 18px; border-bottom: 1px solid var(--cc-border); } -.detail-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; border: 1px solid var(--cc-border); border-radius: 8px; overflow: hidden; } -.detail-grid > div { min-width: 0; padding: 12px 14px; border-right: 1px solid var(--cc-border); background: var(--cc-surface-soft, #f8fafc); } -.detail-grid > div:nth-child(3n) { border-right: 0; } -.detail-grid > div:nth-child(-n+3) { border-bottom: 1px solid var(--cc-border); } -.detail-grid dt { color: var(--cc-muted); font-size: 11px; } -.detail-grid dd { margin: 4px 0 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 15px; font-weight: 700; } -.mapping-section { padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: var(--cc-surface-soft, #f8fafc); } -.mapping-groups { display: grid; gap: 16px; } -.mapping-group { display: grid; gap: 8px; } -.mapping-group h4 { margin: 0; color: var(--cc-text); font-size: 13px; } -.mapping-row { display: grid; grid-template-columns: minmax(180px, 1fr) minmax(280px, 1fr); align-items: center; gap: 16px; padding: 12px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; } -.mapping-row > div { display: grid; min-width: 0; gap: 4px; } -.mapping-row strong { overflow-wrap: anywhere; color: var(--cc-text); font-size: 13px; } -.mapping-row span { overflow-wrap: anywhere; color: var(--cc-muted); font-size: 12px; } -.mapping-load-error { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 14px; border: 1px solid color-mix(in srgb, var(--cc-danger) 24%, var(--cc-border)); border-radius: 8px; background: #fff; } -.mapping-load-error strong { color: var(--cc-danger); font-size: 13px; } -.mapping-load-error p { margin: 4px 0; color: var(--cc-text); font-size: 12px; } -.channel-tabs { width: 100%; min-width: 0; margin-bottom: 0; overflow: hidden; } -.channel-preview :deep(.el-table) { width: 100%; max-width: 100%; min-width: 0; } -.channel-meta { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: -4px 0 8px; color: var(--cc-muted); font-size: 12px; } @media (max-width: 720px) { .table-actions :deep(.el-button) { min-height: 40px; } - .price-book-footer :deep(.el-button) { min-height: 44px; } - .upload-file-control, .detail-summary, .mapping-load-error { align-items: stretch; flex-direction: column; } - .detail-summary__actions { justify-content: flex-start; } - .detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .detail-grid > div:nth-child(3n) { border-right: 1px solid var(--cc-border); } - .detail-grid > div:nth-child(2n) { border-right: 0; } - .detail-grid > div:nth-child(-n+3) { border-bottom: 0; } - .detail-grid > div:nth-child(-n+4) { border-bottom: 1px solid var(--cc-border); } - .mapping-row { grid-template-columns: 1fr; gap: 10px; } - .mapping-row :deep(.el-select) { width: 100%; } -} -@media (max-width: 420px) { - .detail-grid { grid-template-columns: 1fr; } - .detail-grid > div, .detail-grid > div:nth-child(3n) { border-right: 0; border-bottom: 1px solid var(--cc-border); } - .detail-grid > div:last-child { border-bottom: 0; } + .upload-file-control { align-items: stretch; flex-direction: column; } }