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

View File

@ -339,6 +339,11 @@ const toggleItem = (groupKey: string, optionKey: string) => {
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 [consultMap, majorMap] = await Promise.all([
loadConsultCategoryFactorMap(props.projectConsultCategoryFactorKey),
@ -440,8 +445,7 @@ watch(canUseLandScale, enabled => {
<section class="quick-calc-panel quick-calc-panel--catalog">
<header class="quick-calc-panel__header">
<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.catalogTitle') }}</h2>
<h2 class="quick-calc-panel__title">{{ t('quickCalc.catalogEyebrow') }}</h2>
</div>
<div class="quick-calc-status">
@ -516,7 +520,7 @@ watch(canUseLandScale, enabled => {
>
<div class="quick-calc-group__side">
<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>
@ -559,8 +563,7 @@ watch(canUseLandScale, enabled => {
<aside class="quick-calc-panel quick-calc-panel--form">
<header class="quick-calc-panel__header">
<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.formTitle') }}</h2>
<h2 class="quick-calc-panel__title">{{ t('quickCalc.formEyebrow') }}</h2>
</div>
@ -569,10 +572,7 @@ watch(canUseLandScale, enabled => {
<div class="quick-calc-form">
<div class="quick-calc-form-stack">
<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">
<label class="quick-calc-field">
@ -588,10 +588,7 @@ watch(canUseLandScale, enabled => {
</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">
<label class="quick-calc-field">
@ -628,8 +625,7 @@ watch(canUseLandScale, enabled => {
<section class="quick-calc-form-section">
<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.budgetBase') }}</h3>
<h3 class="quick-calc-form-section__title">{{ t('quickCalc.sections.benchmarkBudget') }}</h3>
</header>
<div class="quick-calc-form-grid">
@ -647,8 +643,7 @@ watch(canUseLandScale, enabled => {
<section class="quick-calc-form-section">
<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.factorsAndResult') }}</h3>
<h3 class="quick-calc-form-section__title">{{ t('quickCalc.sections.serviceBudget') }}</h3>
</header>
<div class="quick-calc-form-grid">
@ -667,11 +662,11 @@ watch(canUseLandScale, enabled => {
</label>
<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
v-model="workEnvFactor"
class="quick-calc-field__input"
:placeholder="t('quickCalc.fields.workEnvFactorPlaceholder')"
:placeholder="t('quickCalc.fields.workEnvCoefficientPlaceholder')"
@blur="applyWorkEnvFactorInput"
@keydown.enter.prevent="applyWorkEnvFactorInput"
>
@ -821,6 +816,11 @@ watch(canUseLandScale, enabled => {
color: var(--qc-text);
}
.quick-calc-group__title--multiline {
white-space: pre-line;
line-height: 1.2;
}
.quick-calc-status {
display: flex;
flex-wrap: wrap;

View File

@ -21,6 +21,7 @@ export const enUS = {
title: 'Calculation Entry',
subtitle: 'Project Budget · Quick Calc · Import Data',
projectCalcTab: 'Project Calculation',
quickCalcTab: 'Quick Calculation',
cards: {
heroTitle: 'One-Click Smart Budget',
heroSubTitle: 'Accelerate standards adoption',
@ -463,9 +464,7 @@ export const enUS = {
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...',
@ -474,6 +473,28 @@ export const enUS = {
notSelected: 'Not selected',
consultCategory: 'Consult Category',
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: {
industry: 'Industry',
code: 'Code',
@ -483,19 +504,16 @@ export const enUS = {
amount: 'Amount (CNY)',
consultFactor: 'Consult Category Factor',
majorFactor: 'Major Factor',
workEnvFactor: 'Work Environment Factor',
workEnvFactorPlaceholder: 'Default 1',
workEnvCoefficient: 'Work environment coefficient',
workEnvCoefficientPlaceholder: '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.',

View File

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

View File

@ -1433,14 +1433,6 @@ const prepareImportPayloadFromFile = async (
if (!isDataPackageLike(payload)) {
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
pendingImportFileName.value = file.name
if (options?.skipConfirm) {
@ -1459,14 +1451,6 @@ const importData = async (event: Event) => {
await prepareImportPayloadFromFile(file)
} catch (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'))
} finally {
input.value = ''

View File

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