|
|
|
@@ -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) : '-'
|
|
|
|
|