This commit is contained in:
2026-09-11 18:44:01 +08:00
parent b9330a9674
commit 497530d6a9
3 changed files with 33 additions and 140 deletions
@@ -1,6 +1,6 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type PriceBookStatus = 'PENDING_CONFIRMATION' | 'ACTIVE' | 'SUPERSEDED' | 'FAILED'
export type PriceBookStatus = 'PENDING_CONFIRMATION' | 'ACTIVE' | 'SUPERSEDED' | 'VOIDED' | 'FAILED'
export type PriceModel = 'FBA_WAREHOUSE_RATE' | 'REGION_WEIGHT_TIER'
export type PricingTaxMode = 'TAX_INCLUDED' | 'TAX_EXCLUDED'
export type PricingDestinationType = 'FBA' | 'OVERSEAS'
@@ -27,6 +27,9 @@ export interface PlatformPriceBookVersion {
publishedBy: string | null
publishedAt: string | null
supersededAt: string | null
voidedBy: string | null
voidedAt: string | null
voidReason: string | null
}
/** Read-only channel data parsed from the uploaded workbook. */
@@ -102,7 +105,7 @@ export interface PlatformPriceBookQuery {
}
interface PriceBookVersionPayload extends Omit<PlatformPriceBookVersion,
'id' | 'customerId' | 'versionNo' | 'sourceFileId' | 'channelCount' | 'rateCount' | 'blockingErrorCount' | 'warningCount' | 'createdBy' | 'publishedBy'> {
'id' | 'customerId' | 'versionNo' | 'sourceFileId' | 'channelCount' | 'rateCount' | 'blockingErrorCount' | 'warningCount' | 'createdBy' | 'publishedBy' | 'voidedBy'> {
id: ApiId
customerId: ApiId
versionNo: ApiNumber
@@ -113,6 +116,7 @@ interface PriceBookVersionPayload extends Omit<PlatformPriceBookVersion,
warningCount?: ApiNumber | null
createdBy?: ApiId | null
publishedBy?: ApiId | null
voidedBy?: ApiId | null
}
interface PriceBookParsedChannelPayload extends Omit<PlatformPriceBookParsedChannel,
@@ -181,6 +185,9 @@ function normalizeVersion(value: PriceBookVersionPayload): PlatformPriceBookVers
publishedBy: nullableId(value.publishedBy),
publishedAt: nullableText(value.publishedAt),
supersededAt: nullableText(value.supersededAt),
voidedBy: nullableId(value.voidedBy),
voidedAt: nullableText(value.voidedAt),
voidReason: nullableText(value.voidReason),
}
}
@@ -271,6 +278,14 @@ export async function publishPlatformPriceBook(
return normalizeDetail(unwrapApiResponse(response))
}
export async function voidPlatformPriceBook(versionId: string, reason: string): Promise<PlatformPriceBookDetail> {
const response = await http.post<ApiEnvelope<PriceBookDetailPayload>, ApiEnvelope<PriceBookDetailPayload>>(
`${basePath}/${encodeURIComponent(versionId)}/void`,
{ reason },
)
return normalizeDetail(unwrapApiResponse(response))
}
export function downloadPlatformPriceBookSource(versionId: string) {
return http.get<Blob, Blob>(`${basePath}/${encodeURIComponent(versionId)}/source`, { responseType: 'blob' })
}
@@ -1,31 +1,24 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { AlertTriangle, CircleCheck, Download, Eye, FileSpreadsheet, FileUp, RefreshCw, Ship, Warehouse } from '@lucide/vue'
import { CircleX, Download, Eye, FileSpreadsheet, FileUp } from '@lucide/vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
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'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import {
downloadPlatformPriceBookSource,
getPlatformPriceBook,
listPlatformPriceBooks,
publishPlatformPriceBook,
uploadPlatformPriceBook,
type PlatformPriceBookDetail,
type PlatformPriceBookParseIssue,
type PlatformPriceBookParsedRate,
voidPlatformPriceBook,
type PlatformPriceBookVersion,
type PriceBookConfirmationRequest,
type PriceBookStatus,
} from '../api/platformPriceBooks'
@@ -34,21 +27,8 @@ type PriceBookFilters = QueryModel & {
status: PriceBookStatus | ''
}
interface OriginMappingRow {
key: string
sourceCode: string
sourceName: string
warehouseId: string | null
}
interface OverseasMappingRow {
key: string
sourceName: string
destinationCode: string | null
warehouseId: string | null
}
const access = useAccessPreviewStore()
const router = useRouter()
const rows = ref<PlatformPriceBookVersion[]>([])
const page = ref(1)
const pageSize = ref(20)
@@ -60,18 +40,8 @@ const uploadFileError = ref('')
const uploadCustomerId = ref('0')
const uploadInput = ref<HTMLInputElement>()
const uploading = ref(false)
const detailOpen = ref(false)
const detailLoading = ref(false)
const detailError = ref<ApiErrorDetails | null>(null)
const detail = ref<PlatformPriceBookDetail | null>(null)
const selectedVersionId = ref('')
const selectedChannelCode = ref('')
const publishing = ref(false)
const downloadingIds = ref(new Set<string>())
const mappingWarehouses = ref<PlatformWarehouseResource[]>([])
const mappingWarehousesLoaded = ref(false)
const mappingWarehousesLoading = ref(false)
const mappingWarehousesError = ref<ApiErrorDetails | null>(null)
const voidingIds = ref(new Set<string>())
const customerOptions = ref<PlatformTenantResource[]>([])
const customersLoading = ref(false)
const originSelections = ref<Record<string, string>>({})
@@ -79,8 +49,6 @@ const overseasSelections = ref<Record<string, string>>({})
const filters = ref<PriceBookFilters>({ customerId: '0', status: '' })
const appliedFilters = ref<PriceBookFilters>({ ...filters.value })
let loadSequence = 0
let detailSequence = 0
let warehouseSequence = 0
const queryFields = computed<QueryField[]>(() => [
{
@@ -129,107 +97,6 @@ const activeCount = computed(() => rows.value.filter((row) => row.status === 'AC
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) => (
warehouse.status === 'ACTIVE' && warehouse.warehouseType === 'DOMESTIC'
)))
const overseasWarehouses = computed(() => mappingWarehouses.value.filter((warehouse) => (
warehouse.status === 'ACTIVE' && warehouse.warehouseType === 'OVERSEAS'
)))
const originMappingRows = computed<OriginMappingRow[]>(() => {
const unique = new Map<string, OriginMappingRow>()
for (const rate of detail.value?.rates || []) {
const sourceCode = rate.originWarehouseCode.trim()
if (!sourceCode) continue
const key = mappingKey(sourceCode)
if (!unique.has(key)) {
unique.set(key, {
key,
sourceCode,
sourceName: rate.originWarehouseName || sourceCode,
warehouseId: rate.originWarehouseId,
})
}
}
return [...unique.values()]
})
const overseasMappingRows = computed<OverseasMappingRow[]>(() => {
const unique = new Map<string, OverseasMappingRow>()
for (const rate of detail.value?.rates || []) {
if (rate.destinationType !== 'OVERSEAS' || !rate.destinationName?.trim()) continue
const sourceName = rate.destinationName.trim()
const key = mappingKey(sourceName)
if (!unique.has(key)) {
unique.set(key, {
key,
sourceName,
destinationCode: rate.destinationCode,
warehouseId: rate.destinationWarehouseId,
})
}
}
return [...unique.values()]
})
const hasMissingOverseasSource = computed(() => (detail.value?.rates || []).some((rate) => (
rate.destinationType === 'OVERSEAS' && !rate.destinationName?.trim()
)))
const selectedChannelRates = computed(() => {
const rates = detail.value?.rates || []
return selectedChannelCode.value ? rates.filter((rate) => rate.channelCode === selectedChannelCode.value) : rates
})
const selectedChannel = computed(() => detail.value?.channels.find((channel) => channel.channelCode === selectedChannelCode.value) || null)
const mappingBlockingIssueCodes = new Set([
'ORIGIN_MAPPING_REQUIRED',
'ORIGIN_WAREHOUSE_INVALID',
'OVERSEAS_MAPPING_REQUIRED',
'OVERSEAS_WAREHOUSE_INVALID',
'WAREHOUSE_VALIDATION_FAILED',
'DUPLICATE_ORIGIN_MAPPING',
'DUPLICATE_OVERSEAS_MAPPING',
'UNUSED_ORIGIN_MAPPING',
'UNUSED_OVERSEAS_MAPPING',
'DUPLICATE_WAREHOUSE_MAPPING',
'DUPLICATE_RATE',
])
const nonMappingBlockingIssues = computed(() => (detail.value?.issues || []).filter((issue) => (
issue.severity === 'BLOCKING' && !mappingBlockingIssueCodes.has(issue.issueCode)
)))
const mappingsComplete = computed(() => (
originMappingRows.value.length > 0
&& originMappingRows.value.every((row) => Boolean(originSelections.value[row.key]))
&& overseasMappingRows.value.every((row) => Boolean(overseasSelections.value[row.key]))
))
const mappingCandidatesMissing = computed(() => (
originMappingRows.value.some((row) => !domesticWarehouses.value.some((warehouse) => originWarehouseMatches(row, warehouse)))
|| overseasMappingRows.value.some((row) => !overseasWarehouses.value.some((warehouse) => overseasWarehouseMatches(row, warehouse)))
))
const canConfirmDetail = computed(() => (
isPendingDetail.value
&& canEdit.value
&& !detailLoading.value
&& !publishing.value
&& !mappingWarehousesLoading.value
&& mappingWarehousesLoaded.value
&& !mappingWarehousesError.value
&& !hasMissingOverseasSource.value
&& !mappingCandidatesMissing.value
&& !nonMappingBlockingIssues.value.length
&& mappingsComplete.value
))
const confirmationHint = computed(() => {
if (!isPendingDetail.value) return ''
if (nonMappingBlockingIssues.value.length) return '当前文件仍有无法通过仓库映射解决的阻断错误,请修改原 Excel 后重新上传。'
if (hasMissingOverseasSource.value) return '存在未识别名称的海外仓价格行,无法完成映射。'
if (mappingWarehousesError.value) return '平台仓库候选加载失败,暂不能确认生效。'
if (mappingWarehousesLoading.value || !mappingWarehousesLoaded.value) return '正在加载平台仓库候选。'
if (mappingCandidatesMissing.value) return '上传表中的仓库编码尚未配置为匹配的启用平台公共仓库。'
if (!mappingsComplete.value) return '请完成所有起运仓和海外仓映射。'
return '确认后将立即替换当前生效版本,历史版本和原文件仍可查询。'
})
function mappingKey(value: string) {
return value.trim().toUpperCase()
}
function formatDateTime(value: string | null | undefined) {
return value ? value.replace('T', ' ').slice(0, 16) : '-'
+11
View File
@@ -11,4 +11,15 @@ export const fbaPricingRoutes = [
permission: 'firstleg:fba-pricing:rate:view',
},
},
{
path: 'first-leg-pricing/carrier-rates/:versionId',
name: 'fba-carrier-rate-detail',
component: () => import('./carrier-rates/pages/CarrierRateDetailView.vue'),
meta: {
title: '船期价格表详情',
description: '查看价格表版本、仓库映射和渠道价格',
permission: 'firstleg:fba-pricing:rate:view',
showPageHeading: false,
},
},
] satisfies RouteRecordRaw[]