This commit is contained in:
wintsa 2026-03-26 09:34:26 +08:00
parent 1d016f8c51
commit de3585bde3
7 changed files with 97 additions and 68 deletions

View File

@ -44,7 +44,7 @@ import {
setPendingHomeImportFile, setPendingHomeImportFile,
writeWorkspaceMode writeWorkspaceMode
} from '@/lib/workspace' } from '@/lib/workspace'
import { upsertProject } from '@/lib/projectRegistry' import { createProject, upsertProject } from '@/lib/projectRegistry'
interface QuickProjectInfoState { interface QuickProjectInfoState {
projectIndustry?: string projectIndustry?: string
@ -188,7 +188,7 @@ const enterQuickCalc = (contractName: string) => {
writeWorkspaceMode('quick') writeWorkspaceMode('quick')
tabStore.enterWorkspace({ tabStore.enterWorkspace({
id: `contract-${QUICK_CONTRACT_ID}`, id: `contract-${QUICK_CONTRACT_ID}`,
title: contractName, title: t('home.quickCalcTab'),
componentName: 'QuickCalcWorkbenchView', componentName: 'QuickCalcWorkbenchView',
props: { props: {
contractId: QUICK_CONTRACT_ID, contractId: QUICK_CONTRACT_ID,
@ -261,9 +261,8 @@ const confirmHomeImport = () => {
window.dispatchEvent(new CustomEvent('home-import-selected', { window.dispatchEvent(new CustomEvent('home-import-selected', {
detail: { file } detail: { file }
})) }))
const projectId = getActiveProjectId() const project = createProject()
upsertProject(projectId, projectId === DEFAULT_PROJECT_ID ? t('tab.messages.defaultProjectLabel') : undefined) writeProjectIdToUrl(project.id)
writeProjectIdToUrl(projectId)
writeWorkspaceMode('project') writeWorkspaceMode('project')
tabStore.enterWorkspace({ tabStore.enterWorkspace({
id: PROJECT_TAB_ID, id: PROJECT_TAB_ID,

View File

@ -339,6 +339,11 @@ const toggleItem = (groupKey: string, optionKey: string) => {
selectedMajor.value = { groupKey, optionKey } selectedMajor.value = { groupKey, optionKey }
} }
const getGroupTitle = (group: { key: string; label: string }) => {
if (group.key !== 'consult') return group.label
return group.label.replace(/([(](?:常用|Common)[)])$/i, '\n$1')
}
const loadFactorDefaults = async () => { const loadFactorDefaults = async () => {
const [consultMap, majorMap] = await Promise.all([ const [consultMap, majorMap] = await Promise.all([
loadConsultCategoryFactorMap(props.projectConsultCategoryFactorKey), loadConsultCategoryFactorMap(props.projectConsultCategoryFactorKey),
@ -440,8 +445,7 @@ watch(canUseLandScale, enabled => {
<section class="quick-calc-panel quick-calc-panel--catalog"> <section class="quick-calc-panel quick-calc-panel--catalog">
<header class="quick-calc-panel__header"> <header class="quick-calc-panel__header">
<div class="quick-calc-panel__title-wrap"> <div class="quick-calc-panel__title-wrap">
<div class="quick-calc-panel__eyebrow">{{ t('quickCalc.catalogEyebrow') }}</div> <h2 class="quick-calc-panel__title">{{ t('quickCalc.catalogEyebrow') }}</h2>
<h2 class="quick-calc-panel__title">{{ t('quickCalc.catalogTitle') }}</h2>
</div> </div>
<div class="quick-calc-status"> <div class="quick-calc-status">
@ -516,7 +520,7 @@ watch(canUseLandScale, enabled => {
> >
<div class="quick-calc-group__side"> <div class="quick-calc-group__side">
<div class="quick-calc-group__eyebrow">{{ group.key === 'consult' ? t('quickCalc.consultCategory') : t('quickCalc.majorCategory') }}</div> <div class="quick-calc-group__eyebrow">{{ group.key === 'consult' ? t('quickCalc.consultCategory') : t('quickCalc.majorCategory') }}</div>
<h3 class="quick-calc-group__title">{{ group.label }}</h3> <h3 class="quick-calc-group__title quick-calc-group__title--multiline">{{ getGroupTitle(group) }}</h3>
</div> </div>
@ -559,8 +563,7 @@ watch(canUseLandScale, enabled => {
<aside class="quick-calc-panel quick-calc-panel--form"> <aside class="quick-calc-panel quick-calc-panel--form">
<header class="quick-calc-panel__header"> <header class="quick-calc-panel__header">
<div class="quick-calc-panel__title-wrap"> <div class="quick-calc-panel__title-wrap">
<div class="quick-calc-panel__eyebrow">{{ t('quickCalc.formEyebrow') }}</div> <h2 class="quick-calc-panel__title">{{ t('quickCalc.formEyebrow') }}</h2>
<h2 class="quick-calc-panel__title">{{ t('quickCalc.formTitle') }}</h2>
</div> </div>
@ -569,10 +572,7 @@ watch(canUseLandScale, enabled => {
<div class="quick-calc-form"> <div class="quick-calc-form">
<div class="quick-calc-form-stack"> <div class="quick-calc-form-stack">
<section class="quick-calc-form-section quick-calc-form-section--summary"> <section class="quick-calc-form-section quick-calc-form-section--summary">
<header class="quick-calc-form-section__header">
<div class="quick-calc-form-section__eyebrow">{{ t('quickCalc.sections.currentSelection') }}</div>
<h3 class="quick-calc-form-section__title">{{ t('quickCalc.sections.basicInfo') }}</h3>
</header>
<div class="quick-calc-form-grid quick-calc-form-grid--summary"> <div class="quick-calc-form-grid quick-calc-form-grid--summary">
<label class="quick-calc-field"> <label class="quick-calc-field">
@ -588,10 +588,7 @@ watch(canUseLandScale, enabled => {
</section> </section>
<section class="quick-calc-form-section"> <section class="quick-calc-form-section">
<header class="quick-calc-form-section__header">
<div class="quick-calc-form-section__eyebrow">{{ t('quickCalc.sections.scaleBase') }}</div>
<h3 class="quick-calc-form-section__title">{{ t('quickCalc.sections.scaleParams') }}</h3>
</header>
<div class="quick-calc-form-grid"> <div class="quick-calc-form-grid">
<label class="quick-calc-field"> <label class="quick-calc-field">
@ -628,8 +625,7 @@ watch(canUseLandScale, enabled => {
<section class="quick-calc-form-section"> <section class="quick-calc-form-section">
<header class="quick-calc-form-section__header"> <header class="quick-calc-form-section__header">
<div class="quick-calc-form-section__eyebrow">{{ t('quickCalc.sections.benchmarkBudget') }}</div> <h3 class="quick-calc-form-section__title">{{ t('quickCalc.sections.benchmarkBudget') }}</h3>
<h3 class="quick-calc-form-section__title">{{ t('quickCalc.sections.budgetBase') }}</h3>
</header> </header>
<div class="quick-calc-form-grid"> <div class="quick-calc-form-grid">
@ -647,8 +643,7 @@ watch(canUseLandScale, enabled => {
<section class="quick-calc-form-section"> <section class="quick-calc-form-section">
<header class="quick-calc-form-section__header"> <header class="quick-calc-form-section__header">
<div class="quick-calc-form-section__eyebrow">{{ t('quickCalc.sections.serviceBudget') }}</div> <h3 class="quick-calc-form-section__title">{{ t('quickCalc.sections.serviceBudget') }}</h3>
<h3 class="quick-calc-form-section__title">{{ t('quickCalc.sections.factorsAndResult') }}</h3>
</header> </header>
<div class="quick-calc-form-grid"> <div class="quick-calc-form-grid">
@ -667,11 +662,11 @@ watch(canUseLandScale, enabled => {
</label> </label>
<div class="quick-calc-field"> <div class="quick-calc-field">
<span class="quick-calc-field__label">{{ t('quickCalc.fields.workEnvFactor') }}</span> <span class="quick-calc-field__label">{{ t('quickCalc.fields.workEnvCoefficient') }}</span>
<input <input
v-model="workEnvFactor" v-model="workEnvFactor"
class="quick-calc-field__input" class="quick-calc-field__input"
:placeholder="t('quickCalc.fields.workEnvFactorPlaceholder')" :placeholder="t('quickCalc.fields.workEnvCoefficientPlaceholder')"
@blur="applyWorkEnvFactorInput" @blur="applyWorkEnvFactorInput"
@keydown.enter.prevent="applyWorkEnvFactorInput" @keydown.enter.prevent="applyWorkEnvFactorInput"
> >
@ -821,6 +816,11 @@ watch(canUseLandScale, enabled => {
color: var(--qc-text); color: var(--qc-text);
} }
.quick-calc-group__title--multiline {
white-space: pre-line;
line-height: 1.2;
}
.quick-calc-status { .quick-calc-status {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -21,6 +21,7 @@ export const enUS = {
title: 'Calculation Entry', title: 'Calculation Entry',
subtitle: 'Project Budget · Quick Calc · Import Data', subtitle: 'Project Budget · Quick Calc · Import Data',
projectCalcTab: 'Project Calculation', projectCalcTab: 'Project Calculation',
quickCalcTab: 'Quick Calculation',
cards: { cards: {
heroTitle: 'One-Click Smart Budget', heroTitle: 'One-Click Smart Budget',
heroSubTitle: 'Accelerate standards adoption', heroSubTitle: 'Accelerate standards adoption',
@ -463,9 +464,7 @@ export const enUS = {
quickCalc: { quickCalc: {
projectName: 'Quick Calculation', projectName: 'Quick Calculation',
catalogEyebrow: 'Category List', catalogEyebrow: 'Category List',
catalogTitle: 'Quick Calc Options',
formEyebrow: 'Parameter Form', formEyebrow: 'Parameter Form',
formTitle: 'Calculation Parameters',
industryLabel: 'Industry {name}', industryLabel: 'Industry {name}',
selectIndustry: 'Select industry', selectIndustry: 'Select industry',
saving: 'Saving...', saving: 'Saving...',
@ -474,6 +473,28 @@ export const enUS = {
notSelected: 'Not selected', notSelected: 'Not selected',
consultCategory: 'Consult Category', consultCategory: 'Consult Category',
majorCategory: 'Major', majorCategory: 'Major',
types: {
consult: {
label: 'Consult Category (Common)',
hint: 'Select consult category first, then complete scale and budget parameters.'
},
general: {
label: 'General Major',
hint: 'Cross-industry common compensation and other expense majors.'
},
road: {
label: 'Highway Major',
hint: 'Shown by default when industry is Highway Engineering.'
},
railway: {
label: 'Railway Major',
hint: 'Shown by default when industry is Railway Engineering.'
},
waterway: {
label: 'Waterway Major',
hint: 'Shown by default when industry is Waterway Engineering.'
}
},
fields: { fields: {
industry: 'Industry', industry: 'Industry',
code: 'Code', code: 'Code',
@ -483,19 +504,16 @@ export const enUS = {
amount: 'Amount (CNY)', amount: 'Amount (CNY)',
consultFactor: 'Consult Category Factor', consultFactor: 'Consult Category Factor',
majorFactor: 'Major Factor', majorFactor: 'Major Factor',
workEnvFactor: 'Work Environment Factor', workEnvCoefficient: 'Work environment coefficient',
workEnvFactorPlaceholder: 'Default 1', workEnvCoefficientPlaceholder: 'Default 1',
budgetAmount: 'Budget Amount (CNY)' budgetAmount: 'Budget Amount (CNY)'
}, },
sections: { sections: {
currentSelection: 'Current Selection', currentSelection: 'Current Selection',
basicInfo: 'Basic Info', basicInfo: 'Basic Info',
scaleBase: 'Scale Base', scaleBase: 'Scale Base',
scaleParams: 'Scale Parameters',
benchmarkBudget: 'Benchmark Budget', benchmarkBudget: 'Benchmark Budget',
budgetBase: 'Budget Base',
serviceBudget: 'Service Budget', serviceBudget: 'Service Budget',
factorsAndResult: 'Factors and Result'
}, },
empty: { empty: {
selectIndustry: 'Select an industry first. Then choose consult category and matched majors will appear.', selectIndustry: 'Select an industry first. Then choose consult category and matched majors will appear.',

View File

@ -21,6 +21,7 @@ export const zhCN = {
title: '计算入口', title: '计算入口',
subtitle: '项目计算 · 单项速算 · 导入数据', subtitle: '项目计算 · 单项速算 · 导入数据',
projectCalcTab: '项目计算', projectCalcTab: '项目计算',
quickCalcTab: '快速计算',
cards: { cards: {
heroTitle: '智能预算一键生成', heroTitle: '智能预算一键生成',
heroSubTitle: '助力《规范》高效落地', heroSubTitle: '助力《规范》高效落地',
@ -463,9 +464,7 @@ export const zhCN = {
quickCalc: { quickCalc: {
projectName: '快速计算', projectName: '快速计算',
catalogEyebrow: '分类清单', catalogEyebrow: '分类清单',
catalogTitle: '快速计算选项',
formEyebrow: '参数表单', formEyebrow: '参数表单',
formTitle: '计算参数',
industryLabel: '行业 {name}', industryLabel: '行业 {name}',
selectIndustry: '请选择工程行业', selectIndustry: '请选择工程行业',
saving: '保存中...', saving: '保存中...',
@ -474,6 +473,28 @@ export const zhCN = {
notSelected: '未选择', notSelected: '未选择',
consultCategory: '咨询类别', consultCategory: '咨询类别',
majorCategory: '工程专业', majorCategory: '工程专业',
types: {
consult: {
label: '咨询类别(常用)',
hint: '先选择咨询类别,再补规模和预算参数。'
},
general: {
label: '通用专业',
hint: '跨行业共用的补偿与其他费用专业。'
},
road: {
label: '公路工程专业',
hint: '首页行业为公路工程时默认展示。'
},
railway: {
label: '铁路工程专业',
hint: '首页行业为铁路工程时默认展示。'
},
waterway: {
label: '水运工程专业',
hint: '首页行业为水运工程时默认展示。'
}
},
fields: { fields: {
industry: '工程行业', industry: '工程行业',
code: '编码', code: '编码',
@ -483,19 +504,15 @@ export const zhCN = {
amount: '金额(元)', amount: '金额(元)',
consultFactor: '咨询分类系数', consultFactor: '咨询分类系数',
majorFactor: '工程专业系数', majorFactor: '工程专业系数',
workEnvFactor: '工作环境系数', workEnvCoefficient: '工作环节系数',
workEnvFactorPlaceholder: '默认 1', workEnvCoefficientPlaceholder: '默认 1',
budgetAmount: '预算金额(元)' budgetAmount: '预算金额(元)'
}, },
sections: { sections: {
currentSelection: '当前选项',
basicInfo: '基础信息', basicInfo: '基础信息',
scaleBase: '计算基数', scaleBase: '计算基数',
scaleParams: '规模参数',
benchmarkBudget: '基准预算', benchmarkBudget: '基准预算',
budgetBase: '预算基础值',
serviceBudget: '服务预算', serviceBudget: '服务预算',
factorsAndResult: '系数与结果'
}, },
empty: { empty: {
selectIndustry: '请选择工程行业。选中后可先选择咨询类别,再显示对应专业分类。', selectIndustry: '请选择工程行业。选中后可先选择咨询类别,再显示对应专业分类。',

View File

@ -1433,14 +1433,6 @@ const prepareImportPayloadFromFile = async (
if (!isDataPackageLike(payload)) { if (!isDataPackageLike(payload)) {
throw new Error('INVALID_DATA_PACKAGE') throw new Error('INVALID_DATA_PACKAGE')
} }
const currentProjectId = readCurrentProjectId()
const payloadProjectId = String(payload.projectId || '').trim()
if (!payloadProjectId) {
throw new Error('PROJECT_ID_MISSING')
}
if (payloadProjectId && payloadProjectId !== currentProjectId) {
throw new Error(`PROJECT_ID_MISMATCH:${payloadProjectId}:${currentProjectId}`)
}
pendingImportPayload.value = payload pendingImportPayload.value = payload
pendingImportFileName.value = file.name pendingImportFileName.value = file.name
if (options?.skipConfirm) { if (options?.skipConfirm) {
@ -1459,14 +1451,6 @@ const importData = async (event: Event) => {
await prepareImportPayloadFromFile(file) await prepareImportPayloadFromFile(file)
} catch (error) { } catch (error) {
console.error('import failed:', error) console.error('import failed:', error)
if (error instanceof Error && error.message === 'PROJECT_ID_MISSING') {
showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importProjectIdMissing'))
return
}
if (error instanceof Error && error.message.startsWith('PROJECT_ID_MISMATCH:')) {
showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importProjectMismatch'))
return
}
showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importInvalidFile')) showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importInvalidFile'))
} finally { } finally {
input.value = '' input.value = ''

View File

@ -124,7 +124,8 @@ const localizeDictItem = (item: Record<string, any>) => {
return { return {
...item, ...item,
name: item.nameEn || item.name, name: item.nameEn || item.name,
quickLabel: item.quickLabelEn || item.quickLabel || item.nameEn || item.name, // English locale should prefer English name when quickLabelEn is absent.
quickLabel: item.quickLabelEn || item.nameEn || item.quickLabel || item.name,
basicParam: item.basicParamEn || item.basicParam, basicParam: item.basicParamEn || item.basicParam,
unit: item.unitEn || item.unit, unit: item.unitEn || item.unit,
desc: item.descEn || item.desc desc: item.descEn || item.desc
@ -790,6 +791,16 @@ export type QuickCalcGroup = {
industryId?: string industryId?: string
} }
const getQuickCalcI18nText = (path: string, fallback: string) => {
try {
const text = i18n?.global?.t?.(path as any)
if (typeof text === 'string' && text !== path) return text
} catch {
// ignore i18n runtime errors and fallback to default text
}
return fallback
}
const getQuickDictLabel = (item: DictItem | undefined, fallback = '') => const getQuickDictLabel = (item: DictItem | undefined, fallback = '') =>
String(item?.quickLabel || item?.name || fallback) String(item?.quickLabel || item?.name || fallback)
@ -820,8 +831,8 @@ const createQuickOptionByMajorKey = (
export const getQuickCalcGroups = (): QuickCalcGroup[] => [ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
{ {
key: 'consult', key: 'consult',
label: '咨询类别(常用)', label: getQuickCalcI18nText('quickCalc.types.consult.label', '咨询类别(常用)'),
hint: '先选择咨询类别,再补规模和预算参数。', hint: getQuickCalcI18nText('quickCalc.types.consult.hint', '先选择咨询类别,再补规模和预算参数。'),
items: [ items: [
createQuickOptionByServiceKey('0'), createQuickOptionByServiceKey('0'),
createQuickOptionByServiceKey('2'), createQuickOptionByServiceKey('2'),
@ -863,8 +874,8 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
}, },
{ {
key: 'general', key: 'general',
label: '通用专业', label: getQuickCalcI18nText('quickCalc.types.general.label', '通用专业'),
hint: '跨行业共用的补偿与其他费用专业。', hint: getQuickCalcI18nText('quickCalc.types.general.hint', '跨行业共用的补偿与其他费用专业。'),
items: [ items: [
createQuickOptionByMajorKey('1'), createQuickOptionByMajorKey('1'),
createQuickOptionByMajorKey('2'), createQuickOptionByMajorKey('2'),
@ -875,8 +886,8 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
}, },
{ {
key: 'road', key: 'road',
label: '公路工程专业', label: getQuickCalcI18nText('quickCalc.types.road.label', '公路工程专业'),
hint: '首页行业为公路工程时默认展示。', hint: getQuickCalcI18nText('quickCalc.types.road.hint', '首页行业为公路工程时默认展示。'),
industryId: '0', industryId: '0',
items: [ items: [
createQuickOptionByMajorKey('7'), createQuickOptionByMajorKey('7'),
@ -899,8 +910,8 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
}, },
{ {
key: 'railway', key: 'railway',
label: '铁路工程专业', label: getQuickCalcI18nText('quickCalc.types.railway.label', '铁路工程专业'),
hint: '首页行业为铁路工程时默认展示。', hint: getQuickCalcI18nText('quickCalc.types.railway.hint', '首页行业为铁路工程时默认展示。'),
industryId: '1', industryId: '1',
items: [ items: [
createQuickOptionByMajorKey('18'), createQuickOptionByMajorKey('18'),
@ -922,8 +933,8 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
}, },
{ {
key: 'waterway', key: 'waterway',
label: '水运工程专业', label: getQuickCalcI18nText('quickCalc.types.waterway.label', '水运工程专业'),
hint: '首页行业为水运工程时默认展示。', hint: getQuickCalcI18nText('quickCalc.types.waterway.hint', '首页行业为水运工程时默认展示。'),
industryId: '2', industryId: '2',
items: [ items: [
createQuickOptionByMajorKey('28'), createQuickOptionByMajorKey('28'),
@ -931,7 +942,7 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
createQuickOptionByMajorKey('30'), createQuickOptionByMajorKey('30'),
createQuickOptionByMajorKey('31'), createQuickOptionByMajorKey('31'),
createQuickOptionByMajorKey('32'), createQuickOptionByMajorKey('32'),
createQuickOptionByMajorKey('33', { label: '房建工程(房屋建筑及附属工程)' }) createQuickOptionByMajorKey('33')
], ],
rows: [ rows: [
['28'], ['28'],
@ -1140,7 +1151,7 @@ async function generateTemplate(data) {
const suffixTexts = ['协助委托人完成咨询服务相应造价文件的报审、报备、报批、检查与审计涉及的解释与回复、修改与调整等工作', '完成本合同造价档案的收集、整理和归档']; const suffixTexts = ['协助委托人完成咨询服务相应造价文件的报审、报备、报批、检查与审计涉及的解释与回复、修改与调整等工作', '完成本合同造价档案的收集、整理和归档'];
try { try {
// 获取模板 // 获取模板
let templateExcel = 'template20260226001test010'; let templateExcel = 'template202603';
let templateUrl = `./${templateExcel}.xlsx`; let templateUrl = `./${templateExcel}.xlsx`;
let buf = await (await fetch(templateUrl)).arrayBuffer(); let buf = await (await fetch(templateUrl)).arrayBuffer();
let workbook = new ExcelJS.Workbook(); let workbook = new ExcelJS.Workbook();