diff --git a/public/data.js b/public/data.js index 0a4557c..0b74662 100644 --- a/public/data.js +++ b/public/data.js @@ -201,13 +201,13 @@ let data1 = { }, ], addtional: {// 附加工作费 - ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'C' }] }, + code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'C' }] }, name: '附加工作', fee: 10000, det: [ { id: 0, - ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'F' }] }, + code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'F' }] }, name: '人员驻场服务及其他附加工作', fee: 10000, m4: { @@ -258,7 +258,7 @@ let data1 = { }, { id: 1, - ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'X' }] }, + code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'X' }] }, name: '咨询服务协调工作', fee: 10000, m0: { @@ -314,7 +314,7 @@ let data1 = { ] }, reserve: {// 预备费 - ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'Y' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'B' }] }, + code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'Y' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'B' }] }, name: '预备费', fee: 10000, tasks:[], diff --git a/src/components/ht/Ht.vue b/src/components/ht/Ht.vue index 9dcb6b8..b39217a 100644 --- a/src/components/ht/Ht.vue +++ b/src/components/ht/Ht.vue @@ -12,6 +12,29 @@ import { useZxFwPricingHtFeeStore } from '@/pinia/zxFwPricingHtFee' import { useKvStore } from '@/pinia/kv' import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next' import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive' +import { + cloneJson, + CONTRACT_CONSULT_FACTOR_KEY_PREFIX, + CONTRACT_KEY_PREFIX, + CONTRACT_MAJOR_FACTOR_KEY_PREFIX, + CONTRACT_SEGMENT_FILE_EXTENSION, + CONTRACT_SEGMENT_VERSION, + formatExportTimestamp, + generateContractId, + isContractRelatedForageKey, + isContractRelatedKeyedStateKey, + isContractSegmentPackage, + isRecord, + normalizeContractSegmentPackage, + PROJECT_INFO_KEY, + PROJECT_SCALE_KEY, + PRICING_KEY_PREFIXES, + rewriteKeyWithContractId, + SERVICE_KEY_PREFIX, + SERVICE_PRICING_METHODS, + type ContractSegmentPackage, + type DataEntry +} from '@/lib/contractSegment' import { industryTypeList } from '@/sql' import { roundTo } from '@/lib/decimal' import { formatThousands } from '@/lib/numberFormat' @@ -39,43 +62,6 @@ interface ContractItem { createdAt: string } -interface DataEntry { - key: string - value: any -} - -interface ContractSegmentPackage { - version: number - exportedAt: string - packageType?: 'contract-segments' - project?: { - industry: string - } - storage?: { - localforageEntries: DataEntry[] - keyedEntries?: DataEntry[] - } - contracts: ContractItem[] - projectIndustry?: string - localforageEntries?: DataEntry[] - keyedEntries?: DataEntry[] - pinia?: { - zxFwPricing?: { - contracts?: Record - servicePricingStates?: Record - htFeeMainStates?: Record - htFeeMethodStates?: Record - } - } - piniaState?: { - zxFwPricing?: { - contracts?: Record - servicePricingStates?: Record - htFeeMainStates?: Record - htFeeMethodStates?: Record - } - } -} interface XmBaseInfoState { projectIndustry?: string } @@ -117,18 +103,6 @@ interface QuantityMethodStateLike { } const STORAGE_KEY = 'ht-card-v1' -const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw' -const CONTRACT_SEGMENT_VERSION = 3 -const CONTRACT_KEY_PREFIX = 'ht-info-v3-' -const SERVICE_KEY_PREFIX = 'zxFW-' -const CONTRACT_CONSULT_FACTOR_KEY_PREFIX = 'ht-consult-category-factor-v1-' -const CONTRACT_MAJOR_FACTOR_KEY_PREFIX = 'ht-major-factor-v1-' -const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-', 'htExtraFee-'] -const PROJECT_INFO_KEY = 'xm-base-info-v1' -const PROJECT_SCALE_KEY = 'xm-info-v3' -const SERVICE_PRICING_METHODS = ['investScale', 'landScale', 'workload', 'hourly'] as const - - const tabStore = useTabStore() const zxFwPricingStore = useZxFwPricingStore() const zxFwPricingKeysStore = useZxFwPricingKeysStore() @@ -431,15 +405,6 @@ const scheduleRefreshContractBudgets = () => { }, 80) } -const formatExportTimestamp = (date: Date): string => { - const yyyy = date.getFullYear() - const mm = String(date.getMonth() + 1).padStart(2, '0') - const dd = String(date.getDate()).padStart(2, '0') - const hh = String(date.getHours()).padStart(2, '0') - const mi = String(date.getMinutes()).padStart(2, '0') - return `${yyyy}${mm}${dd}-${hh}${mi}` -} - const industryNameByCode = (() => { const map = new Map() for (const item of industryTypeList) { @@ -623,31 +588,6 @@ const normalizeContractsFromPayload = (value: unknown): ContractItem[] => { }) } -const normalizeDataEntries = (value: unknown): DataEntry[] => { - if (!Array.isArray(value)) return [] - return value - .filter(item => item && typeof item === 'object' && typeof (item as DataEntry).key === 'string') - .map(item => ({ - key: String((item as DataEntry).key), - value: (item as DataEntry).value - })) -} - -const normalizeContractSegmentPackage = (payload: ContractSegmentPackage) => ({ - projectIndustry: - typeof payload.project?.industry === 'string' && payload.project.industry.trim() - ? payload.project.industry.trim() - : (typeof payload.projectIndustry === 'string' ? payload.projectIndustry.trim() : ''), - localforageEntries: normalizeDataEntries(payload.storage?.localforageEntries ?? payload.localforageEntries), - keyedEntries: normalizeDataEntries(payload.storage?.keyedEntries ?? payload.keyedEntries), - piniaState: payload.pinia ?? payload.piniaState -}) - -const isRecord = (value: unknown): value is Record => - Boolean(value && typeof value === 'object' && !Array.isArray(value)) - -const cloneJson = (value: T): T => JSON.parse(JSON.stringify(value)) as T - const buildContractPiniaPayload = async (contractIds: string[]) => { const idSet = new Set(contractIds.map(id => String(id || '').trim()).filter(Boolean)) const payload = { @@ -749,27 +689,6 @@ const applyImportedContractPiniaPayload = async ( } } -const isContractSegmentPackage = (value: unknown): value is ContractSegmentPackage => { - const payload = value as Partial | null - return Boolean(payload && typeof payload === 'object' && Array.isArray(payload.contracts)) -} - -const isContractRelatedForageKey = (key: string, contractId: string) => { - if (key === `${CONTRACT_KEY_PREFIX}${contractId}`) return true - if (key === `${SERVICE_KEY_PREFIX}${contractId}`) return true - if (key === `${CONTRACT_CONSULT_FACTOR_KEY_PREFIX}${contractId}`) return true - if (key === `${CONTRACT_MAJOR_FACTOR_KEY_PREFIX}${contractId}`) return true - if (PRICING_KEY_PREFIXES.some(prefix => key.startsWith(`${prefix}${contractId}-`))) return true - return false -} - -const isContractRelatedKeyedStateKey = (key: string, contractId: string) => { - if (key === `ht-base-info-${contractId}`) return true - if (key.startsWith(`work-content-${contractId}-`)) return true - if (key.startsWith(`work-content-htExtraFee-${contractId}-`)) return true - return false -} - const readContractRelatedForageEntries = async (contractIds: string[]) => { const keys = await kvStore.keys() const idSet = new Set(contractIds) @@ -802,39 +721,6 @@ const readContractRelatedKeyedEntries = (contractIds: string[]) => { })) } -const rewriteKeyWithContractId = (key: string, fromId: string, toId: string) => { - if (key === `${CONTRACT_KEY_PREFIX}${fromId}`) return `${CONTRACT_KEY_PREFIX}${toId}` - if (key === `${SERVICE_KEY_PREFIX}${fromId}`) return `${SERVICE_KEY_PREFIX}${toId}` - if (key === `${CONTRACT_CONSULT_FACTOR_KEY_PREFIX}${fromId}`) { - return `${CONTRACT_CONSULT_FACTOR_KEY_PREFIX}${toId}` - } - if (key === `${CONTRACT_MAJOR_FACTOR_KEY_PREFIX}${fromId}`) { - return `${CONTRACT_MAJOR_FACTOR_KEY_PREFIX}${toId}` - } - if (key === `ht-base-info-${fromId}`) return `ht-base-info-${toId}` - if (key.startsWith(`work-content-${fromId}-`)) { - return key.replace(`work-content-${fromId}-`, `work-content-${toId}-`) - } - if (key.startsWith(`work-content-htExtraFee-${fromId}-`)) { - return key.replace(`work-content-htExtraFee-${fromId}-`, `work-content-htExtraFee-${toId}-`) - } - for (const prefix of PRICING_KEY_PREFIXES) { - if (key.startsWith(`${prefix}${fromId}-`)) { - return key.replace(`${prefix}${fromId}-`, `${prefix}${toId}-`) - } - } - return key -} - -const generateContractId = (usedIds: Set) => { - let nextId = '' - while (!nextId || usedIds.has(nextId)) { - nextId = `ct-${Date.now()}-${Math.random().toString(16).slice(2, 8)}` - } - usedIds.add(nextId) - return nextId -} - const exportSelectedContracts = async () => { if (selectedContractIds.value.length === 0) { window.alert('请先勾选至少一个合同段。') diff --git a/src/components/ht/HtContractSummary.vue b/src/components/ht/HtContractSummary.vue index f4139c3..0d04eb3 100644 --- a/src/components/ht/HtContractSummary.vue +++ b/src/components/ht/HtContractSummary.vue @@ -1,5 +1,5 @@  + diff --git a/src/components/pricing/LandScalePricingPane.vue b/src/components/pricing/LandScalePricingPane.vue index f697cf2..75b9696 100644 --- a/src/components/pricing/LandScalePricingPane.vue +++ b/src/components/pricing/LandScalePricingPane.vue @@ -1,17 +1,70 @@ diff --git a/src/components/views/QuickCalcWorkbenchView.vue b/src/components/views/QuickCalcWorkbenchView.vue index b02cc1e..b3dd46e 100644 --- a/src/components/views/QuickCalcWorkbenchView.vue +++ b/src/components/views/QuickCalcWorkbenchView.vue @@ -190,10 +190,9 @@ const hasResolvedMajor = computed(() => effectiveMajorDictItem.value != null) const majorSupportsCostScale = computed(() => effectiveMajorDictItem.value?.hasCost === true) const majorSupportsLandScale = computed(() => effectiveMajorDictItem.value?.hasArea === true) const preferLandScaleForDualMajor = computed(() => majorSupportsCostScale.value && majorSupportsLandScale.value) -const workEnvCoefficient = computed(() => { - const parsed = Number(workEnvFactor.value) - return Number.isFinite(parsed) ? parsed : null -}) +const workEnvCoefficient = computed(() => + parseNumberOrNull(workEnvFactor.value, { sanitize: true, precision: 3 }) +) const consultSupportsScale = computed(() => selectedConsultDictItem.value?.scale === true) const consultOnlySupportsCostScale = computed(() => selectedConsultDictItem.value?.onlyCostScale === true) const canUseInvestScale = computed(() => @@ -244,10 +243,8 @@ const scaleBudgetPreview = computed(() => { { sanitize: true, precision: 3 } ) if (scaleValue == null) return null - console.log(mode) const rawSplit = getBenchmarkBudgetSplitByScale(scaleValue, mode) - console.log(rawSplit) if (!rawSplit) return null const checkedSplit = { @@ -305,6 +302,11 @@ const applyScaleInput = (field: 'invest' | 'land') => { landScale.value = normalized } +const applyWorkEnvFactorInput = () => { + const next = parseNumberOrNull(workEnvFactor.value, { sanitize: true, precision: 3 }) + workEnvFactor.value = next == null ? '' : String(next) +} + const totalSelectedCount = computed(() => { let count = 0 if (selectedConsultKey.value) count += 1 @@ -595,6 +597,7 @@ watch(canUseLandScale, enabled => { :disabled="!canUseInvestScale" :placeholder="investScalePlaceholder" @blur="applyScaleInput('invest')" + @keydown.enter.prevent="applyScaleInput('invest')" > @@ -609,6 +612,7 @@ watch(canUseLandScale, enabled => { :disabled="!canUseLandScale" :placeholder="landScalePlaceholder" @blur="applyScaleInput('land')" + @keydown.enter.prevent="applyScaleInput('land')" > @@ -656,7 +660,13 @@ watch(canUseLandScale, enabled => {
工作环境系数 - +