From 44420919f6b5af20174823e14428b6f6ce30258a Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 8 Sep 2026 17:43:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E4=B8=93=E5=B1=9E=E5=A4=B4=E7=A8=8B=E6=8A=A5=E4=BB=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/platformPriceBooks.test.ts | 18 +++-- .../carrier-rates/api/platformPriceBooks.ts | 15 +++-- .../pages/CarrierRatesView.test.ts | 11 +++- .../carrier-rates/pages/CarrierRatesView.vue | 65 ++++++++++++++++--- 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/src/modules/fba-pricing/carrier-rates/api/platformPriceBooks.test.ts b/src/modules/fba-pricing/carrier-rates/api/platformPriceBooks.test.ts index faf2a07c..43216167 100644 --- a/src/modules/fba-pricing/carrier-rates/api/platformPriceBooks.test.ts +++ b/src/modules/fba-pricing/carrier-rates/api/platformPriceBooks.test.ts @@ -20,6 +20,7 @@ import { const version = { id: '9007199254740995', + customerId: '2096953508805279745', versionNo: '2', originalFileName: '2026-08-price-book.xlsx', sourceFileId: '9007199254740996', @@ -88,16 +89,17 @@ const detail = { describe('platform price-book API', () => { beforeEach(() => vi.clearAllMocks()) - it('uses the platform price-book list endpoint without customer scope and preserves long ids', async () => { + it('uses the selected customer price-book scope and preserves long ids', async () => { http.get.mockResolvedValue({ data: [version] }) - const result = await listPlatformPriceBooks({ status: ' PENDING_CONFIRMATION ' }) + const result = await listPlatformPriceBooks({ customerId: '2096953508805279745', status: ' PENDING_CONFIRMATION ' }) expect(http.get).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books', { - params: { status: 'PENDING_CONFIRMATION' }, + params: { customerId: '2096953508805279745', status: 'PENDING_CONFIRMATION' }, }) expect(result[0]).toMatchObject({ id: '9007199254740995', + customerId: '2096953508805279745', sourceFileId: '9007199254740996', createdBy: '10001', versionNo: 2, @@ -105,12 +107,12 @@ describe('platform price-book API', () => { }) }) - it('omits an empty status filter for the complete version history', async () => { + it('defaults an empty scope to the public price-book history', async () => { http.get.mockResolvedValue({ data: [] }) await listPlatformPriceBooks({ status: ' ' }) - expect(http.get).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books', { params: {} }) + expect(http.get).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books', { params: { customerId: '0' } }) }) it('uploads one workbook, loads its parsed detail, and normalizes parsed rows', async () => { @@ -118,10 +120,12 @@ describe('platform price-book API', () => { http.get.mockResolvedValue({ data: detail }) const file = new File(['book'], 'price-book.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) - const uploadResult = await uploadPlatformPriceBook(file) + const uploadResult = await uploadPlatformPriceBook(file, '2096953508805279745') const detailResult = await getPlatformPriceBook('9007199254740995') - expect(http.post).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books', expect.any(FormData)) + expect(http.post).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books', expect.any(FormData), { + params: { customerId: '2096953508805279745' }, + }) const form = http.post.mock.calls[0][1] as FormData expect(form.get('file')).toBe(file) expect(http.get).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books/9007199254740995') diff --git a/src/modules/fba-pricing/carrier-rates/api/platformPriceBooks.ts b/src/modules/fba-pricing/carrier-rates/api/platformPriceBooks.ts index 81b64e6c..e1cf9459 100644 --- a/src/modules/fba-pricing/carrier-rates/api/platformPriceBooks.ts +++ b/src/modules/fba-pricing/carrier-rates/api/platformPriceBooks.ts @@ -11,6 +11,7 @@ type ApiNumber = string | number export interface PlatformPriceBookVersion { id: string + customerId: string versionNo: number originalFileName: string sourceFileId: string | null @@ -96,12 +97,14 @@ export interface PriceBookConfirmationRequest { } export interface PlatformPriceBookQuery { + customerId?: string status?: PriceBookStatus | string } interface PriceBookVersionPayload extends Omit { + 'id' | 'customerId' | 'versionNo' | 'sourceFileId' | 'channelCount' | 'rateCount' | 'blockingErrorCount' | 'warningCount' | 'createdBy' | 'publishedBy'> { id: ApiId + customerId: ApiId versionNo: ApiNumber sourceFileId?: ApiId | null channelCount?: ApiNumber | null @@ -165,6 +168,7 @@ function normalizeVersion(value: PriceBookVersionPayload): PlatformPriceBookVers return { ...value, id: String(value.id), + customerId: String(value.customerId), versionNo: numberValue(value.versionNo), sourceFileId: nullableId(value.sourceFileId), channelCount: numberValue(value.channelCount), @@ -233,8 +237,9 @@ function normalizeDetail(value: PriceBookDetailPayload): PlatformPriceBookDetail export async function listPlatformPriceBooks(query: PlatformPriceBookQuery = {}): Promise { const status = query.status?.trim() + const customerId = query.customerId?.trim() || '0' const response = await http.get, ApiEnvelope>(basePath, { - params: status ? { status } : {}, + params: status ? { customerId, status } : { customerId }, }) return unwrapApiResponse(response).map(normalizeVersion) } @@ -246,10 +251,12 @@ export async function getPlatformPriceBook(versionId: string): Promise { +export async function uploadPlatformPriceBook(file: File, customerId = '0'): Promise { const form = new FormData() form.append('file', file) - const response = await http.post, ApiEnvelope>(basePath, form) + const response = await http.post, ApiEnvelope>(basePath, form, { + params: { customerId: customerId.trim() || '0' }, + }) return normalizeDetail(unwrapApiResponse(response)) } 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 c41aaf8c..351807d3 100644 --- a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts +++ b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts @@ -72,7 +72,16 @@ describe('CarrierRatesView price-book source contract', () => { 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(source).not.toContain('客户选择') + }) + + it('supports public and customer-specific price-book scopes without exposing technical ids', () => { + expect(source).toContain('listPlatformTenants') + expect(source).toContain("customerId: '0'") + expect(source).toContain('平台公共报价') + expect(source).toContain('客户专属') + expect(source).toContain('uploadPlatformPriceBook(file, uploadCustomerId.value)') + expect(source).toContain('只替换同一报价范围') + expect(source).not.toContain('{{ detail.version.customerId }}') }) it('shows parsing blockers and a channel-tabbed, read-only preview', () => { diff --git a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue index 60379e53..49817d7c 100644 --- a/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue +++ b/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue @@ -3,6 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue' import { AlertTriangle, CircleCheck, Download, Eye, FileSpreadsheet, FileUp, RefreshCw, Ship, Warehouse } from '@lucide/vue' import { ElMessage, ElMessageBox } from 'element-plus' import { useAccessPreviewStore } from '@/core/access/accessPreview' +import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' @@ -29,6 +30,7 @@ import { } from '../api/platformPriceBooks' type PriceBookFilters = QueryModel & { + customerId: string status: PriceBookStatus | '' } @@ -55,6 +57,7 @@ const loadError = ref(null) const uploadOpen = ref(false) const uploadFile = ref(null) const uploadFileError = ref('') +const uploadCustomerId = ref('0') const uploadInput = ref() const uploading = ref(false) const detailOpen = ref(false) @@ -69,15 +72,29 @@ const mappingWarehouses = ref([]) const mappingWarehousesLoaded = ref(false) const mappingWarehousesLoading = ref(false) const mappingWarehousesError = ref(null) +const customerOptions = ref([]) +const customersLoading = ref(false) const originSelections = ref>({}) const overseasSelections = ref>({}) -const filters = ref({ status: '' }) +const filters = ref({ customerId: '0', status: '' }) const appliedFilters = ref({ ...filters.value }) let loadSequence = 0 let detailSequence = 0 let warehouseSequence = 0 -const queryFields: QueryField[] = [ +const queryFields = computed(() => [ + { + key: 'customerId', + type: 'select', + width: 260, + label: '报价范围', + placeholder: '请选择报价范围', + props: { 'aria-label': '报价范围', filterable: true, loading: customersLoading.value }, + options: [ + { label: '平台公共报价', value: '0' }, + ...customerOptions.value.map((customer) => ({ label: `客户专属 · ${customer.tenantName || '未命名客户'}`, value: String(customer.id) })), + ], + }, { key: 'status', type: 'select', @@ -92,10 +109,11 @@ const queryFields: QueryField[] = [ { label: '解析失败', value: 'FAILED' }, ], }, -] +]) const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 210, 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 }, { key: 'status', label: '状态', width: 110, slotName: 'status' }, @@ -109,6 +127,7 @@ 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 || '') const isPendingDetail = computed(() => detail.value?.version.status === 'PENDING_CONFIRMATION') const domesticWarehouses = computed(() => mappingWarehouses.value.filter((warehouse) => ( @@ -222,6 +241,25 @@ function formatEffectivePeriod(version: PlatformPriceBookVersion) { return version.supersededAt ? `${start} 至 ${formatDateTime(version.supersededAt)}` : `${start} 起` } +function priceBookScopeLabel(customerId: string | null | undefined) { + if (!customerId || customerId === '0') return '平台公共报价' + const customer = customerOptions.value.find((item) => String(item.id) === String(customerId)) + return customer ? `客户专属 · ${customer.tenantName || '未命名客户'}` : '客户专属报价' +} + +async function loadCustomerOptions() { + customersLoading.value = true + try { + const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }) + customerOptions.value = result.records.filter((customer) => customer.status === 'ACTIVE') + } catch { + customerOptions.value = [] + ElMessage.warning('客户列表加载失败,仍可维护平台公共报价') + } finally { + customersLoading.value = false + } +} + function formatMoney(value: number, currency: string) { return `${currency} ${value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 4 })}` } @@ -291,7 +329,7 @@ async function loadPriceBooks() { loading.value = true loadError.value = null try { - const result = await listPlatformPriceBooks({ status: appliedFilters.value.status }) + const result = await listPlatformPriceBooks({ customerId: appliedFilters.value.customerId, status: appliedFilters.value.status }) if (sequence !== loadSequence) return rows.value = result } catch (error) { @@ -304,7 +342,10 @@ async function loadPriceBooks() { } async function applyQuery(model: QueryModel) { - appliedFilters.value = { status: (model.status || '') as PriceBookStatus | '' } + appliedFilters.value = { + customerId: String(model.customerId || '0'), + status: (model.status || '') as PriceBookStatus | '', + } page.value = 1 await loadPriceBooks() } @@ -317,6 +358,7 @@ function resetUpload() { function openUpload() { resetUpload() + uploadCustomerId.value = appliedFilters.value.customerId || '0' uploadOpen.value = true } @@ -477,7 +519,7 @@ async function submitUpload() { } uploading.value = true try { - const result = await uploadPlatformPriceBook(file) + const result = await uploadPlatformPriceBook(file, uploadCustomerId.value) uploadOpen.value = false resetUpload() await showUploadedDetail(result) @@ -530,7 +572,7 @@ async function confirmPriceBook() { if (!detail.value || !canConfirmDetail.value) return try { await ElMessageBox.confirm( - '确认后,该价格表会立即成为唯一生效版本,并替换当前生效版本。历史版本和原始文件将继续保留。', + `确认后,该价格表会立即成为“${priceBookScopeLabel(detail.value.version.customerId)}”的唯一生效版本,并仅替换同一报价范围的当前生效版本。历史版本和原始文件将继续保留。`, '确认并生效船期价格表', { type: 'warning', confirmButtonText: '确认并立即生效', cancelButtonText: '取消' }, ) @@ -592,6 +634,7 @@ watch(detailOpen, (open) => { }) onMounted(() => { + void loadCustomerOptions() void loadPriceBooks() }) @@ -650,6 +693,7 @@ onMounted(() => { + @@ -669,7 +713,8 @@ onMounted(() => { @confirm="submitUpload" >
- + +
选择 XLSX 文件 @@ -710,7 +755,7 @@ onMounted(() => { @@ -830,6 +875,8 @@ onMounted(() => { .issue-counts { color: var(--cc-muted); font-size: 12px; } .issue-counts .is-blocking, .is-danger { color: var(--el-color-danger); font-weight: 700; } .upload-content { display: grid; gap: 18px; } +.upload-scope-field { display: grid; gap: 6px; color: var(--cc-muted); font-size: 12px; } +.upload-scope-field :deep(.el-select) { width: 100%; } .visually-hidden-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; clip-path: inset(50%); } .upload-file-control { display: flex; align-items: center; gap: 14px; } .upload-file-name { display: grid; min-width: 0; gap: 3px; color: var(--cc-text); font-size: 13px; }