优化
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
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 PriceModel = 'FBA_WAREHOUSE_RATE' | 'REGION_WEIGHT_TIER'
|
||||||
export type PricingTaxMode = 'TAX_INCLUDED' | 'TAX_EXCLUDED'
|
export type PricingTaxMode = 'TAX_INCLUDED' | 'TAX_EXCLUDED'
|
||||||
export type PricingDestinationType = 'FBA' | 'OVERSEAS'
|
export type PricingDestinationType = 'FBA' | 'OVERSEAS'
|
||||||
@@ -27,6 +27,9 @@ export interface PlatformPriceBookVersion {
|
|||||||
publishedBy: string | null
|
publishedBy: string | null
|
||||||
publishedAt: string | null
|
publishedAt: string | null
|
||||||
supersededAt: string | null
|
supersededAt: string | null
|
||||||
|
voidedBy: string | null
|
||||||
|
voidedAt: string | null
|
||||||
|
voidReason: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read-only channel data parsed from the uploaded workbook. */
|
/** Read-only channel data parsed from the uploaded workbook. */
|
||||||
@@ -102,7 +105,7 @@ export interface PlatformPriceBookQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PriceBookVersionPayload extends Omit<PlatformPriceBookVersion,
|
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
|
id: ApiId
|
||||||
customerId: ApiId
|
customerId: ApiId
|
||||||
versionNo: ApiNumber
|
versionNo: ApiNumber
|
||||||
@@ -113,6 +116,7 @@ interface PriceBookVersionPayload extends Omit<PlatformPriceBookVersion,
|
|||||||
warningCount?: ApiNumber | null
|
warningCount?: ApiNumber | null
|
||||||
createdBy?: ApiId | null
|
createdBy?: ApiId | null
|
||||||
publishedBy?: ApiId | null
|
publishedBy?: ApiId | null
|
||||||
|
voidedBy?: ApiId | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PriceBookParsedChannelPayload extends Omit<PlatformPriceBookParsedChannel,
|
interface PriceBookParsedChannelPayload extends Omit<PlatformPriceBookParsedChannel,
|
||||||
@@ -181,6 +185,9 @@ function normalizeVersion(value: PriceBookVersionPayload): PlatformPriceBookVers
|
|||||||
publishedBy: nullableId(value.publishedBy),
|
publishedBy: nullableId(value.publishedBy),
|
||||||
publishedAt: nullableText(value.publishedAt),
|
publishedAt: nullableText(value.publishedAt),
|
||||||
supersededAt: nullableText(value.supersededAt),
|
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))
|
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) {
|
export function downloadPlatformPriceBookSource(versionId: string) {
|
||||||
return http.get<Blob, Blob>(`${basePath}/${encodeURIComponent(versionId)}/source`, { responseType: 'blob' })
|
return http.get<Blob, Blob>(`${basePath}/${encodeURIComponent(versionId)}/source`, { responseType: 'blob' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
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 { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||||
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
|
|
||||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||||
import AppDialog from '@/shared/components/AppDialog.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 DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
|
||||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import {
|
import {
|
||||||
downloadPlatformPriceBookSource,
|
downloadPlatformPriceBookSource,
|
||||||
getPlatformPriceBook,
|
|
||||||
listPlatformPriceBooks,
|
listPlatformPriceBooks,
|
||||||
publishPlatformPriceBook,
|
|
||||||
uploadPlatformPriceBook,
|
uploadPlatformPriceBook,
|
||||||
type PlatformPriceBookDetail,
|
voidPlatformPriceBook,
|
||||||
type PlatformPriceBookParseIssue,
|
|
||||||
type PlatformPriceBookParsedRate,
|
|
||||||
type PlatformPriceBookVersion,
|
type PlatformPriceBookVersion,
|
||||||
type PriceBookConfirmationRequest,
|
|
||||||
type PriceBookStatus,
|
type PriceBookStatus,
|
||||||
} from '../api/platformPriceBooks'
|
} from '../api/platformPriceBooks'
|
||||||
|
|
||||||
@@ -34,21 +27,8 @@ type PriceBookFilters = QueryModel & {
|
|||||||
status: PriceBookStatus | ''
|
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 access = useAccessPreviewStore()
|
||||||
|
const router = useRouter()
|
||||||
const rows = ref<PlatformPriceBookVersion[]>([])
|
const rows = ref<PlatformPriceBookVersion[]>([])
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
@@ -60,18 +40,8 @@ const uploadFileError = ref('')
|
|||||||
const uploadCustomerId = ref('0')
|
const uploadCustomerId = ref('0')
|
||||||
const uploadInput = ref<HTMLInputElement>()
|
const uploadInput = ref<HTMLInputElement>()
|
||||||
const uploading = ref(false)
|
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 downloadingIds = ref(new Set<string>())
|
||||||
const mappingWarehouses = ref<PlatformWarehouseResource[]>([])
|
const voidingIds = ref(new Set<string>())
|
||||||
const mappingWarehousesLoaded = ref(false)
|
|
||||||
const mappingWarehousesLoading = ref(false)
|
|
||||||
const mappingWarehousesError = ref<ApiErrorDetails | null>(null)
|
|
||||||
const customerOptions = ref<PlatformTenantResource[]>([])
|
const customerOptions = ref<PlatformTenantResource[]>([])
|
||||||
const customersLoading = ref(false)
|
const customersLoading = ref(false)
|
||||||
const originSelections = ref<Record<string, string>>({})
|
const originSelections = ref<Record<string, string>>({})
|
||||||
@@ -79,8 +49,6 @@ const overseasSelections = ref<Record<string, string>>({})
|
|||||||
const filters = ref<PriceBookFilters>({ customerId: '0', status: '' })
|
const filters = ref<PriceBookFilters>({ customerId: '0', status: '' })
|
||||||
const appliedFilters = ref<PriceBookFilters>({ ...filters.value })
|
const appliedFilters = ref<PriceBookFilters>({ ...filters.value })
|
||||||
let loadSequence = 0
|
let loadSequence = 0
|
||||||
let detailSequence = 0
|
|
||||||
let warehouseSequence = 0
|
|
||||||
|
|
||||||
const queryFields = computed<QueryField[]>(() => [
|
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 pendingCount = computed(() => rows.value.filter((row) => row.status === 'PENDING_CONFIRMATION').length)
|
||||||
const selectedScopeLabel = computed(() => priceBookScopeLabel(appliedFilters.value.customerId))
|
const selectedScopeLabel = computed(() => priceBookScopeLabel(appliedFilters.value.customerId))
|
||||||
const loadErrorMessage = computed(() => loadError.value?.message || '')
|
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) {
|
function formatDateTime(value: string | null | undefined) {
|
||||||
return value ? value.replace('T', ' ').slice(0, 16) : '-'
|
return value ? value.replace('T', ' ').slice(0, 16) : '-'
|
||||||
|
|||||||
@@ -11,4 +11,15 @@ export const fbaPricingRoutes = [
|
|||||||
permission: 'firstleg:fba-pricing:rate:view',
|
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[]
|
] satisfies RouteRecordRaw[]
|
||||||
|
|||||||
Reference in New Issue
Block a user