From 1d016f8c515ceb3b6761acadcadbdc5a4af1d59c Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Wed, 25 Mar 2026 17:18:35 +0800 Subject: [PATCH] i18n --- src/features/ht/components/Ht.vue | 216 ++++--- .../ht/components/HtAdditionalWorkFee.vue | 8 +- src/features/ht/components/HtBaseInfo.vue | 181 +++--- .../ht/components/HtConsultCategoryFactor.vue | 4 +- .../ht/components/HtContractSummary.vue | 42 +- .../ht/components/HtFeeRateMethodForm.vue | 14 +- src/features/ht/components/HtMajorFactor.vue | 4 +- src/features/ht/components/HtReserveFee.vue | 8 +- src/features/ht/components/htCard.vue | 34 +- src/features/ht/components/htInfo.vue | 4 +- src/features/ht/components/zxFw.vue | 142 +++-- .../pricing/components/HourlyPricingPane.vue | 4 +- .../components/InvestmentScalePricingPane.vue | 133 +++-- .../components/LandScalePricingPane.vue | 133 +++-- .../components/WorkloadPricingPane.vue | 96 ++- .../shared/components/HourlyFeeGrid.vue | 83 ++- src/features/shared/components/HtFeeGrid.vue | 48 +- .../shared/components/HtFeeMethodGrid.vue | 69 ++- .../components/MethodUnavailableNotice.vue | 6 +- .../components/ServiceCheckboxSelector.vue | 8 +- .../shared/components/WorkContentGrid.vue | 103 ++-- .../shared/components/XmFactorGrid.vue | 18 +- .../shared/components/xmCommonAgGrid.vue | 91 ++- src/features/tab/importExport.ts | 7 +- .../workbench/components/HomeEntryView.vue | 32 +- .../components/HtFeeMethodTypeLineView.vue | 29 +- .../components/QuickCalcWorkbenchView.vue | 112 ++-- .../workbench/components/ZxFwView.vue | 38 +- .../xm/components/XmConsultCategoryFactor.vue | 4 +- src/features/xm/components/XmMajorFactor.vue | 4 +- src/features/xm/components/info.vue | 59 +- src/features/xm/components/xmCard.vue | 18 +- src/i18n/locales/en-US.ts | 550 +++++++++++++++++- src/i18n/locales/zh-CN.ts | 550 +++++++++++++++++- src/layout/tab.vue | 285 +++++---- src/layout/typeLine.vue | 29 +- src/lib/agGridReadonlyAutoHeight.ts | 55 ++ src/lib/agGridResetHeader.ts | 11 +- src/lib/diyAgGridOptions.ts | 28 + src/lib/pricingScaleColumns.ts | 47 +- src/lib/projectRegistry.ts | 15 +- src/lib/workspace.ts | 4 +- src/main.ts | 3 + src/pinia/tab.ts | 3 +- src/pinia/uiPrefs.ts | 57 ++ src/sql.ts | 406 +++++++------ src/style.css | 11 +- tsconfig.tsbuildinfo | 2 +- 48 files changed, 2822 insertions(+), 986 deletions(-) create mode 100644 src/lib/agGridReadonlyAutoHeight.ts create mode 100644 src/pinia/uiPrefs.ts diff --git a/src/features/ht/components/Ht.vue b/src/features/ht/components/Ht.vue index ff15692..4d7512d 100644 --- a/src/features/ht/components/Ht.vue +++ b/src/features/ht/components/Ht.vue @@ -1,5 +1,6 @@ import { onBeforeUnmount, onMounted, ref, watch } from 'vue' -import { - useZxFwPricingStore } from '@/pinia/zxFwPricing' - +import { useI18n } from 'vue-i18n' +import { useZxFwPricingStore } from '@/pinia/zxFwPricing' + interface HtBaseInfoState { - quality: string - duration: - string + quality: string + duration: string } - - const DEFAULT_QUALITY = '造价咨询服务的综合评价应达到"较好"或综合评分90分' - const DEFAULT_DURATION = '' - - const props = - defineProps<{ - contractId: string - - }>() - - const zxFwPricingStore = useZxFwPricingStore() - const storageKey = () => - `ht-base-info-${props.contractId}` - - const quality = ref(DEFAULT_QUALITY) - const duration = ref('') - const - lastSavedSnapshot = ref('') - - const saveForm = (force = false) => { - const payload: HtBaseInfoState = { - - quality: quality.value, - duration: duration.value + +const { t } = useI18n() +const DEFAULT_QUALITY = t('htBaseInfo.defaultQuality') +const DEFAULT_DURATION = '' + +const props = defineProps<{ + contractId: string +}>() + +const zxFwPricingStore = useZxFwPricingStore() +const storageKey = () => `ht-base-info-${props.contractId}` +const quality = ref(DEFAULT_QUALITY) +const duration = ref('') +const lastSavedSnapshot = ref('') + +const saveForm = (force = false) => { + const payload: HtBaseInfoState = { + quality: quality.value, + duration: duration.value } - const snapshot = JSON.stringify(payload) - if (!force - && snapshot === lastSavedSnapshot.value) return - zxFwPricingStore.setKeyState(storageKey(), payload) - + const snapshot = JSON.stringify(payload) + if (!force && snapshot === lastSavedSnapshot.value) return + zxFwPricingStore.setKeyState(storageKey(), payload) lastSavedSnapshot.value = snapshot } - - const loadForm = async () => { - const data = await - zxFwPricingStore.loadKeyState(storageKey()) - const hasStoredValue = Boolean( - data && - (Object.prototype.hasOwnProperty.call(data, 'quality') || Object.prototype.hasOwnProperty.call(data, 'duration')) + +const loadForm = async () => { + const data = await zxFwPricingStore.loadKeyState(storageKey()) + const hasStoredValue = Boolean( + data && (Object.prototype.hasOwnProperty.call(data, 'quality') || Object.prototype.hasOwnProperty.call(data, 'duration')) ) - quality.value = typeof data?.quality === 'string' && - data.quality ? data.quality : DEFAULT_QUALITY - duration.value = typeof data?.duration === 'string' - ? data.duration - : (hasStoredValue ? '' : DEFAULT_DURATION) - const payload: HtBaseInfoState = { quality: quality.value, duration: duration.value } - - lastSavedSnapshot.value = JSON.stringify(payload) - if (!hasStoredValue) { - saveForm(true) - } + quality.value = typeof data?.quality === 'string' && data.quality ? data.quality : DEFAULT_QUALITY + duration.value = typeof data?.duration === 'string' ? data.duration : (hasStoredValue ? '' : DEFAULT_DURATION) + lastSavedSnapshot.value = JSON.stringify({ quality: quality.value, duration: duration.value }) + if (!hasStoredValue) saveForm(true) } - - watch([quality, duration], () => { saveForm() - }) - - onMounted(() => { void loadForm() }) - onBeforeUnmount(() => { saveForm(true) }) - - - - - - - 基础信息 - - - - 质量要求 - - - - - 工期要求 - - - - - - - - + +watch([quality, duration], () => { + saveForm() +}) + +onMounted(() => { + void loadForm() +}) + +onBeforeUnmount(() => { + saveForm(true) +}) + + + + + + + {{ t('htBaseInfo.title') }} + + + + {{ t('htBaseInfo.qualityLabel') }} + + + + {{ t('htBaseInfo.durationLabel') }} + + + + + + diff --git a/src/features/ht/components/HtConsultCategoryFactor.vue b/src/features/ht/components/HtConsultCategoryFactor.vue index c2ce0a2..babbcce 100644 --- a/src/features/ht/components/HtConsultCategoryFactor.vue +++ b/src/features/ht/components/HtConsultCategoryFactor.vue @@ -1,5 +1,6 @@ import { computed, markRaw, defineAsyncComponent, defineComponent, h, onActivated, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue'; +import { useI18n } from 'vue-i18n' import TypeLine from '@/layout/typeLine.vue'; import { useZxFwPricingStore } from '@/pinia/zxFwPricing' import { roundTo, sumNullableNumbers, toFiniteNumber } from '@/lib/decimal' @@ -28,6 +29,7 @@ const props = defineProps<{ projectConsultCategoryFactorKey?: string; // 工作区咨询分类系数键 projectMajorFactorKey?: string; // 工作区工程专业系数键 }>(); +const { t } = useI18n() const zxFwPricingStore = useZxFwPricingStore() interface HtFeeMainRowLike { @@ -65,7 +67,9 @@ const typeLineStorageKey = computed(() => `project-active-cat-${props.contractId let budgetRefreshTimer: ReturnType | null = null const formatBudgetAmount = (value: number | null | undefined) => - typeof value === 'number' && Number.isFinite(value) ? `${formatThousands(value, 2)} 元` : '--' + typeof value === 'number' && Number.isFinite(value) + ? `${formatThousands(value, 2)} ${t('htCard.currencySuffix')}` + : '--' const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null => { const rows = Array.isArray(state?.detailRows) ? state.detailRows : [] @@ -326,18 +330,16 @@ const summaryView = markRaw( ) // 4. 给分类数组添加严格类型标注 -const xmCategories: XmCategoryItem[] = [ - { key: 'base-info', label: '基础信息', component: htBaseInfoView }, - { key: 'info', label: '规模信息', component: htView }, - { key: 'contract', label: '咨询服务', component: zxfwView }, - { key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView }, - { key: 'major-factor', label: '工程专业系数', component: majorFactorView }, - { key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView }, - { key: 'reserve-fee', label: '预备费', component: reserveFeeView }, - { key: 'all', label: '汇总', component: summaryView }, - - -]; +const xmCategories = computed(() => [ + { key: 'base-info', label: t('htCard.categories.baseInfo'), component: htBaseInfoView }, + { key: 'info', label: t('htCard.categories.scaleInfo'), component: htView }, + { key: 'contract', label: t('htCard.categories.services'), component: zxfwView }, + { key: 'consult-category-factor', label: t('htCard.categories.consultFactor'), component: consultCategoryFactorView }, + { key: 'major-factor', label: t('htCard.categories.majorFactor'), component: majorFactorView }, + { key: 'additional-work-fee', label: t('htCard.categories.additionalFee'), component: additionalWorkFeeView }, + { key: 'reserve-fee', label: t('htCard.categories.reserveFee'), component: reserveFeeView }, + { key: 'all', label: t('htCard.categories.summary'), component: summaryView }, +]); watch(budgetRefreshSignature, (next, prev) => { if (next === prev) return diff --git a/src/features/ht/components/htInfo.vue b/src/features/ht/components/htInfo.vue index 3c7e39d..e414ed0 100644 --- a/src/features/ht/components/htInfo.vue +++ b/src/features/ht/components/htInfo.vue @@ -1,5 +1,6 @@ - + diff --git a/src/features/ht/components/zxFw.vue b/src/features/ht/components/zxFw.vue index ec28733..8971d67 100644 --- a/src/features/ht/components/zxFw.vue +++ b/src/features/ht/components/zxFw.vue @@ -1,10 +1,12 @@ -import { computed, nextTick, ref } from 'vue' +import { computed, nextTick, ref, watch } from 'vue' +import { useI18n } from 'vue-i18n' import { AgGridVue } from 'ag-grid-vue3' import type { ColDef, ColGroupDef, GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community' -import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql' +import { getIndustryDisplayName, getMajorDictEntries, getServiceDictItemById, isMajorIdInIndustryScope } from '@/sql' import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions' import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' import { formatThousandsFlexible } from '@/lib/numberFormat' +import { withReadonlyAutoHeight } from '@/lib/agGridReadonlyAutoHeight' import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync' import { useZxFwPricingStore } from '@/pinia/zxFwPricing' import { useKvStore } from '@/pinia/kv' @@ -145,6 +147,7 @@ const props = defineProps<{ projectInfoKey?: string }>() const zxFwPricingStore = useZxFwPricingStore() +const { t, locale } = useI18n() const kvStore = useKvStore() const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`) const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) @@ -165,12 +168,6 @@ const gridApi = ref | null>(null) const lastAppliedConsultFactorChangeAt = ref(0) const lastAppliedMajorFactorChangeAt = ref(0) const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__' -const industryNameMap = new Map( - industryTypeList.flatMap(item => [ - [String(item.id).trim(), item.name], - [String(item.type).trim(), item.name] - ]) -) const getDefaultConsultCategoryFactor = () => consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null @@ -194,8 +191,8 @@ const inferProjectCountFromRows = (rows?: Array>) => { return inferScaleProjectCountFromRows(rows, isMutipleService.value) } const totalLabel = computed(() => { - const industryName = industryNameMap.get(activeIndustryCode.value.trim()) || '' - return industryName ? `${industryName}总投资` : '总投资' + const industryName = getIndustryDisplayName(activeIndustryCode.value.trim(), locale.value) + return industryName ? t('pricingScale.totalInvestmentByIndustry', { industryName }) : t('pricingScale.totalInvestment') }) const loadFactorDefaults = async () => { @@ -269,13 +266,24 @@ type majorLite = { hasArea?: boolean industryId?: string | number | null } -const serviceEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, majorLite]) -const detailDict: ScaleDictGroup[] = buildScaleDetailDict( - serviceEntries, - ({ hasCost, hasArea }) => hasCost && !hasArea -) - -const idLabelMap = buildScaleIdLabelMap(detailDict) +const serviceEntries: Array<[string, majorLite]> = [] +const detailDict: ScaleDictGroup[] = [] +const idLabelMap = new Map() +const rebuildScaleDictCaches = () => { + const nextServiceEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, majorLite]) + serviceEntries.splice(0, serviceEntries.length, ...nextServiceEntries) + const nextDetailDict = buildScaleDetailDict( + serviceEntries, + ({ hasCost, hasArea }) => hasCost && !hasArea + ) + detailDict.splice(0, detailDict.length, ...nextDetailDict) + const nextIdLabelMap = buildScaleIdLabelMap(nextDetailDict) + idLabelMap.clear() + nextIdLabelMap.forEach((label, id) => { + idLabelMap.set(id, label) + }) +} +rebuildScaleDictCaches() const buildDefaultRows = (projectCountValue = getTargetProjectCount()): DetailRow[] => { return buildScaleRowsFromDict({ @@ -522,7 +530,7 @@ const formatEditableMoney = (params: any) => formatScaleEditableConditionalNumber(params, { enabled: Boolean(params.data?.hasCost), precision: 3, - emptyText: '点击输入' + emptyText: t('pricingScale.clickToInput') }) const createBudgetCellRendererWithCheck = createScaleBudgetCellRendererToggleFactory( @@ -638,12 +646,12 @@ const restoreMajorFactorColumnDefaults = async () => { const columnDefs: Array | ColGroupDef> = [ createScaleValueColumn({ - headerName: '造价金额(万元)', + headerName: t('pricingScale.columns.investAmount'), field: 'amount', - headerTooltip: '点击右侧↻恢复本列默认造价金额', + headerTooltip: t('pricingScale.tooltip.resetInvestAmount'), headerComponent: AgGridResetHeader, onReset: restoreAmountColumnDefaults, - resetTitle: '恢复本列默认造价金额', + resetTitle: t('pricingScale.tooltip.resetInvestAmount'), minWidth: 90, flex: 2, isEditable: row => Boolean(row?.hasCost), @@ -665,6 +673,7 @@ const columnDefs: Array | ColGroupDef> = [ }), createScaleRemarkColumn() ] +const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs)) const autoGroupColumnDef: ColDef = createScaleAutoGroupColumn({ totalLabel: totalLabel.value, @@ -871,6 +880,56 @@ const applyDetailRows = (rows: DetailRow[]) => { detailRows.value = rows } +const relabelDetailRowsFromDict = async () => { + rebuildScaleDictCaches() + if (detailRows.value.length === 0) { + gridApi.value?.refreshCells({ force: true }) + return + } + const majorMetaMap = new Map>() + for (const group of detailDict) { + majorMetaMap.set(group.id, { + groupCode: group.code, + groupName: group.name, + majorCode: group.code, + majorName: group.name + }) + for (const child of group.children) { + majorMetaMap.set(child.id, { + groupCode: group.code, + groupName: group.name, + majorCode: child.code, + majorName: child.name + }) + } + } + let changed = false + detailRows.value = detailRows.value.map(row => { + const meta = majorMetaMap.get(resolveRowMajorDictId(row)) + if (!meta) return row + if ( + row.groupCode === meta.groupCode && + row.groupName === meta.groupName && + row.majorCode === meta.majorCode && + row.majorName === meta.majorName + ) { + return row + } + changed = true + return { + ...row, + groupCode: meta.groupCode, + groupName: meta.groupName, + majorCode: meta.majorCode, + majorName: meta.majorName + } + }) + gridApi.value?.refreshCells({ force: true }) + if (!changed) return + syncComputedValuesToDetailRows() + await saveToIndexedDB({ skipComputedSync: true }) +} + const mergeProjectExpandedRow = (defaultRow: DetailRow, existingRow: DetailRow): DetailRow => ({ ...defaultRow, ...existingRow, @@ -1016,6 +1075,12 @@ usePricingPaneLifecycle({ }, saveToIndexedDB: () => saveToIndexedDB() }) +watch( + () => locale.value, + () => { + void relabelDetailRowsFromDict() + } +) const processCellForClipboard = (params: any) => { if (Array.isArray(params.value)) { return JSON.stringify(params.value); // 数组转字符串复制 @@ -1050,9 +1115,9 @@ const processCellFromClipboard = (params: any) => { - 投资规模明细 + {{ t('pricingPane.investment.title') }} - 项目数量 + {{ t('pricingPane.projectCount') }} { - 清空 + {{ t('common.clear') }} - 确认清空当前明细 + {{ t('pricingPane.clearTitle') }} - 将清空当前投资规模明细,是否继续? + {{ t('pricingPane.investment.clearDesc') }} - 取消 + {{ t('common.cancel') }} - 确认清空 + {{ t('pricingPane.confirmClear') }} @@ -1091,21 +1156,21 @@ const processCellFromClipboard = (params: any) => { - 使用默认数据 + {{ t('pricingPane.useDefault') }} - 确认覆盖当前明细 + {{ t('pricingPane.overrideTitle') }} - 将使用合同默认数据覆盖当前投资规模明细,是否继续? + {{ t('pricingPane.investment.overrideDesc') }} - 取消 + {{ t('common.cancel') }} - 确认覆盖 + {{ t('pricingPane.confirmOverride') }} @@ -1116,7 +1181,7 @@ const processCellFromClipboard = (params: any) => { -import { computed, nextTick, ref } from 'vue' +import { computed, nextTick, ref, watch } from 'vue' +import { useI18n } from 'vue-i18n' import { AgGridVue } from 'ag-grid-vue3' import type { ColDef, ColGroupDef, GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community' -import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql' +import { getIndustryDisplayName, getMajorDictEntries, getServiceDictItemById, isMajorIdInIndustryScope } from '@/sql' import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions' import { decimalAggSum, roundTo } from '@/lib/decimal' import { formatThousandsFlexible } from '@/lib/numberFormat' +import { withReadonlyAutoHeight } from '@/lib/agGridReadonlyAutoHeight' import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync' import { useZxFwPricingStore } from '@/pinia/zxFwPricing' import { useKvStore } from '@/pinia/kv' @@ -145,6 +147,7 @@ const props = defineProps<{ projectInfoKey?: string }>() const zxFwPricingStore = useZxFwPricingStore() +const { t, locale } = useI18n() const kvStore = useKvStore() const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`) const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) @@ -164,15 +167,9 @@ const paneInstanceCreatedAt = Date.now() const gridApi = ref | null>(null) const lastAppliedConsultFactorChangeAt = ref(0) const lastAppliedMajorFactorChangeAt = ref(0) -const industryNameMap = new Map( - industryTypeList.flatMap(item => [ - [String(item.id).trim(), item.name], - [String(item.type).trim(), item.name] - ]) -) const totalLabel = computed(() => { - const industryName = industryNameMap.get(activeIndustryCode.value.trim()) || '' - return industryName ? `${industryName}总投资` : '总投资' + const industryName = getIndustryDisplayName(activeIndustryCode.value.trim(), locale.value) + return industryName ? t('pricingScale.totalInvestmentByIndustry', { industryName }) : t('pricingScale.totalInvestment') }) const isMutipleService = computed(() => { @@ -258,13 +255,24 @@ const shouldSkipPersist = () => { } type majorLite = { code: string; name: string; defCoe: number | null; hasCost?: boolean; hasArea?: boolean } -const serviceEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, majorLite]) -const detailDict: ScaleDictGroup[] = buildScaleDetailDict( - serviceEntries, - ({ hasArea }) => hasArea -) - -const idLabelMap = buildScaleIdLabelMap(detailDict) +const serviceEntries: Array<[string, majorLite]> = [] +const detailDict: ScaleDictGroup[] = [] +const idLabelMap = new Map() +const rebuildScaleDictCaches = () => { + const nextServiceEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, majorLite]) + serviceEntries.splice(0, serviceEntries.length, ...nextServiceEntries) + const nextDetailDict = buildScaleDetailDict( + serviceEntries, + ({ hasArea }) => hasArea + ) + detailDict.splice(0, detailDict.length, ...nextDetailDict) + const nextIdLabelMap = buildScaleIdLabelMap(nextDetailDict) + idLabelMap.clear() + nextIdLabelMap.forEach((label, id) => { + idLabelMap.set(id, label) + }) +} +rebuildScaleDictCaches() const buildDefaultRows = (projectCountValue = getTargetProjectCount()): DetailRow[] => { return buildScaleRowsFromDict({ @@ -446,7 +454,7 @@ const formatEditableFlexibleNumber = (params: any) => formatScaleEditableConditionalNumber(params, { enabled: Boolean(params.data?.hasArea), precision: 3, - emptyText: '点击输入' + emptyText: t('pricingScale.clickToInput') }) const restoreLandAreaColumnDefaults = async () => { @@ -512,12 +520,12 @@ const restoreMajorFactorColumnDefaults = async () => { const columnDefs: Array | ColGroupDef> = [ createScaleValueColumn({ - headerName: '用地面积(亩)', + headerName: t('pricingScale.columns.landArea'), field: 'landArea', - headerTooltip: '点击右侧↻恢复本列默认用地面积', + headerTooltip: t('pricingScale.tooltip.resetLandArea'), headerComponent: AgGridResetHeader, onReset: restoreLandAreaColumnDefaults, - resetTitle: '恢复本列默认用地面积', + resetTitle: t('pricingScale.tooltip.resetLandArea'), minWidth: 90, flex: 2, isEditable: row => Boolean(row?.hasArea), @@ -542,6 +550,7 @@ const columnDefs: Array | ColGroupDef> = [ }), createScaleRemarkColumn() ] +const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs)) const autoGroupColumnDef: ColDef = createScaleAutoGroupColumn({ totalLabel: totalLabel.value, @@ -731,6 +740,56 @@ const applyDetailRows = (rows: DetailRow[]) => { detailRows.value = rows } +const relabelDetailRowsFromDict = async () => { + rebuildScaleDictCaches() + if (detailRows.value.length === 0) { + gridApi.value?.refreshCells({ force: true }) + return + } + const majorMetaMap = new Map>() + for (const group of detailDict) { + majorMetaMap.set(group.id, { + groupCode: group.code, + groupName: group.name, + majorCode: group.code, + majorName: group.name + }) + for (const child of group.children) { + majorMetaMap.set(child.id, { + groupCode: group.code, + groupName: group.name, + majorCode: child.code, + majorName: child.name + }) + } + } + let changed = false + detailRows.value = detailRows.value.map(row => { + const meta = majorMetaMap.get(resolveRowMajorDictId(row)) + if (!meta) return row + if ( + row.groupCode === meta.groupCode && + row.groupName === meta.groupName && + row.majorCode === meta.majorCode && + row.majorName === meta.majorName + ) { + return row + } + changed = true + return { + ...row, + groupCode: meta.groupCode, + groupName: meta.groupName, + majorCode: meta.majorCode, + majorName: meta.majorName + } + }) + gridApi.value?.refreshCells({ force: true }) + if (!changed) return + syncComputedValuesToDetailRows() + await saveToIndexedDB({ skipComputedSync: true }) +} + const mergeProjectExpandedRow = (defaultRow: DetailRow, existingRow: DetailRow): DetailRow => ({ ...defaultRow, ...existingRow, @@ -874,6 +933,12 @@ usePricingPaneLifecycle({ }, saveToIndexedDB: () => saveToIndexedDB() }) +watch( + () => locale.value, + () => { + void relabelDetailRowsFromDict() + } +) const processCellForClipboard = (params: any) => { if (Array.isArray(params.value)) { return JSON.stringify(params.value); // 数组转字符串复制 @@ -908,9 +973,9 @@ const processCellFromClipboard = (params: any) => { - 用地规模明细 + {{ t('pricingPane.land.title') }} - 项目数量 + {{ t('pricingPane.projectCount') }} { - 清空 + {{ t('common.clear') }} - 确认清空当前明细 + {{ t('pricingPane.clearTitle') }} - 将清空当前用地规模明细,是否继续? + {{ t('pricingPane.land.clearDesc') }} - 取消 + {{ t('common.cancel') }} - 确认清空 + {{ t('pricingPane.confirmClear') }} @@ -950,22 +1015,22 @@ const processCellFromClipboard = (params: any) => { - 使用默认数据 + {{ t('pricingPane.useDefault') }} - 确认覆盖当前明细 + {{ t('pricingPane.overrideTitle') }} - 将使用合同默认数据覆盖当前用地规模明细,是否继续? + {{ t('pricingPane.land.overrideDesc') }} - 取消 + {{ t('common.cancel') }} - 确认覆盖 + {{ t('pricingPane.confirmOverride') }} @@ -976,7 +1041,7 @@ const processCellFromClipboard = (params: any) => { -import { computed, ref } from 'vue' +import { computed, ref, watch } from 'vue' +import { useI18n } from 'vue-i18n' import { AgGridVue } from 'ag-grid-vue3' import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' import { taskList } from '@/sql' @@ -7,6 +8,7 @@ import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgG import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal' import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat' import { parseNumberOrNull } from '@/lib/number' +import { withReadonlyAutoHeight } from '@/lib/agGridReadonlyAutoHeight' import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync' import { useZxFwPricingStore } from '@/pinia/zxFwPricing' import { useKvStore } from '@/pinia/kv' @@ -47,6 +49,7 @@ const props = defineProps<{ serviceId: string | number }>() const zxFwPricingStore = useZxFwPricingStore() +const { t, locale } = useI18n() const kvStore = useKvStore() const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`) const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`) @@ -127,6 +130,13 @@ type taskLite = { desc: string | null } +const getTaskDisplayName = (task: taskLite | undefined) => { + if (!task) return '' + return String(locale.value).toLowerCase().startsWith('en') + ? (task as taskLite & { nameEn?: string }).nameEn || task.name + : task.name +} + const formatTaskReferenceUnitPrice = (task: taskLite) => { const unit = task.unit || '' const hasMin = typeof task.minPrice === 'number' && Number.isFinite(task.minPrice) @@ -160,7 +170,7 @@ const buildDefaultRows = (): DetailRow[] => { rows.push({ id: rowId, taskCode, - taskName: task.name, + taskName: getTaskDisplayName(task), unit: task.unit || '', conversion: typeof task.conversion === 'number' && Number.isFinite(task.conversion) ? task.conversion : null, workload: null, @@ -250,9 +260,9 @@ const calcServiceFee = (row: DetailRow | undefined) => { } const formatEditableNumber = (params: any) => { - if (isNoTaskRow(params.data)) return '无' + if (isNoTaskRow(params.data)) return t('workloadPricing.none') if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { - return '点击输入' + return t('workloadPricing.clickToInput') } if (params.value == null) return '' return formatThousandsFlexible(params.value, 3) @@ -269,16 +279,16 @@ const spanRowsByTaskName = (params: any) => { const columnDefs: ColDef[] = [ { - headerName: '编码', + headerName: t('workloadPricing.columns.code'), field: 'taskCode', minWidth: 100, width: 120, pinned: 'left', colSpan: params => (params.node?.rowPinned ? 2 : 1), - valueFormatter: params => (params.node?.rowPinned ? '总合计' : params.value || '') + valueFormatter: params => (params.node?.rowPinned ? t('workloadPricing.total') : params.value || '') }, { - headerName: '名称', + headerName: t('workloadPricing.columns.name'), field: 'taskName', minWidth: 150, width: 220, @@ -291,7 +301,7 @@ const columnDefs: ColDef[] = [ valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '') }, { - headerName: '预算基数', + headerName: t('workloadPricing.columns.budgetBase'), field: 'budgetBase', minWidth: 150, autoHeight: true, @@ -302,14 +312,14 @@ const columnDefs: ColDef[] = [ valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '') }, { - headerName: '预算参考单价', + headerName: t('workloadPricing.columns.budgetReferenceUnitPrice'), field: 'budgetReferenceUnitPrice', minWidth: 170, flex: 1, valueFormatter: params => params.value || '' }, { - headerName: '预算采用单价', + headerName: t('workloadPricing.columns.budgetAdoptedUnitPrice'), field: 'budgetAdoptedUnitPrice', headerClass: 'ag-right-aligned-header', minWidth: 170, @@ -326,9 +336,9 @@ const columnDefs: ColDef[] = [ }, valueParser: params => parseSanitizedAdoptedPriceOrNull(params.newValue), valueFormatter: params => { - if (isNoTaskRow(params.data)) return '无' + if (isNoTaskRow(params.data)) return t('workloadPricing.none') if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { - return '点击输入' + return t('workloadPricing.clickToInput') } if (params.value == null) return '' const unit = params.data?.unit || '' @@ -336,7 +346,7 @@ const columnDefs: ColDef[] = [ } }, { - headerName: '工作量', + headerName: t('workloadPricing.columns.workload'), field: 'workload', minWidth: 140, flex: 1, @@ -356,7 +366,7 @@ const columnDefs: ColDef[] = [ valueFormatter: formatEditableNumber }, { - headerName: '咨询分类系数', + headerName: t('workloadPricing.columns.consultCategoryFactor'), field: 'consultCategoryFactor', width: 80, minWidth: 70, @@ -376,7 +386,7 @@ const columnDefs: ColDef[] = [ valueFormatter: formatEditableNumber }, { - headerName: '服务费用(元)', + headerName: t('workloadPricing.columns.serviceFee'), field: 'serviceFee', headerClass: 'ag-right-aligned-header', minWidth: 150, @@ -388,13 +398,13 @@ const columnDefs: ColDef[] = [ valueGetter: params => (params.node?.rowPinned ? params.data?.serviceFee ?? null : calcServiceFee(params.data)), aggFunc: decimalAggSum, valueFormatter: params => { - if (isNoTaskRow(params.data)) return '无' + if (isNoTaskRow(params.data)) return t('workloadPricing.none') if (params.value == null || params.value === '') return '' return formatThousandsFlexible(roundTo(params.value, 3), 3) } }, { - headerName: '说明', + headerName: t('workloadPricing.columns.remark'), field: 'remark', minWidth: 180, flex: 1.2, @@ -418,6 +428,7 @@ const columnDefs: ColDef[] = [ } } ] +const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs)) const totalWorkload = computed(() => sumByNumber(detailRows.value, row => row.workload)) const totalBasicFee = computed(() => sumByNumber(detailRows.value, row => calcBasicFee(row))) @@ -425,7 +436,7 @@ const totalServiceFee = computed(() => sumNullableBy(detailRows.value, row => ca const pinnedTopRowData = computed(() => createPinnedTopRowData({ id: 'pinned-total-row', - taskCode: '总合计', + taskCode: t('workloadPricing.total'), taskName: '', unit: '', conversion: null, @@ -533,6 +544,41 @@ const loadFromIndexedDB = async () => { } } +const relabelRowsFromTaskDict = async () => { + if (!isWorkloadMethodApplicable.value || detailRows.value.length === 0) return + let changed = false + detailRows.value = detailRows.value.map(row => { + if (isNoTaskRow(row)) return row + const match = String(row.id || '').match(/^task-(\d+)-\d+$/) + if (!match) return row + const task = (taskList as Record)[match[1]] + if (!task) return row + const nextTaskName = getTaskDisplayName(task) + const nextUnit = task.unit || '' + const nextBudgetBase = task.basicParam || '' + const nextBudgetReferenceUnitPrice = formatTaskReferenceUnitPrice(task) + if ( + row.taskName === nextTaskName && + row.unit === nextUnit && + row.budgetBase === nextBudgetBase && + row.budgetReferenceUnitPrice === nextBudgetReferenceUnitPrice + ) { + return row + } + changed = true + return { + ...row, + taskName: nextTaskName, + unit: nextUnit, + budgetBase: nextBudgetBase, + budgetReferenceUnitPrice: nextBudgetReferenceUnitPrice + } + }) + gridApi.value?.refreshCells({ force: true }) + if (!changed) return + await saveToIndexedDB() +} + let isBulkClipboardMutation = false const commitGridChanges = () => { @@ -564,6 +610,12 @@ usePricingPaneLifecycle({ linkedSourceSignature: linkedConsultFactorSignature, saveToIndexedDB }) +watch( + () => locale.value, + () => { + void relabelRowsFromTaskDict() + } +) const processCellForClipboard = (params: any) => { if (Array.isArray(params.value)) { return JSON.stringify(params.value); // 数组转字符串复制 @@ -608,13 +660,13 @@ const mydiyTheme = myTheme.withParams({ - 工作量明细 + {{ t('workloadPricing.title') }} diff --git a/src/features/shared/components/HourlyFeeGrid.vue b/src/features/shared/components/HourlyFeeGrid.vue index baf3ed7..8e3744d 100644 --- a/src/features/shared/components/HourlyFeeGrid.vue +++ b/src/features/shared/components/HourlyFeeGrid.vue @@ -1,5 +1,6 @@ @@ -15,7 +17,7 @@ const props = withDefaults( class="flex h-full min-h-0 w-full flex-1 items-center justify-center bg-[radial-gradient(circle_at_top,_rgba(220,38,38,0.18),rgba(0,0,0,0.03)_46%,transparent_72%)] p-6" > - {{ props.title }} + {{ props.title || t('methodUnavailable.defaultTitle') }} {{ props.message }} diff --git a/src/features/shared/components/ServiceCheckboxSelector.vue b/src/features/shared/components/ServiceCheckboxSelector.vue index 4541a33..b1f054a 100644 --- a/src/features/shared/components/ServiceCheckboxSelector.vue +++ b/src/features/shared/components/ServiceCheckboxSelector.vue @@ -1,5 +1,6 @@ - diff --git a/src/features/xm/components/XmMajorFactor.vue b/src/features/xm/components/XmMajorFactor.vue index ad9e78f..c33440b 100644 --- a/src/features/xm/components/XmMajorFactor.vue +++ b/src/features/xm/components/XmMajorFactor.vue @@ -1,5 +1,6 @@ diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 9feafbb..417e312 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -3,7 +3,8 @@ export const enUS = { cancel: 'Cancel', confirm: 'Confirm', delete: 'Delete', - close: 'Close' + close: 'Close', + clear: 'Clear' }, app: { projectConflict: { @@ -19,6 +20,7 @@ export const enUS = { home: { title: 'Calculation Entry', subtitle: 'Project Budget · Quick Calc · Import Data', + projectCalcTab: 'Project Calculation', cards: { heroTitle: 'One-Click Smart Budget', heroSubTitle: 'Accelerate standards adoption', @@ -26,7 +28,7 @@ export const enUS = { projectBudget: 'Project Budget', projectBudgetDesc: 'For full project-level calculation across multiple contracts with import/export support', quickCalc: 'Quick Calc', - quickCalcDesc: 'Pick industry and consulting type, input scale values, and get results instantly', + quickCalcDesc: 'Suitable for single service trial, select industry, consultation type, engineering specialty, input base number and get results in seconds', importData: 'Import Data', importDataDesc: 'Import ".zw" package to restore project state and continue work quickly', enter: 'Enter', @@ -92,13 +94,553 @@ export const enUS = { later: 'Later', prev: 'Prev', next: 'Next', - finish: 'Finish and Disable Auto Popup' + finish: 'Finish and Disable Auto Popup', + jumpToStep: 'Jump to step {index}', + steps: { + step1: { + title: 'Welcome', + description: 'This guide covers major features and the full workflow. It is recommended to go through it in order.', + point1: 'The top area is the tab bar, for quick switching between project, segment, and pricing pages.', + point2: 'Tables and forms on the page auto-save locally. No manual save is needed.', + point3: 'You can reopen this tutorial anytime from the top-right "Guide" button.' + }, + step2: { + title: 'Project Card and Four Modules', + description: 'The default "Project Card" tab is the entry. The left flow line contains four project-level modules.', + point1: 'Basic Info: fill project name and project scale details.', + point2: 'Contract Segment Management: create, sort, search, import/export segments.', + point3: 'Consult Category Factor / Major Factor: maintain budget values and remarks.' + }, + step3: { + title: 'Fill Basic Info', + description: 'Complete project-level data in "Basic Info" first, then continue to segment-level calculations.', + point1: 'Project name is used in export file names and page display.', + point2: 'Project detail table supports direct edit, copy/paste, and undo/redo.', + point3: 'Group rows auto-summarize, and the pinned top row shows grand total.' + }, + step4: { + title: 'Contract Segment Management', + description: 'Manage the full lifecycle of contract segments in this module.', + point1: '"Add Segment" creates a new one; top-right actions on each card support edit/delete.', + point2: 'Search and grid/list switch are supported; drag-sort is available when not searching.', + point3: 'The more menu supports import/export; click a card to enter segment details.' + }, + step5: { + title: 'Contract Segment Detail', + description: 'Inside a segment, the left flow line includes scale info and consulting services.', + point1: 'Scale Info: fill segment scale data by engineering major.', + point2: 'Consulting Services: choose services from dictionary and generate fee details.', + point3: 'Each segment page has isolated cache and does not interfere with others.' + }, + step6: { + title: 'Service and Pricing Pages', + description: 'The consulting service page manages service details and opens specific pricing method pages.', + point1: 'Click "Browse" to select services, then confirm to generate detail rows.', + point2: 'In detail table, "Edit" opens service pricing page, and "Clear" resets that service calculation.', + point3: 'Pricing page includes investment scale, land scale, workload, and hourly methods.' + }, + step7: { + title: 'Factor Maintenance', + description: 'Project-level factors adjust budget values and can be maintained on two factor pages.', + point1: 'Consult Category Factor page: maintain budget values and notes by consult category.', + point2: 'Major Factor page: maintain budget values and notes by major tree.', + point3: 'Batch paste and undo/redo are supported for efficient multi-row updates.' + }, + step8: { + title: 'Data Management and Recovery', + description: 'Top toolbar handles full import/export and reset initialization.', + point1: '"Import/Export" operates on full-project data package.', + point2: '"Reset" clears all local data and restores default page.', + point3: 'It is recommended to export a backup before major changes.' + } + } }, toast: { export: 'Export Report', success: 'Export Success', failed: 'Export Failed' + }, + messages: { + defaultProjectLabel: 'Default Project', + defaultProjectName: 'Cost Project', + projectNamePrefix: 'Project-{id}', + contractFallbackName: 'Contract-{index}', + reportFileSuffix: 'Budget Report', + reportGenerating: 'Generating report file...', + reportExportDone: 'Report export completed', + reportExportFailedRetry: 'Report export failed, please retry', + importFailedTitle: 'Import Failed', + importProjectIdMissing: 'This package does not contain project ID (legacy export). Import is blocked to avoid cross-project overwrite.', + importProjectMismatch: 'This package belongs to another project and cannot override current project.', + importInvalidFile: 'File is invalid, corrupted, or modified.', + importWriteError: 'An error occurred while writing local data.', + openFile: 'Open file' + } + }, + typeLine: { + copy: 'Copy', + copied: 'Copied', + copyFailed: 'Copy failed', + brandAlt: 'Zhongwei', + supportText: 'This website is supported by Zhongwei Engineering Consulting Co., Ltd.', + aboutTitle: 'About Us', + companyName: 'Zhongwei Engineering Consulting Co., Ltd.', + openOfficialSiteAria: 'Open official website', + officialSiteTitle: 'Official Website', + aboutParagraph1: 'Zhongwei Engineering Consulting Co., Ltd. was founded in 2009, focusing on whole-process consulting for project cost and cost control. It is a preferred audit vendor for Guangdong government. The company serves multi-domain and diverse clients, with cumulative project investment over one trillion CNY, deep participation in major national projects such as the Hong Kong-Zhuhai-Macao Bridge and Hengqin Campus of the University of Macau, and participation in over 30 national/provincial/municipal standards.', + aboutParagraph2: 'Based in the Greater Bay Area and expanding globally, the company has offices in Macau and Sri Lanka, with cross-border and overseas delivery capabilities. With 15 years of expertise and trillion-level project experience, it provides precise and reliable engineering consulting services.' + }, + agGrid: { + resetDefault: 'Reset to default' + }, + ht: { + title: 'Contract Segments', + projectTotalBudget: 'Project Total Budget: {amount}', + budgetLoading: 'Calculating...', + selectedCount: '{count} selected', + exportSelected: 'Export Selected', + deleteSelected: 'Delete Selected', + cancelSelect: 'Cancel', + addContract: 'Add Segment', + batchDelete: 'Batch Delete', + exportContracts: 'Export Segments', + importContracts: 'Import Segments', + searchPlaceholder: 'Search by segment name or ID', + clearFilter: 'Clear Filter', + searchingHint: 'Searching ({filtered} / {total}), drag sorting is disabled', + selectModeExportHint: 'Export mode: select segments and click "Export Selected"', + selectModeDeleteHint: 'Delete mode: select segments and click "Delete Selected"', + setupRequiredHint: 'Set project industry in "Basic Info" before adding or importing segments', + listLayout: 'List', + gridLayout: 'Grid', + dragSort: 'Drag to Sort', + dragSortSearchOff: 'Drag Sort (Disabled in Search)', + edit: 'Edit', + remove: 'Delete', + idLabel: 'ID: {id}', + contractBudget: 'Budget: {amount}', + contractBudgetLine: 'Segment Budget: {amount}', + createdAt: 'Created: {time}', + emptyTitle: 'No Contract Segments', + emptyDesc: 'Add one to get started', + notFound: 'No matching contract segment', + backToTop: 'Back to Top', + editContract: 'Edit Segment', + createContract: 'New Segment', + contractTabTitle: 'Segment {name}', + contractName: 'Segment Name', + contractNamePlaceholder: 'Enter segment name', + save: 'Save', + ok: 'OK', + toastSuccessTitle: 'Success', + createSuccess: 'Created successfully', + editSuccess: 'Updated successfully', + deleteSuccess: 'Deleted successfully', + sortDone: 'Sort completed', + exportSuccess: 'Exported successfully ({count} segments)', + importSuccess: 'Imported successfully ({count} segments)', + deleteBatchSuccess: 'Deleted successfully ({count} segments)', + tipTitle: 'Notice', + exportFailedTitle: 'Export Failed', + importFailedTitle: 'Import Failed', + batchDeleteFailedTitle: 'Batch Delete Failed', + retry: 'Please try again.', + selectAtLeastOne: 'Please select at least one contract segment.', + noContractsToDelete: 'No contract segment found to delete.', + industryMissingForExport: 'Project industry is missing. Please set it in "Basic Info" first.', + importIndustryMismatch: 'Industry mismatch (package: {importIndustry}, current: {currentIndustry}).', + importCurrentIndustryMissing: 'Current project industry is not set. Please set it in "Basic Info" first.', + importPackageIndustryMissing: 'Import package missing industry info. Re-export with latest version and try again.', + importFileInvalid: 'Invalid or corrupted file, or not a contract-segment package.', + deleteSingleTitle: 'Confirm Delete Segment', + deleteSingleDesc: 'Delete "{name}" and all related service/pricing data. Continue?', + deleteBatchTitle: 'Confirm Batch Delete', + deleteBatchDesc: 'Delete {count} segments and related service/pricing data. Continue?' + }, + htCard: { + title: 'Segment: {name}', + subtitle: 'Segment ID: {id}', + metaBudget: 'Segment Budget: {amount}', + currencySuffix: 'CNY', + categories: { + baseInfo: 'Basic Info', + scaleInfo: 'Scale Info', + services: 'Consulting Services', + consultFactor: 'Consult Category Factor', + majorFactor: 'Major Factor', + additionalFee: 'Additional Fee', + reserveFee: 'Reserve Fee', + summary: 'Summary' + } + }, + htBaseInfo: { + title: 'Basic Info', + defaultQuality: 'The comprehensive evaluation of cost consulting services should reach "Good" or a score of 90.', + qualityLabel: 'Quality Requirement', + qualityPlaceholder: 'Enter quality requirement', + durationLabel: 'Duration Requirement', + durationPlaceholder: 'Enter duration requirement' + }, + htFactors: { + consultCategoryTitle: 'Consult Category Factor Details', + majorTitle: 'Major Factor Details' + }, + htFee: { + additionalTitle: 'Additional Work Fee', + reserveTitle: 'Reserve Fee' + }, + htInfo: { + scaleDetailTitle: 'Contract Scale Details' + }, + htFeeRate: { + baseLabel: 'Base (total budget of all service fees)', + reserveBaseLabel: 'Base (consulting services total + additional work fee total)', + rateLabel: 'Rate (%)', + ratePlaceholder: 'Enter rate, suggested 1 ~ 5', + budgetFeeLabel: 'Budget Fee (Auto)', + remarkLabel: 'Remark', + remarkPlaceholder: 'Enter remark' + }, + htZxFw: { + title: 'Consulting Service Details', + warning: 'Please review and adjust recommended limits/special values in the specification, then update final fee if needed.', + editTabTitle: 'Service Edit-{name}', + subtotal: 'Subtotal', + edit: 'Edit', + resetDefault: 'Reset', + delete: 'Remove', + processDraft: 'Draft', + processReview: 'Review', + columns: { + code: 'Code', + name: 'Name', + process: 'Process', + investScale: 'Investment Scale', + landScale: 'Land Scale', + workload: 'Workload', + hourly: 'Hourly', + subtotal: 'Subtotal', + finalFee: 'Final Fee ✎', + finalFeeTooltip: 'This column supports manual edits and will auto-sync to the fixed subtotal row.', + actions: 'Actions' + }, + dialog: { + resetTitle: 'Confirm Reset to Default', + resetDesc: 'This will recalculate default data from latest scale/factor values and overwrite current data for "{name}". Continue?', + confirmReset: 'Confirm Reset', + deleteTitle: 'Confirm Delete Service', + deleteDesc: 'This will logically remove "{name}". Existing entered data is kept and will restore if re-selected. Continue?' + } + }, + htSummary: { + title: 'Contract Summary', + total: 'Total', + remark: 'Remark', + placeholder: 'Fill consulting services / additional work fee / reserve fee first', + additionalPrefix: 'Additional Work Fee', + reservePrefix: 'Reserve Fee', + explainByRate: 'By rate {rate}%, calculated {fee} CNY', + explainByHourly: 'By hourly method, calculated {fee} CNY', + explainByQuantity: 'By quantity-unit-price method, calculated {fee} CNY', + columns: { + code: 'Code', + name: 'Name', + investScale: 'Investment Scale', + landScale: 'Land Scale', + workload: 'Workload', + hourly: 'Hourly', + subtotal: 'Subtotal', + finalFee: 'Final Fee' + } + }, + htFeeGrid: { + subtotal: 'Subtotal', + currentRow: 'Current Row', + unnamed: 'Unnamed', + edit: 'Edit', + clear: 'Clear', + add: 'Add', + editTabTitle: 'Fee Edit-{name}', + columns: { + name: 'Name', + rateFee: 'Rate Fee', + hourlyFee: 'Hourly', + quantityUnitPriceFee: 'Quantity Unit Price', + subtotal: 'Subtotal', + actions: 'Actions' + }, + dialog: { + clearTitle: 'Confirm Clear', + clearDesc: 'This will clear editable and auto-calculated data for "{name}" and its edit page. Continue?', + confirmClear: 'Confirm Clear' + } + }, + xmFactorGrid: { + clickToInput: 'Click to input', + columns: { + standardFactor: 'Standard Factor', + budgetValue: 'Budget Value', + remark: 'Remark', + groupName: 'Major Code and Major Name' + } + }, + serviceSelector: { + title: 'Select Services', + clear: 'Clear', + empty: 'No services' + }, + zxFwView: { + contractPrefix: 'Contract: {name}', + calcSuffix: ' Calculation', + contractId: 'Contract ID: {id}', + workContentTitle: 'Work Content', + categories: { + investmentScale: 'Investment Scale', + landScale: 'Land Scale', + workload: 'Workload', + hourly: 'Hourly', + workContent: 'Work Content' + }, + unavailable: { + investmentScaleTitle: 'Investment Scale Not Applicable', + investmentScaleMessage: 'Scale method is not enabled for this service, so Investment Scale is not editable.', + landScaleTitle: 'Land Scale Not Applicable', + landScaleMessage: 'This service only supports Investment Scale, so Land Scale is not editable.', + workloadTitle: 'Workload Not Applicable', + workloadMessage: 'Workload method is not enabled for this service, so Workload is not editable.', + hourlyTitle: 'Hourly Not Applicable', + hourlyMessage: 'Hourly method is not enabled for this service, so Hourly is not editable.' + } + }, + htFeeDetail: { + subtotal: 'Subtotal', + currentRow: 'Current Row', + clickToInput: 'Click to input', + addRow: 'Add Row', + columns: { + no: 'No.', + feeItem: 'Fee Item', + unit: 'Unit', + quantity: 'Quantity', + unitPrice: 'Unit Price (CNY)', + budgetFee: 'Budget Fee (CNY)', + remark: 'Remark', + actions: 'Actions' + }, + dialog: { + deleteTitle: 'Confirm Delete Row', + deleteDesc: 'Delete row "{name}"?' + } + }, + workContent: { + title: 'Work Content', + addCustom: 'Add Custom Content', + clickToInput: 'Click to input', + clickToInputContent: 'Click to input work content', + currentRow: 'Current Row', + unnamed: 'Unnamed', + ungrouped: 'Ungrouped', + type: { + basic: 'Basic Work', + optional: 'Optional Work', + daily: 'Daily Advisory', + special: 'Special Advisory', + additional: 'Additional Work', + custom: 'Custom' + }, + columns: { + no: 'No.', + content: 'Content', + type: 'Type', + remark: 'Remark', + actions: 'Actions' + }, + dialog: { + deleteTitle: 'Confirm Delete Row', + deleteDesc: 'Delete row "{name}"?' + } + }, + quickCalc: { + projectName: 'Quick Calculation', + catalogEyebrow: 'Category List', + catalogTitle: 'Quick Calc Options', + formEyebrow: 'Parameter Form', + formTitle: 'Calculation Parameters', + industryLabel: 'Industry {name}', + selectIndustry: 'Select industry', + saving: 'Saving...', + synced: 'Industry synced', + notSelectedIndustry: 'Industry not selected', + notSelected: 'Not selected', + consultCategory: 'Consult Category', + majorCategory: 'Major', + fields: { + industry: 'Industry', + code: 'Code', + investScale: 'Investment Scale (10k CNY)', + landScale: 'Land Scale (mu)', + formula: 'Formula', + amount: 'Amount (CNY)', + consultFactor: 'Consult Category Factor', + majorFactor: 'Major Factor', + workEnvFactor: 'Work Environment Factor', + workEnvFactorPlaceholder: 'Default 1', + budgetAmount: 'Budget Amount (CNY)' + }, + sections: { + currentSelection: 'Current Selection', + basicInfo: 'Basic Info', + scaleBase: 'Scale Base', + scaleParams: 'Scale Parameters', + benchmarkBudget: 'Benchmark Budget', + budgetBase: 'Budget Base', + serviceBudget: 'Service Budget', + factorsAndResult: 'Factors and Result' + }, + empty: { + selectIndustry: 'Select an industry first. Then choose consult category and matched majors will appear.', + selectConsult: 'Select a consult category first. Matched general and major categories will then appear.', + scaleUnavailable: 'The selected consult category does not support scale method, so major categories are hidden.', + consultCostOnly: 'The selected consult category is priced by industry summary. Major factor is auto-applied by industry.' + }, + placeholder: { + selectConsultFirst: 'Select consult category first', + scaleUnavailable: 'Current category does not support scale method', + selectMajorFirst: 'Select major first', + preferLandScale: 'Current major is priced by land scale', + investUnavailable: 'Current major does not support investment scale', + consultCostOnly: 'Current category supports investment scale only', + landUnavailable: 'Current major does not support land scale', + input: 'Please input', + selectScaleFirst: 'Select and input a scale value first' + } + }, + methodUnavailable: { + defaultTitle: 'This Service Is Not Applicable to Current Pricing Method' + }, + xmCard: { + categories: { + info: 'Basic Info', + scaleInfo: 'Scale Info', + consultCategoryFactor: 'Consult Category Factor', + majorFactor: 'Major Factor', + contract: 'Contract Segment Management' + } + }, + htFeeMethodTypeLine: { + feeDetail: 'Fee Details', + unnamed: 'Unnamed', + title: 'Segment: {contractName} · {rowName}', + contractId: 'Contract ID: {id}', + quantityUnitPrice: 'Quantity Unit Price' + }, + pricingScale: { + totalInvestmentByIndustry: '{industryName} Total Investment', + totalInvestment: 'Total Investment', + clickToInput: 'Click to input', + projectLabel: 'Project {index}', + columns: { + investAmount: 'Cost Amount (10k CNY)', + landArea: 'Land Area (mu)', + benchmarkBudget: 'Benchmark Budget (CNY)', + basicWork: 'Basic Work', + optionalWork: 'Optional Work', + subtotal: 'Subtotal', + budgetFee: 'Budget Fee', + consultCategoryFactor: 'Consult Category Factor', + majorFactor: 'Major Factor', + workStageFactor: 'Work Stage Factor (Draft/Review)', + workRatio: 'Work Ratio (%)', + total: 'Total', + remark: 'Remark', + majorGroup: 'Major Code and Major Name' + }, + tooltip: { + resetInvestAmount: 'Click ↻ to restore default cost amount for this column', + resetLandArea: 'Click ↻ to restore default land area for this column', + resetConsultCategoryFactor: 'Click ↻ to restore default consult category factor for this column', + resetMajorFactor: 'Click ↻ to restore default major factor for this column' + } + }, + pricingPane: { + projectCount: 'Project Count', + clearTitle: 'Confirm Clear Current Details', + confirmClear: 'Confirm Clear', + useDefault: 'Use Default Data', + overrideTitle: 'Confirm Override Current Details', + confirmOverride: 'Confirm Override', + investment: { + title: 'Investment Scale Details', + clearDesc: 'This will clear current investment scale details. Continue?', + overrideDesc: 'Use contract default data to override current investment scale details. Continue?' + }, + land: { + title: 'Land Scale Details', + clearDesc: 'This will clear current land scale details. Continue?', + overrideDesc: 'Use contract default data to override current land scale details. Continue?' + } + }, + workloadPricing: { + title: 'Workload Details', + unavailableTitle: 'Workload Method Not Applicable', + unavailableMessage: 'No workload tasks are associated with this service. No input is needed.', + clickToInput: 'Click to input', + none: 'N/A', + total: 'Grand Total', + columns: { + code: 'Code', + name: 'Name', + budgetBase: 'Budget Base', + budgetReferenceUnitPrice: 'Budget Reference Unit Price', + budgetAdoptedUnitPrice: 'Budget Adopted Unit Price', + workload: 'Workload', + consultCategoryFactor: 'Consult Category Factor', + serviceFee: 'Service Fee (CNY)', + remark: 'Remark' + } + }, + hourlyFeeGrid: { + title: 'Hourly Method Details', + clickToInput: 'Click to input', + total: 'Grand Total', + columns: { + code: 'Code', + name: 'Personnel Name', + referenceUnitPrice: 'Budget Reference Unit Price', + laborBudgetUnitPrice: 'Labor Budget Unit Price (CNY/workday)', + compositeBudgetUnitPrice: 'Composite Budget Unit Price (CNY/workday)', + adoptedBudgetUnitPrice: 'Adopted Budget Unit Price (CNY/workday)', + personnelCount: 'Personnel Count', + workdayCount: 'Workday Count', + serviceBudget: 'Service Budget (CNY)', + remark: 'Remark' + } + }, + xmScaleGrid: { + syncToastTitle: 'Consulting Services Synced', + syncToastDesc: 'Scale info synced to consulting services ({serviceCount} services, {methodCount} pricing pages, {rowCount} rows)' + }, + xmInfo: { + defaultProjectName: 'xxx Cost Consulting Service', + defaultDesc: 'When providing cost consulting services, penalties should be graded by service quality. For scores >=85 and <90, penalty is 10% of budget fee; >=80 and <85: 20%; >=75 and <80: 30%; >=70 and <75: 40%; <70: 50% or above.', + industryHint: 'Changing industry requires reset and re-selection', + industryHintAria: 'Industry hint', + createFromHomeFirst: 'Please create a project from Home before entering this page.', + fields: { + projectName: 'Project Name', + projectIndustry: 'Industry', + overview: 'Project Overview', + preparedBy: 'Prepared By', + reviewedBy: 'Reviewed By', + preparedCompany: 'Prepared Company', + preparedDate: 'Prepared Date', + desc: 'Other Notes' + }, + placeholders: { + overview: 'Enter project overview', + preparedBy: 'Enter preparer', + reviewedBy: 'Enter reviewer', + preparedCompany: 'Enter prepared company' } } } as const - diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 082cb22..820bebc 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -3,7 +3,8 @@ export const zhCN = { cancel: '取消', confirm: '确认', delete: '删除', - close: '关闭' + close: '关闭', + clear: '清空' }, app: { projectConflict: { @@ -19,6 +20,7 @@ export const zhCN = { home: { title: '计算入口', subtitle: '项目计算 · 单项速算 · 导入数据', + projectCalcTab: '项目计算', cards: { heroTitle: '智能预算一键生成', heroSubTitle: '助力《规范》高效落地', @@ -26,7 +28,7 @@ export const zhCN = { projectBudget: '项目预算', projectBudgetDesc: '适用于多合同段、项目级整体计算,支持导出/导入完整项目数据', quickCalc: '单项速算', - quickCalcDesc: '单项速算,选择行业与咨询类型,输入基数秒出结果', + quickCalcDesc: '适用于单项服务试算,选择行业、咨询类型、工程专业,输入基数秒出结果', importData: '导入数据', importDataDesc: '导入".zw"数据包,快速恢复项目计算状态,续未完成工作', enter: '进入计算', @@ -92,13 +94,553 @@ export const zhCN = { later: '稍后再看', prev: '上一步', next: '下一步', - finish: '完成并不再自动弹出' + finish: '完成并不再自动弹出', + jumpToStep: '跳转到第 {index} 步', + steps: { + step1: { + title: '欢迎使用', + description: '这个引导会覆盖系统主要功能和完整使用路径,建议按顺序走一遍。', + point1: '顶部是标签页区,可在项目、合同段、服务计算页之间快速切换。', + point2: '页面里的表格与表单会自动保存到本地,无需手动点击保存。', + point3: '你可以随时点击右上角“使用引导”重新打开本教程。' + }, + step2: { + title: '项目卡片与四个模块', + description: '默认标签“项目卡片”是入口,左侧流程线包含四个项目级功能。', + point1: '基础信息:填写项目名称与项目规模明细。', + point2: '合同段管理:新建、排序、搜索、导入/导出合同段。', + point3: '咨询分类系数 / 工程专业系数:维护系数预算取值和备注。' + }, + step3: { + title: '基础信息填写', + description: '先在“基础信息”中补齐项目级数据,再进入后续合同段计算。', + point1: '项目名称会用于导出文件名和页面展示。', + point2: '项目明细表支持直接编辑、复制粘贴、撤销重做。', + point3: '分组行自动汇总,顶部固定行显示总合计。' + }, + step4: { + title: '合同段管理', + description: '在“合同段管理”中完成合同段生命周期操作。', + point1: '“添加合同段”用于新增,卡片右上角可编辑或删除。', + point2: '支持搜索、网格/列表切换,非搜索状态可拖拽排序。', + point3: '更多菜单可导入/导出合同段;点击卡片进入该合同段详情。' + }, + step5: { + title: '合同段详情', + description: '进入合同段后,左侧同样是流程线,包含规模信息和咨询服务两部分。', + point1: '规模信息:按工程专业填写当前合同段的规模数据。', + point2: '咨询服务:选择服务词典并生成服务费用明细。', + point3: '合同段页面会独立缓存,不同合同段互不干扰。' + }, + step6: { + title: '咨询服务与计算页', + description: '咨询服务页面用于管理服务明细,并进入具体计费方法页面。', + point1: '先点击“浏览”选择服务,再确认生成明细行。', + point2: '明细表“编辑”可打开服务计算页,“清空”会重置该服务计算结果。', + point3: '服务计算页包含投资规模法、用地规模法、工作量法、工时法四种方法。' + }, + step7: { + title: '系数维护', + description: '项目级系数用于调节预算取值,可在两个系数页分别维护。', + point1: '咨询分类系数页:按咨询分类维护预算取值与说明。', + point2: '工程专业系数页:按专业树维护预算取值与说明。', + point3: '支持批量粘贴、撤销重做,便于一次性维护多行数据。' + }, + step8: { + title: '数据管理与恢复', + description: '顶部工具栏负责全量数据导入导出与初始化重置。', + point1: '“导入/导出”是整项目级别的数据包操作。', + point2: '“重置”会清空本地全部数据并恢复默认页面。', + point3: '建议在重要调整前先导出备份。' + } + } }, toast: { export: '导出报表', success: '导出成功', failed: '导出失败' + }, + messages: { + defaultProjectLabel: '默认项目', + defaultProjectName: '造价项目', + projectNamePrefix: '项目-{id}', + contractFallbackName: '合同段-{index}', + reportFileSuffix: '预算文件', + reportGenerating: '正在生成报表文件...', + reportExportDone: '报表导出完成', + reportExportFailedRetry: '报表导出失败,请重试', + importFailedTitle: '导入失败', + importProjectIdMissing: '该数据包不包含项目标识(旧版本导出包),为避免串项目已禁止导入。', + importProjectMismatch: '该数据包属于其他项目,不能覆盖当前项目。', + importInvalidFile: '文件无效、已损坏或被修改。', + importWriteError: '写入本地数据时发生错误。', + openFile: '打开文件' + } + }, + typeLine: { + copy: '复制', + copied: '已复制', + copyFailed: '复制失败', + brandAlt: '众为咨询', + supportText: '本网站由众为工程咨询有限公司提供免费技术支持', + aboutTitle: '关于我们', + companyName: '众为工程咨询有限公司', + openOfficialSiteAria: '跳转到官网首页', + officialSiteTitle: '官网首页', + aboutParagraph1: '众为工程咨询有限公司 2009 年成立,专注工程造价与工程成本管控全过程咨询,是广东省政府审计入库优选单位。公司服务覆盖多领域、全类型客户,累计服务投资额超万亿元,深度参与港珠澳大桥、澳门大学横琴校区等国家级重点工程,参编三十余项国家及省市行业标准。', + aboutParagraph2: '公司立足大湾区,布局全球,设有澳门公司、斯里兰卡分公司,具备跨境与海外项目服务能力,以十五年专业沉淀、万亿级项目经验,为客户提供精准、可靠的工程咨询服务。' + }, + agGrid: { + resetDefault: '恢复默认值' + }, + ht: { + title: '合同段列表', + projectTotalBudget: '项目总预算金额:{amount}', + budgetLoading: '计算中...', + selectedCount: '已选 {count} 个', + exportSelected: '导出已选', + deleteSelected: '删除已选', + cancelSelect: '取消', + addContract: '添加合同段', + batchDelete: '批量删除', + exportContracts: '导出合同段', + importContracts: '导入合同段', + searchPlaceholder: '搜索合同段名称或ID', + clearFilter: '清空筛选', + searchingHint: '搜索中({filtered} / {total}),已关闭拖拽排序', + selectModeExportHint: '导出选择模式:勾选合同段后点击“导出已选”', + selectModeDeleteHint: '删除选择模式:勾选合同段后点击“删除已选”', + setupRequiredHint: '请先在“基础信息”里新建项目并选择工程行业后,再新增或导入合同段', + listLayout: '列表布局', + gridLayout: '网格布局', + dragSort: '拖动排序', + dragSortSearchOff: '拖动排序(搜索时关闭)', + edit: '编辑', + remove: '删除', + idLabel: 'ID:{id}', + contractBudget: '预算:{amount}', + contractBudgetLine: '本合同预算金额:{amount}', + createdAt: '创建时间:{time}', + emptyTitle: '暂无合同卡片', + emptyDesc: '赶紧来添加吧', + notFound: '未找到匹配的合同段', + backToTop: '回到顶部', + editContract: '编辑合同段', + createContract: '新增合同段', + contractTabTitle: '合同段{name}', + contractName: '合同段名称', + contractNamePlaceholder: '请输入合同段名称', + save: '保存', + ok: '确定', + toastSuccessTitle: '操作成功', + createSuccess: '新建成功', + editSuccess: '编辑成功', + deleteSuccess: '删除成功', + sortDone: '排序完成', + exportSuccess: '导出成功({count} 个合同段)', + importSuccess: '导入成功({count} 个合同段)', + deleteBatchSuccess: '删除成功({count} 个合同段)', + tipTitle: '提示', + exportFailedTitle: '导出失败', + importFailedTitle: '导入失败', + batchDeleteFailedTitle: '批量删除失败', + retry: '请重试。', + selectAtLeastOne: '请先勾选至少一个合同段。', + noContractsToDelete: '未找到可删除的合同段。', + industryMissingForExport: '未读取到当前项目工程行业,请先在“基础信息”里新建项目。', + importIndustryMismatch: '工程行业不一致(导入包:{importIndustry},当前项目:{currentIndustry})。', + importCurrentIndustryMissing: '当前项目未设置工程行业,请先在“基础信息”里新建项目。', + importPackageIndustryMissing: '导入包缺少工程行业信息,请使用最新版本重新导出后再导入。', + importFileInvalid: '文件无效、已损坏或不是合同段导出文件。', + deleteSingleTitle: '确认删除合同段', + deleteSingleDesc: '即将删除“{name}”及其关联咨询服务和计价数据,是否继续?', + deleteBatchTitle: '确认批量删除', + deleteBatchDesc: '即将删除 {count} 个合同段及其关联咨询服务和计价数据,是否继续?' + }, + htCard: { + title: '合同段:{name}', + subtitle: '合同段ID:{id}', + metaBudget: '合同段预算金额:{amount}', + currencySuffix: '元', + categories: { + baseInfo: '基础信息', + scaleInfo: '规模信息', + services: '咨询服务', + consultFactor: '咨询分类系数', + majorFactor: '工程专业系数', + additionalFee: '附加工作费', + reserveFee: '预备费', + summary: '汇总' + } + }, + htBaseInfo: { + title: '基础信息', + defaultQuality: '造价咨询服务的综合评价应达到"较好"或综合评分90分', + qualityLabel: '质量要求', + qualityPlaceholder: '请输入质量要求', + durationLabel: '工期要求', + durationPlaceholder: '请输入工期要求' + }, + htFactors: { + consultCategoryTitle: '咨询分类系数明细', + majorTitle: '工程专业系数明细' + }, + htFee: { + additionalTitle: '附加工作费', + reserveTitle: '预备费' + }, + htInfo: { + scaleDetailTitle: '合同规模明细' + }, + htFeeRate: { + baseLabel: '基数(所有服务费预算合计)', + reserveBaseLabel: '基数(咨询服务总计 + 附加工作费总计)', + rateLabel: '费率(%)', + ratePlaceholder: '请输入费率,建议1 ~ 5', + budgetFeeLabel: '预算费用(自动计算)', + remarkLabel: '说明', + remarkPlaceholder: '请输入说明' + }, + htZxFw: { + title: '咨询服务明细', + warning: '※ 请注意检查并修改《规范》建议的限值或特殊值,并在确认金额栏修改', + editTabTitle: '服务编辑-{name}', + subtotal: '小计', + edit: '编辑', + resetDefault: '恢复默认', + delete: '删除', + processDraft: '编制', + processReview: '审核', + columns: { + code: '编码', + name: '名称', + process: '工作环节', + investScale: '投资规模法', + landScale: '用地规模法', + workload: '工作量法', + hourly: '工时法', + subtotal: '小计', + finalFee: '确认金额 ✎', + finalFeeTooltip: '该列支持手动修改,修改后会自动汇总到固定小计行', + actions: '操作' + }, + dialog: { + resetTitle: '确认恢复默认数据', + resetDesc: '会使用合同卡片里面最新填写的规模信息以及系数,自动计算默认数据,覆盖“{name}”当前数据,是否继续?', + confirmReset: '确认恢复', + deleteTitle: '确认删除服务', + deleteDesc: '将逻辑删除“{name}”,已填写的数据不会清空,重新勾选后会恢复,是否继续?' + } + }, + htSummary: { + title: '合同段汇总', + total: '合计', + remark: '说明', + placeholder: '请先填咨询服务/附加工作费/预备费的数据', + additionalPrefix: '附加工作费', + reservePrefix: '预备费', + explainByRate: '按费率{rate}%计得{fee}元', + explainByHourly: '按工时法计得{fee}元', + explainByQuantity: '按数量单价计得{fee}元', + columns: { + code: '编码', + name: '名称', + investScale: '投资规模法', + landScale: '用地规模法', + workload: '工作量法', + hourly: '工时法', + subtotal: '小计', + finalFee: '确认金额' + } + }, + htFeeGrid: { + subtotal: '小计', + currentRow: '当前行', + unnamed: '未命名', + edit: '编辑', + clear: '清空', + add: '新增', + editTabTitle: '费用编辑-{name}', + columns: { + name: '名字', + rateFee: '费率计取', + hourlyFee: '工时法', + quantityUnitPriceFee: '数量单价', + subtotal: '小计', + actions: '操作' + }, + dialog: { + clearTitle: '确认清空', + clearDesc: '将清空“{name}”及其编辑页面的可填和自动计算数据,是否继续?', + confirmClear: '确认清空' + } + }, + xmFactorGrid: { + clickToInput: '点击输入', + columns: { + standardFactor: '标准系数', + budgetValue: '预算取值', + remark: '说明', + groupName: '专业编码以及工程专业名称' + } + }, + serviceSelector: { + title: '选择服务', + clear: '清空', + empty: '暂无服务' + }, + zxFwView: { + contractPrefix: '合同段:{name}', + calcSuffix: '计算', + contractId: '合同ID:{id}', + workContentTitle: '工作内容', + categories: { + investmentScale: '投资规模法', + landScale: '用地规模法', + workload: '工作量法', + hourly: '工时法', + workContent: '工作内容' + }, + unavailable: { + investmentScaleTitle: '该服务不适用投资规模法', + investmentScaleMessage: '当前服务未启用规模法,投资规模法不可编辑。', + landScaleTitle: '该服务不适用用地规模法', + landScaleMessage: '当前服务仅支持投资规模法,用地规模法不可编辑。', + workloadTitle: '该服务不适用工作量法', + workloadMessage: '当前服务未启用工作量法,工作量法不可编辑。', + hourlyTitle: '该服务不适用工时法', + hourlyMessage: '当前服务未启用工时法,工时法不可编辑。' + } + }, + htFeeDetail: { + subtotal: '小计', + currentRow: '当前行', + clickToInput: '点击输入', + addRow: '添加行', + columns: { + no: '序号', + feeItem: '费用项', + unit: '单位', + quantity: '数量', + unitPrice: '单价(元)', + budgetFee: '预算费用(元)', + remark: '说明', + actions: '操作' + }, + dialog: { + deleteTitle: '确认删除行', + deleteDesc: '将删除“{name}”这条明细,是否继续?' + } + }, + workContent: { + title: '工作内容', + addCustom: '添加自定义内容', + clickToInput: '点击输入', + clickToInputContent: '点击输入工作内容', + currentRow: '当前行', + unnamed: '未命名', + ungrouped: '未分组', + type: { + basic: '基本工作', + optional: '可选工作', + daily: '日常顾问', + special: '专项顾问', + additional: '附加工作', + custom: '自定义' + }, + columns: { + no: '序号', + content: '工作内容', + type: '工作类型', + remark: '备注', + actions: '操作' + }, + dialog: { + deleteTitle: '确认删除行', + deleteDesc: '将删除“{name}”这条明细,是否继续?' + } + }, + quickCalc: { + projectName: '快速计算', + catalogEyebrow: '分类清单', + catalogTitle: '快速计算选项', + formEyebrow: '参数表单', + formTitle: '计算参数', + industryLabel: '行业 {name}', + selectIndustry: '请选择工程行业', + saving: '保存中...', + synced: '已同步行业', + notSelectedIndustry: '未选择行业', + notSelected: '未选择', + consultCategory: '咨询类别', + majorCategory: '工程专业', + fields: { + industry: '工程行业', + code: '编码', + investScale: '投资规模(万元)', + landScale: '用地规模(亩)', + formula: '计算式', + amount: '金额(元)', + consultFactor: '咨询分类系数', + majorFactor: '工程专业系数', + workEnvFactor: '工作环境系数', + workEnvFactorPlaceholder: '默认 1', + budgetAmount: '预算金额(元)' + }, + sections: { + currentSelection: '当前选项', + basicInfo: '基础信息', + scaleBase: '计算基数', + scaleParams: '规模参数', + benchmarkBudget: '基准预算', + budgetBase: '预算基础值', + serviceBudget: '服务预算', + factorsAndResult: '系数与结果' + }, + empty: { + selectIndustry: '请选择工程行业。选中后可先选择咨询类别,再显示对应专业分类。', + selectConsult: '请先选择咨询类别。选中后才会显示匹配的通用专业和工程专业分类。', + scaleUnavailable: '当前咨询类别不适用规模法,因此不显示专业分类。', + consultCostOnly: '当前咨询类别按行业汇总计价,工程专业系数已按所选行业自动带入,不再显示内部互补专业行。' + }, + placeholder: { + selectConsultFirst: '请先选择咨询类别', + scaleUnavailable: '当前分类不适用规模法', + selectMajorFirst: '请先选择工程专业', + preferLandScale: '当前专业按用地规模计价', + investUnavailable: '当前专业不适用投资规模', + consultCostOnly: '当前分类仅支持投资规模', + landUnavailable: '当前专业不适用用地规模', + input: '请输入', + selectScaleFirst: '请先选择输入对应规模' + } + }, + methodUnavailable: { + defaultTitle: '该服务不适用当前计价方法' + }, + xmCard: { + categories: { + info: '基础信息', + scaleInfo: '规模信息', + consultCategoryFactor: '咨询分类系数', + majorFactor: '工程专业系数', + contract: '合同段管理' + } + }, + htFeeMethodTypeLine: { + feeDetail: '费用明细', + unnamed: '未命名', + title: '合同段:{contractName} · {rowName}', + contractId: '合同ID:{id}', + quantityUnitPrice: '数量单价' + }, + pricingScale: { + totalInvestmentByIndustry: '{industryName}总投资', + totalInvestment: '总投资', + clickToInput: '点击输入', + projectLabel: '项目{index}', + columns: { + investAmount: '造价金额(万元)', + landArea: '用地面积(亩)', + benchmarkBudget: '基准预算(元)', + basicWork: '基本工作', + optionalWork: '可选工作', + subtotal: '小计', + budgetFee: '预算费用', + consultCategoryFactor: '咨询分类系数', + majorFactor: '专业系数', + workStageFactor: '工作环节系数(编审系数)', + workRatio: '工作占比(%)', + total: '合计', + remark: '说明', + majorGroup: '专业编码以及工程专业名称' + }, + tooltip: { + resetInvestAmount: '点击右侧↻恢复本列默认造价金额', + resetLandArea: '点击右侧↻恢复本列默认用地面积', + resetConsultCategoryFactor: '点击右侧↻恢复本列默认咨询分类系数', + resetMajorFactor: '点击右侧↻恢复本列默认专业系数' + } + }, + pricingPane: { + projectCount: '项目数量', + clearTitle: '确认清空当前明细', + confirmClear: '确认清空', + useDefault: '使用默认数据', + overrideTitle: '确认覆盖当前明细', + confirmOverride: '确认覆盖', + investment: { + title: '投资规模明细', + clearDesc: '将清空当前投资规模明细,是否继续?', + overrideDesc: '将使用合同默认数据覆盖当前投资规模明细,是否继续?' + }, + land: { + title: '用地规模明细', + clearDesc: '将清空当前用地规模明细,是否继续?', + overrideDesc: '将使用合同默认数据覆盖当前用地规模明细,是否继续?' + } + }, + workloadPricing: { + title: '工作量明细', + unavailableTitle: '该服务不适用工作量法', + unavailableMessage: '当前服务没有关联工作量法任务,无需填写此部分内容。', + clickToInput: '点击输入', + none: '无', + total: '总合计', + columns: { + code: '编码', + name: '名称', + budgetBase: '预算基数', + budgetReferenceUnitPrice: '预算参考单价', + budgetAdoptedUnitPrice: '预算采用单价', + workload: '工作量', + consultCategoryFactor: '咨询分类系数', + serviceFee: '服务费用(元)', + remark: '说明' + } + }, + hourlyFeeGrid: { + title: '工时法明细', + clickToInput: '点击输入', + total: '总合计', + columns: { + code: '编码', + name: '人员名称', + referenceUnitPrice: '预算参考单价', + laborBudgetUnitPrice: '人工预算单价(元/工日)', + compositeBudgetUnitPrice: '综合预算单价(元/工日)', + adoptedBudgetUnitPrice: '预算采用单价(元/工日)', + personnelCount: '人员数量(人)', + workdayCount: '工日数量(工日)', + serviceBudget: '服务预算(元)', + remark: '说明' + } + }, + xmScaleGrid: { + syncToastTitle: '已同步咨询服务', + syncToastDesc: '规模信息已同步到咨询服务({serviceCount} 项服务,{methodCount} 个计价页,{rowCount} 行)' + }, + xmInfo: { + defaultProjectName: 'xxx造价咨询服务', + defaultDesc: '在履行造价咨询服务时,宜根据咨询服务质量情况分级确定相应的处罚金额。其中考评得分在大于及等于85和小于90分时,处罚金额为预算费用的10%;其中考评得分在大于及等于80和小于85分时,处罚金额为预算费用的20%;其中考评得分在大于及等于75和小于80分时,处罚金额为预算费用的30%;其中考评得分在大于及等于70和小于75分时,处罚金额为预算费用的40%;其中考评得分小于70分时,处罚金额为预算费用的50%以上。', + industryHint: '变更需要重置后重新选择', + industryHintAria: '工程行业提示', + createFromHomeFirst: '请从首页先新建项目后再进入此页面。', + fields: { + projectName: '项目名称', + projectIndustry: '工程行业', + overview: '项目概况', + preparedBy: '编制人', + reviewedBy: '复核人', + preparedCompany: '编制单位', + preparedDate: '编制日期', + desc: '其他说明' + }, + placeholders: { + overview: '请输入项目概况', + preparedBy: '请输入编制人', + reviewedBy: '请输入复核人', + preparedCompany: '请输入编制单位' } } } as const - diff --git a/src/layout/tab.vue b/src/layout/tab.vue index 6d648dc..cac5ea1 100644 --- a/src/layout/tab.vue +++ b/src/layout/tab.vue @@ -9,6 +9,7 @@ import { useZxFwPricingStore } from '@/pinia/zxFwPricing' import { useZxFwPricingKeysStore } from '@/pinia/zxFwPricingKeys' import { useZxFwPricingHtFeeStore } from '@/pinia/zxFwPricingHtFee' import { useKvStore } from '@/pinia/kv' +import { UI_PREFS_STORAGE_KEY, useUiPrefsStore } from '@/pinia/uiPrefs' import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip' @@ -140,9 +141,16 @@ import { toMoney } from '@/lib/reportExportBuilders' import { exportFile } from '@/sql' -import { getIndustryDisplayName, industryTypeList } from '@/sql' +import { + getAdditionalWorkListEntries, + getIndustryDisplayName, + getReserveListEntries, + getServiceDictItemById, + industryTypeList +} from '@/sql' import { initializeProjectFactorStates } from '@/lib/projectWorkspace' -import { setAppLocale, type AppLocale } from '@/i18n' +import { I18N_LOCALE_KEY, type AppLocale } from '@/i18n' +const { t, locale } = useI18n() const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1' const THEME_PREFERENCE_KEY = 'jgjs-theme-dark-v1' @@ -150,86 +158,24 @@ const PROJECT_INFO_DB_KEY = 'xm-base-info-v1' const LEGACY_PROJECT_DB_KEY = 'xm-info-v3' const CONSULT_CATEGORY_FACTOR_DB_KEY = 'xm-consult-category-factor-v1' const MAJOR_FACTOR_DB_KEY = 'xm-major-factor-v1' -const DEFAULT_PROJECT_NAME = 'xxx 造价咨询服务' +const DEFAULT_PROJECT_NAME = t('xmInfo.defaultProjectName') const MAX_PROJECT_COUNT = 10 const PINIA_PERSIST_DB_NAME = getProjectDbName(readCurrentProjectId()) const PINIA_PERSIST_BASE_STORE_NAME = 'pinia' const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'zxFwPricingKeys', 'zxFwPricingHtFee', 'kv'] as const const RESET_MIN_LOADING_MS = 1000 -const userGuideSteps: UserGuideStep[] = [ - { - title: '欢迎使用', - description: '这个引导会覆盖系统主要功能和完整使用路径,建议按顺序走一遍。', +const userGuideSteps = computed(() => { + const stepKeys = ['step1', 'step2', 'step3', 'step4', 'step5', 'step6', 'step7', 'step8'] + return stepKeys.map(step => ({ + title: t(`tab.guide.steps.${step}.title`), + description: t(`tab.guide.steps.${step}.description`), points: [ - '顶部是标签页区,可在项目、合同段、服务计算页之间快速切换。', - '页面里的表格与表单会自动保存到本地,无需手动点击保存。', - '你可以随时点击右上角“使用引导”重新打开本教程。' + t(`tab.guide.steps.${step}.point1`), + t(`tab.guide.steps.${step}.point2`), + t(`tab.guide.steps.${step}.point3`) ] - }, - { - title: '项目卡片与四个模块', - description: '默认标签“项目卡片”是入口,左侧流程线包含四个项目级功能。', - points: [ - '基础信息:填写项目名称与项目规模明细。', - '合同段管理:新建、排序、搜索、导入/导出合同段。', - '咨询分类系数 / 工程专业系数:维护系数预算取值和备注。' - ] - }, - { - title: '基础信息填写', - description: '先在“基础信息”中补齐项目级数据,再进入后续合同段计算。', - points: [ - '项目名称会用于导出文件名和页面展示。', - '项目明细表支持直接编辑、复制粘贴、撤销重做。', - '分组行自动汇总,顶部固定行显示总合计。' - ] - }, - { - title: '合同段管理', - description: '在“合同段管理”中完成合同段生命周期操作。', - points: [ - '“添加合同段”用于新增,卡片右上角可编辑或删除。', - '支持搜索、网格/列表切换,非搜索状态可拖拽排序。', - '更多菜单可导入/导出合同段;点击卡片进入该合同段详情。' - ] - }, - { - title: '合同段详情', - description: '进入合同段后,左侧同样是流程线,包含规模信息和咨询服务两部分。', - points: [ - '规模信息:按工程专业填写当前合同段的规模数据。', - '咨询服务:选择服务词典并生成服务费用明细。', - '合同段页面会独立缓存,不同合同段互不干扰。' - ] - }, - { - title: '咨询服务与计算页', - description: '咨询服务页面用于管理服务明细,并进入具体计费方法页面。', - points: [ - '先点击“浏览”选择服务,再确认生成明细行。', - '明细表“编辑”可打开服务计算页,“清空”会重置该服务计算结果。', - '服务计算页包含投资规模法、用地规模法、工作量法、工时法四种方法。' - ] - }, - { - title: '系数维护', - description: '项目级系数用于调节预算取值,可在两个系数页分别维护。', - points: [ - '咨询分类系数页:按咨询分类维护预算取值与说明。', - '工程专业系数页:按专业树维护预算取值与说明。', - '支持批量粘贴、撤销重做,便于一次性维护多行数据。' - ] - }, - { - title: '数据管理与恢复', - description: '顶部工具栏负责全量数据导入导出与初始化重置。', - points: [ - '“导入/导出”是整项目级别的数据包操作。', - '“重置”会清空本地全部数据并恢复默认页面。', - '建议在重要调整前先导出备份。' - ] - } -] + })) +}) const componentMap: Record = { ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/features/xm/components/xmCard.vue'))), @@ -244,7 +190,7 @@ const zxFwPricingStore = useZxFwPricingStore() const zxFwPricingKeysStore = useZxFwPricingKeysStore() const zxFwPricingHtFeeStore = useZxFwPricingHtFeeStore() const kvStore = useKvStore() -const { t, locale } = useI18n() +const uiPrefsStore = useUiPrefsStore() const isDark = useDark({ selector: 'html', attribute: 'class', @@ -369,11 +315,11 @@ const hasClosableTabs = computed(() => { return tabStore.tabs.some((t: any) => t.componentName !== fixedId) }) const activeGuideStep = computed( - () => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0] + () => userGuideSteps.value[userGuideStepIndex.value] || userGuideSteps.value[0] ) const isFirstGuideStep = computed(() => userGuideStepIndex.value === 0) -const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideSteps.length - 1) -const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.length}`) +const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideSteps.value.length - 1) +const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.value.length}`) const canCloseLeft = computed(() => { if (contextTabIndex.value <= 0) return false return tabStore.tabs.slice(1, contextTabIndex.value).length > 0 @@ -395,8 +341,104 @@ const newProjectIndustryLabel = computed(() => { const toggleLocale = () => { const nextLocale: AppLocale = locale.value === 'en-US' ? 'zh-CN' : 'en-US' - setAppLocale(nextLocale) - window.location.reload() + uiPrefsStore.setLocale(nextLocale) +} + +const resolveHtFeeMethodRowNameByDict = ( + storageKeyRaw: string, + rowIdRaw: string, + fallbackRaw: string +) => { + const storageKey = String(storageKeyRaw || '').trim() + const rowId = String(rowIdRaw || '').trim() + const fallback = String(fallbackRaw || '').trim() + if (!storageKey || !rowId) return fallback + if (storageKey.includes('-additional-work')) { + const item = getAdditionalWorkListEntries(locale.value).find(entry => String(entry?.id || '').trim() === rowId) + return String(item?.name || '').trim() || fallback + } + if (storageKey.includes('-reserve')) { + const item = getReserveListEntries(locale.value).find(entry => String(entry?.id || '').trim() === rowId) + return String(item?.name || '').trim() || fallback + } + return fallback +} + +const syncTabLabelsByLocale = () => { + const nextProjectTitle = t('home.projectCalcTab') + const nextQuickTitle = t('home.quickCalcTab') + let changed = false + tabStore.tabs = tabStore.tabs.map((tab: any) => { + const currentProps = tab?.props && typeof tab.props === 'object' ? { ...tab.props } : undefined + if (tab.id === PROJECT_TAB_ID) { + if (tab.title === nextProjectTitle) return tab + changed = true + return { ...tab, title: nextProjectTitle } + } + if (tab.id === QUICK_TAB_ID) { + if (tab.title === nextQuickTitle) return tab + changed = true + return { ...tab, title: nextQuickTitle } + } + if (tab.componentName === 'QuickCalcView') { + const contractName = String(currentProps?.contractName || '').trim() + const nextTitle = t('ht.contractTabTitle', { name: contractName || '-' }) + if (tab.title === nextTitle) return tab + changed = true + return { ...tab, title: nextTitle } + } + if (tab.componentName === 'ZxFwView') { + const serviceId = String(currentProps?.serviceId || '').trim() + const dict = serviceId ? (getServiceDictItemById(serviceId) as { code?: string; name?: string } | undefined) : undefined + const serviceName = dict + ? `${String(dict.code || '').trim()}${String(dict.name || '').trim()}` + : String(currentProps?.fwName || '').trim() + const nextTitle = t('htZxFw.editTabTitle', { name: serviceName || '-' }) + const nextProps = { + ...(currentProps || {}), + fwName: serviceName || String(currentProps?.fwName || '') + } + if (tab.title === nextTitle && String(currentProps?.fwName || '') === String(nextProps.fwName || '')) return tab + changed = true + return { ...tab, title: nextTitle, props: nextProps } + } + if (tab.componentName === 'HtFeeMethodTypeLineView') { + const storageKey = String(currentProps?.storageKey || '').trim() + const rowId = String(currentProps?.rowId || '').trim() + const nextSourceTitle = storageKey.includes('-reserve') + ? t('htSummary.reservePrefix') + : storageKey.includes('-additional-work') + ? t('htSummary.additionalPrefix') + : String(currentProps?.sourceTitle || '') + const rows = storageKey + ? (zxFwPricingStore.getHtFeeMainState(storageKey)?.detailRows || []) + : [] + const fromStoreName = Array.isArray(rows) + ? String(rows.find((row: any) => String(row?.id || '') === rowId)?.name || '').trim() + : '' + const rowName = resolveHtFeeMethodRowNameByDict( + storageKey, + rowId, + fromStoreName || String(currentProps?.rowName || '').trim() + ) + const nextTitle = t('htFeeGrid.editTabTitle', { name: rowName || t('htFeeGrid.unnamed') }) + const nextProps = { + ...(currentProps || {}), + sourceTitle: nextSourceTitle, + rowName + } + if ( + tab.title === nextTitle + && String(currentProps?.rowName || '') === String(nextProps.rowName || '') + && String(currentProps?.sourceTitle || '') === String(nextProps.sourceTitle || '') + ) return tab + changed = true + return { ...tab, title: nextTitle, props: nextProps } + } + return tab + }) + if (!changed) return + scheduleUpdateTabTitleOverflow() } const closeMenus = () => { @@ -705,7 +747,7 @@ const shouldAutoOpenGuide = async () => { const openUserGuide = (startAt = 0) => { closeMenus() - userGuideStepIndex.value = Math.min(Math.max(startAt, 0), userGuideSteps.length - 1) + userGuideStepIndex.value = Math.min(Math.max(startAt, 0), userGuideSteps.value.length - 1) userGuideOpen.value = true } @@ -728,7 +770,7 @@ const nextUserGuideStep = () => { } const jumpToGuideStep = (stepIndex: number) => { - userGuideStepIndex.value = Math.min(Math.max(stepIndex, 0), userGuideSteps.length - 1) + userGuideStepIndex.value = Math.min(Math.max(stepIndex, 0), userGuideSteps.value.length - 1) } const openTabContextMenu = (event: MouseEvent, tabId: string) => { @@ -1083,7 +1125,7 @@ const buildAdditionalExport = async (contractId: string): Promise item.fee))), det } @@ -1100,7 +1142,7 @@ const buildReserveExport = async (contractId: string): Promise => { const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorState.resolved?.detailRows) const projectMajorCoes = buildProjectMajorCoes(majorFactorState.resolved?.detailRows) - const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目' + const projectName = isNonEmptyString(projectInfo.projectName) + ? projectInfo.projectName.trim() + : t('tab.messages.defaultProjectName') const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : '' const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : '' const company = isNonEmptyString(projectInfo.preparedCompany) ? projectInfo.preparedCompany.trim() : '' @@ -1274,7 +1318,9 @@ const buildExportReportPayload = async (): Promise => { }) contracts.push({ - name: isNonEmptyString(contract.name) ? contract.name : `合同段-${index + 1}`, + name: isNonEmptyString(contract.name) + ? contract.name + : t('tab.messages.contractFallbackName', { index: index + 1 }), serviceFee, addtionalFee, reserveFee, @@ -1345,7 +1391,7 @@ const exportData = async () => { URL.revokeObjectURL(url) } catch (error) { console.error('export failed:', error) - showMessageDialog('导出失败', '请重试。') + showMessageDialog(t('tab.toast.failed'), t('ht.retry')) } finally { dataMenuOpen.value = false } @@ -1355,15 +1401,17 @@ const exportReport = async () => { try { const now = new Date() const projectInfoRaw = await kvStore.getItem(PROJECT_INFO_DB_KEY) - const projectName = isNonEmptyString(projectInfoRaw?.projectName) ? sanitizeFileNamePart(projectInfoRaw.projectName) : '造价项目' - const fileName = `${formatExportTimestamp(now)}-${projectName}预算文件` + const projectName = isNonEmptyString(projectInfoRaw?.projectName) + ? sanitizeFileNamePart(projectInfoRaw.projectName) + : t('tab.messages.defaultProjectName') + const fileName = `${formatExportTimestamp(now)}-${projectName}${t('tab.messages.reportFileSuffix')}` const blobUrl = await exportFile(fileName, () => buildExportReportPayload(), () => { - showReportExportProgress(30, '正在生成报表文件...') + showReportExportProgress(30, t('tab.messages.reportGenerating')) }) - finishReportExportProgress(true, '报表导出完成', blobUrl) + finishReportExportProgress(true, t('tab.messages.reportExportDone'), blobUrl) } catch (error) { console.error('export report failed:', error) - finishReportExportProgress(false, '报表导出失败,请重试') + finishReportExportProgress(false, t('tab.messages.reportExportFailedRetry')) } finally { dataMenuOpen.value = false @@ -1412,14 +1460,14 @@ const importData = async (event: Event) => { } catch (error) { console.error('import failed:', error) if (error instanceof Error && error.message === 'PROJECT_ID_MISSING') { - showMessageDialog('导入失败', '该数据包不包含项目标识(旧版本导出包),为避免串项目已禁止导入。') + showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importProjectIdMissing')) return } if (error instanceof Error && error.message.startsWith('PROJECT_ID_MISMATCH:')) { - showMessageDialog('导入失败', '该数据包属于其他项目,不能覆盖当前项目。') + showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importProjectMismatch')) return } - showMessageDialog('导入失败', '文件无效、已损坏或被修改。') + showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importInvalidFile')) } finally { input.value = '' } @@ -1479,6 +1527,9 @@ const confirmImportOverride = async () => { kvStore.$patch(kvState as any) } + // 导入快照可能携带旧语言标题,先按当前 locale 归一化一次再持久化。 + syncTabLabelsByLocale() + await Promise.all([ tabStore.$persistNow?.(), zxFwPricingStore.$persistNow?.(), @@ -1490,7 +1541,7 @@ const confirmImportOverride = async () => { window.location.reload() } catch (error) { console.error('import apply failed:', error) - showMessageDialog('导入失败', '写入本地数据时发生错误。') + showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importWriteError')) } finally { cancelImportConfirm() } @@ -1532,12 +1583,22 @@ const handleReset = async () => { window.dispatchEvent(new CustomEvent('jgjs-release-project-lock')) const themePreference = typeof localStorage !== 'undefined' ? localStorage.getItem(THEME_PREFERENCE_KEY) : null + const localePreference = + typeof localStorage !== 'undefined' ? localStorage.getItem(I18N_LOCALE_KEY) : null + const uiPrefsSnapshot = + typeof localStorage !== 'undefined' ? localStorage.getItem(UI_PREFS_STORAGE_KEY) : null // 1) 仅清持久化层,不先改当前页面内存态,避免“先切首页再刷新”的二次闪烁。 localStorage.clear() if (themePreference != null) { localStorage.setItem(THEME_PREFERENCE_KEY, themePreference) } + if (localePreference != null) { + localStorage.setItem(I18N_LOCALE_KEY, localePreference) + } + if (uiPrefsSnapshot != null) { + localStorage.setItem(UI_PREFS_STORAGE_KEY, uiPrefsSnapshot) + } sessionStorage.clear() await projectDefaultForage.clear() @@ -1602,7 +1663,7 @@ onMounted(() => { if (pendingHomeImportFile) { void prepareImportPayloadFromFile(pendingHomeImportFile, { skipConfirm: skipWorkspaceImportConfirm }).catch(error => { console.error('home import failed:', error) - showMessageDialog('导入失败', '文件无效、已损坏或被修改。') + showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importInvalidFile')) }) } void (async () => { @@ -1687,6 +1748,14 @@ watch( }) } ) + +watch( + () => locale.value, + () => { + syncTabLabelsByLocale() + }, + { immediate: true } +) @@ -1815,6 +1884,18 @@ watch( {{ t('tab.toolbar.userGuide') }} + + {{ localeLabel }} + @@ -2111,7 +2192,7 @@ watch( @@ -2145,10 +2226,10 @@ watch( {{ reportExportText }} diff --git a/src/layout/typeLine.vue b/src/layout/typeLine.vue index 6c1cfc3..c9a0cd9 100644 --- a/src/layout/typeLine.vue +++ b/src/layout/typeLine.vue @@ -1,5 +1,6 @@
{{ props.title }}
{{ props.title || t('methodUnavailable.defaultTitle') }}
{{ props.message }}