From 42fd6e48c40ee5154a9dbde0b30b51597ad739e2 Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Wed, 4 Mar 2026 17:43:06 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/views/ContractDetailView.vue | 2 +- src/components/views/Ht.vue | 92 ++++- .../views/ServiceCheckboxSelector.vue | 66 ++++ src/components/views/Xm.vue | 7 +- src/components/views/XmFactorGrid.vue | 12 +- src/components/views/XmMajorFactor.vue | 57 +++- src/components/views/ZxFwView.vue | 2 +- src/components/views/htInfo.vue | 39 ++- src/components/views/info.vue | 302 ++++++++++++++++ .../views/pricingView/HourlyPricingPane.vue | 4 +- .../InvestmentScalePricingPane.vue | 27 +- .../pricingView/LandScalePricingPane.vue | 43 ++- .../pricingView/MethodUnavailableNotice.vue | 22 ++ .../views/pricingView/WorkloadPricingPane.vue | 15 +- src/components/views/xmInfo.vue | 274 +++++++-------- src/components/views/zxFw.vue | 101 ++---- src/layout/tab.vue | 321 +++++++++++++++--- src/layout/typeLine.vue | 183 +++++----- src/lib/number.ts | 14 +- 19 files changed, 1163 insertions(+), 420 deletions(-) create mode 100644 src/components/views/ServiceCheckboxSelector.vue create mode 100644 src/components/views/info.vue create mode 100644 src/components/views/pricingView/MethodUnavailableNotice.vue diff --git a/src/components/views/ContractDetailView.vue b/src/components/views/ContractDetailView.vue index f38bf0e..95177e5 100644 --- a/src/components/views/ContractDetailView.vue +++ b/src/components/views/ContractDetailView.vue @@ -54,7 +54,7 @@ const zxfwView = markRaw( console.error('加载 zxFw 组件失败:', err); } }); - return () => h(AsyncZxFw, { contractId: props.contractId }); + return () => h(AsyncZxFw, { contractId: props.contractId, contractName: props.contractName }); } }) ); diff --git a/src/components/views/Ht.vue b/src/components/views/Ht.vue index aa7c856..79ccf8c 100644 --- a/src/components/views/Ht.vue +++ b/src/components/views/Ht.vue @@ -9,6 +9,7 @@ import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/ import { useTabStore } from '@/pinia/tab' import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next' import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive' +import { majorList } from '@/sql' import { AlertDialogAction, AlertDialogCancel, @@ -41,9 +42,13 @@ interface DataEntry { interface ContractSegmentPackage { version: number exportedAt: string + projectIndustry: string contracts: ContractItem[] localforageEntries: DataEntry[] } +interface XmBaseInfoState { + projectIndustry?: string +} const STORAGE_KEY = 'ht-card-v1' const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw' @@ -51,6 +56,7 @@ const CONTRACT_SEGMENT_VERSION = 1 const CONTRACT_KEY_PREFIX = 'ht-info-v3-' const SERVICE_KEY_PREFIX = 'zxFW-' const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-'] +const PROJECT_INFO_KEY = 'xm-base-info-v1' const tabStore = useTabStore() @@ -84,6 +90,7 @@ const contractListViewportRef = ref(null) const showScrollTopFab = ref(false) const isDraggingContracts = ref(false) const cardMotionState = ref<'enter' | 'ready'>('ready') +const canManageContracts = ref(false) let contractAutoScrollRaf = 0 let dragPointerClientY: number | null = null let cardEnterTimer: ReturnType | null = null @@ -101,6 +108,7 @@ const buildDefaultContracts = (): ContractItem[] => [ const normalizedSearchKeyword = computed(() => contractSearchKeyword.value.trim().toLowerCase()) const selectedExportCount = computed(() => selectedExportContractIds.value.length) +const hasContracts = computed(() => contracts.value.length > 0) const filteredContracts = computed(() => { if (!normalizedSearchKeyword.value) return contracts.value return contracts.value.filter(item => { @@ -143,7 +151,6 @@ const pendingDeleteContractName = computed(() => { const handleDeleteConfirmOpenChange = (open: boolean) => { deleteConfirmOpen.value = open - if (!open) pendingDeleteContractId.value = null } const requestDeleteContract = (id: string) => { @@ -163,6 +170,21 @@ const closeContractDataMenu = () => { contractDataMenuOpen.value = false } +const loadProjectBaseState = async () => { + try { + const data = await localforage.getItem(PROJECT_INFO_KEY) + canManageContracts.value = Boolean(typeof data?.projectIndustry === 'string' && data.projectIndustry.trim()) + } catch (error) { + console.error('load project base state failed:', error) + canManageContracts.value = false + } +} + +const getCurrentProjectIndustry = async (): Promise => { + const data = await localforage.getItem(PROJECT_INFO_KEY) + return typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : '' +} + const formatExportTimestamp = (date: Date): string => { const yyyy = date.getFullYear() const mm = String(date.getMonth() + 1).padStart(2, '0') @@ -172,6 +194,22 @@ const formatExportTimestamp = (date: Date): string => { return `${yyyy}${mm}${dd}-${hh}${mi}` } +const industryNameByCode = (() => { + const map = new Map() + for (const item of Object.values(majorList as Record)) { + if (!item?.code || !item?.name) continue + if (item.code.includes('-')) continue + map.set(item.code, item.name) + } + return map +})() + +const formatIndustryLabel = (code: string) => { + const trimmed = code.trim() + const name = industryNameByCode.get(trimmed) + return name ? `${trimmed} ${name}` : trimmed +} + const isContractSelectedForExport = (contractId: string) => selectedExportContractIds.value.includes(contractId) @@ -190,12 +228,14 @@ const exitContractExportMode = () => { } const enterContractExportMode = () => { + if (!hasContracts.value) return closeContractDataMenu() isExportSelecting.value = true selectedExportContractIds.value = [] } const triggerContractImport = () => { + if (!canManageContracts.value) return closeContractDataMenu() contractImportFileRef.value?.click() } @@ -397,10 +437,17 @@ const exportSelectedContracts = async () => { selectedContracts.map(item => item.id) ) + const projectIndustry = await getCurrentProjectIndustry() + if (!projectIndustry) { + window.alert('导出失败:未读取到当前项目工程行业,请先在“基础信息”里新建项目。') + return + } + const now = new Date() const payload: ContractSegmentPackage = { version: CONTRACT_SEGMENT_VERSION, exportedAt: now.toISOString(), + projectIndustry, contracts: selectedContracts, localforageEntries } @@ -443,6 +490,17 @@ const importContractSegments = async (event: Event) => { throw new Error('INVALID_CONTRACT_SEGMENT_PAYLOAD') } + const currentProjectIndustry = await getCurrentProjectIndustry() + if (!currentProjectIndustry) { + throw new Error('CURRENT_PROJECT_INDUSTRY_MISSING') + } + if (typeof payload.projectIndustry !== 'string' || !payload.projectIndustry.trim()) { + throw new Error('IMPORT_PACKAGE_INDUSTRY_MISSING') + } + if (payload.projectIndustry.trim() !== currentProjectIndustry) { + throw new Error(`PROJECT_INDUSTRY_MISMATCH:${payload.projectIndustry.trim()}:${currentProjectIndustry}`) + } + const importedContracts = normalizeContractsFromPayload(payload.contracts) if (importedContracts.length === 0) { throw new Error('EMPTY_CONTRACTS') @@ -483,7 +541,19 @@ const importContractSegments = async (event: Event) => { scrollContractsToBottom() } catch (error) { console.error('import contract segments failed:', error) - window.alert('导入失败:文件无效、已损坏或不是合同段导出文件。') + const message = error instanceof Error ? error.message : '' + if (message.startsWith('PROJECT_INDUSTRY_MISMATCH:')) { + const [, importIndustry = '', currentIndustry = ''] = message.split(':') + window.alert( + `导入失败:工程行业不一致(导入包:${formatIndustryLabel(importIndustry)},当前项目:${formatIndustryLabel(currentIndustry)})。` + ) + } else if (message === 'CURRENT_PROJECT_INDUSTRY_MISSING') { + window.alert('导入失败:当前项目未设置工程行业,请先在“基础信息”里新建项目。') + } else if (message === 'IMPORT_PACKAGE_INDUSTRY_MISSING') { + window.alert('导入失败:导入包缺少工程行业信息,请使用最新版本重新导出后再导入。') + } else { + window.alert('导入失败:文件无效、已损坏或不是合同段导出文件。') + } } finally { input.value = '' } @@ -540,6 +610,7 @@ const loadContracts = async () => { } const openCreateModal = () => { + if (!canManageContracts.value) return closeContractDataMenu() editingContractId.value = null contractNameInput.value = '' @@ -736,6 +807,7 @@ const handleGlobalMouseDown = (event: MouseEvent) => { } onMounted(async () => { + await loadProjectBaseState() await loadContracts() triggerCardEnterAnimation() await nextTick() @@ -744,6 +816,7 @@ onMounted(async () => { }) onActivated(() => { + void loadProjectBaseState() triggerCardEnterAnimation() void nextTick(() => { bindContractListScroll() @@ -782,7 +855,7 @@ onBeforeUnmount(() => {