优化
This commit is contained in:
@@ -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,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[]
|
||||
|
||||
Reference in New Issue
Block a user