From aa67a66047c2c3ea8e1f97b6c69343f18b326b57 Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Mon, 13 Apr 2026 14:40:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=8D=E8=B4=A3=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/disclaimer.html | 313 ++++++++++++++++-- .../workbench/components/HomeEntryView.vue | 135 +++++--- src/layout/tab.vue | 6 + src/lib/workspace.ts | 107 ++++++ 4 files changed, 500 insertions(+), 61 deletions(-) diff --git a/public/disclaimer.html b/public/disclaimer.html index 1cce9b2..1bb9e6c 100644 --- a/public/disclaimer.html +++ b/public/disclaimer.html @@ -3,7 +3,7 @@ - 免责声明 + 预算编制工具免责声明 @@ -76,11 +209,147 @@

DISCLAIMER

-

免责声明

+

《交通运输工程造价咨询服务预算编制规范》(T/GDHS 017-2026)预算编制工具免责声明

+

最后更新日期:

+

+ 感谢您使用本网站提供的《交通运输工程造价咨询服务预算编制规范》(T/GDHS 017-2026)造价咨询服务预算编制工具(以下简称编制工具)(以下简称本工具)。在您使用本工具前,请仔细阅读以下免责声明条款。您继续使用本工具,即视为您已阅读、理解并同意接受本声明的全部内容。 +

-

本计算工具由众为工程咨询有限公司提供免费技术支持。

-

众为工程咨询有限公司

+ +
+

1. 标准依据说明

+

1.1 本工具依据广东省公路学会发布的团体标准《交通运输工程造价咨询服务预算编制规范》(T/GDHS 017-2026)(以下简称本规范)设定的编制方法。使用者应自行判断该标准是否适用于其具体项目及所在地区主管部门的要求。

+

1.2 本工具所依据的规范版本已在工具界面中标注(T/GDHS 017-2026)。如该规范后续发布修订内容、补充规定或被新版本替代,本工具可能无法及时同步更新。使用者有责任在使用前确认所依据的规范版本是否为最新有效版本。

+

1.3 本工具的计算结果基于本规范中的预算编制方法、费用组成及编制规则,但不同地区、不同项目法人对造价咨询服务预算编制的具体要求和计算方法可能存在差异。本工具不保证其计算结果符合任何特定项目或特定主管部门的审核要求。

+
+ +
+

2. 计算结果仅供参考

+

本工具所提供的所有计算结果(包括但不限于数值、明细表、汇总报表、编制说明等)均基于您输入的参数(如工程行业、项目规模、咨询类别、工程专业、工作内容、调整系数等)以及本规范中的数学模型与公式自动生成,仅供您参考使用。这些结果不构成任何形式的专业建议,也不代表任何官方或强制性的预算审批依据。

+
+ +
+

3. 不保证准确性与完整性

+

尽管我们尽力确保工具的可用性,但本工具按现状和现有基础提供,不附带任何明示或暗示的保证。我们无法保证计算结果在任何情况下均准确、无误或完整。由于数据输入错误、公式取舍、四舍五入或系统延迟等原因,结果可能与实际情况存在偏差。

+
+ +
+

4. 用户自行承担风险

+

您应当独立判断计算结果的可信性,并承担将其用于任何决策所产生的全部风险与责任。您不应依赖本工具的编制结果替代专业人士的具体计算或复核。在作出重大决定前,建议您咨询持有交通运输工程造价工程师注册证书的专业人员,或结合项目具体情况进行人工验证与复核。

+
+ +
+

5. 责任限制

+

在适用法律允许的最大范围内,本工具的开发方、管理方、发布方及其关联方不对因使用或无法使用本工具而导致的任何直接、间接、偶然、特殊或后果性损失承担法律责任,即使已被告知可能发生此类损失。

+

特别声明:任何造价咨询企业或人员依据本工具计算结果出具的造价咨询成果文件,其质量责任由出具方自行承担。本工具不对任何第三方造价咨询成果的准确性、合规性或引发的任何纠纷承担任何责任。

+
+ +
+

6. 服务中断与修改

+

我们保留随时修改、暂停或终止本工具部分或全部功能的权利,且可能不另行通知。对于因技术维护、网络故障、第三方服务中断、规范版本变更等原因导致的工具不可用、数据丢失或计算结果变化,我们不承担责任。

+
+ +
+

7. 外部链接与第三方内容

+

如果本工具引用或链接至全国团体标准信息平台、广东省公路学会官网或其他第三方网站,该等链接仅为方便用户查阅规范原文而提供,不代表我们认可其内容的准确性、时效性或完整性。对于任何第三方网站或工具的信息、服务或内容,我们不承担任何责任。

+
+ +
+

8. 适用法律

+

本声明的解释、效力及争议解决均适用中华人民共和国法律。若本声明任何条款被认定为无效或不可执行,不影响其余条款的效力。

+
+ +
+

用户确认

+

我已阅读、理解并同意本免责声明的全部内容。

+

(勾选后方可继续使用本工具)

+ +
+ + 返回入口 +
+

勾选后将记录当前浏览器的同意状态,后续从同一受限入口访问时不再重复提示。

+
+ +
+ + diff --git a/src/features/workbench/components/HomeEntryView.vue b/src/features/workbench/components/HomeEntryView.vue index dd805c7..6d0b1dd 100644 --- a/src/features/workbench/components/HomeEntryView.vue +++ b/src/features/workbench/components/HomeEntryView.vue @@ -27,11 +27,16 @@ import { SelectViewport } from 'reka-ui' import { + buildDisclaimerUrl, buildProjectUrl, + consumePendingDisclaimerAction, DEFAULT_PROJECT_ID, + hasAcceptedRestrictedDisclaimer, FORCE_HOME_QUERY_KEY, + isDisclaimerAcceptanceRequired, NEW_PROJECT_QUERY_KEY, OPEN_PROJECT_DIALOG_QUERY_KEY, + setPendingDisclaimerAction, PROJECT_TAB_ID, QUICK_PROJECT_ID, QUICK_CONSULT_CATEGORY_FACTOR_KEY, @@ -94,6 +99,7 @@ const homeImportConfirmOpen = ref(false) const pendingHomeImportFile = ref(null) const pendingHomeImportFileName = ref('') const existingProjectDialogOpen = ref(false) +const disclaimerRequired = ref(false) const existingProjects = ref>([]) const existingProjectLoading = ref(false) const hasExistingProjects = ref(false) @@ -174,8 +180,31 @@ const loadProjectDefaults = async () => { } const openProjectCalc = async () => { - - projectDialogOpen.value = true + await runWithDisclaimerGuard({ type: 'project' }, async () => { + await loadProjectDefaults() + projectDialogOpen.value = true + }) +} + +const redirectToDisclaimerPage = () => { + const returnUrl = window.location.href + window.location.href = buildDisclaimerUrl(returnUrl) +} + +const runWithDisclaimerGuard = async ( + pendingAction: { type: 'project' | 'quick' | 'import' | 'existing-project'; projectId?: string }, + action: () => void | Promise +) => { + if (!disclaimerRequired.value || hasAcceptedRestrictedDisclaimer()) { + await action() + return + } + setPendingDisclaimerAction(pendingAction) + redirectToDisclaimerPage() +} + +const syncDisclaimerRequirement = () => { + disclaimerRequired.value = isDisclaimerAcceptanceRequired() } const syncExistingProjectOpenedState = (projectIds: string[]) => { @@ -228,9 +257,11 @@ const startExistingProjectPolling = () => { } const openExistingProjectDialog = async () => { - existingProjectDialogOpen.value = true - await refreshExistingProjects() - startExistingProjectPolling() + await runWithDisclaimerGuard({ type: 'existing-project' }, async () => { + existingProjectDialogOpen.value = true + await refreshExistingProjects() + startExistingProjectPolling() + }) } const closeExistingProjectDialog = () => { @@ -322,35 +353,37 @@ const enterQuickCalc = (contractName: string) => { } const openQuickCalc = async () => { - await loadQuickDefaults() - const contractName = quickContractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME - const industry = quickIndustry.value.trim() + await runWithDisclaimerGuard({ type: 'quick' }, async () => { + await loadQuickDefaults() + const contractName = quickContractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME + const industry = quickIndustry.value.trim() - quickSubmitting.value = true - try { - const currentInfo = await kvStore.getItem(QUICK_PROJECT_INFO_KEY) - await kvStore.setItem(QUICK_PROJECT_INFO_KEY, { - ...currentInfo, - projectIndustry: industry, - projectName: t('quickCalc.projectName') - }) - await kvStore.setItem(QUICK_CONTRACT_META_KEY, { - id: QUICK_CONTRACT_ID, - name: contractName, - updatedAt: new Date().toISOString() - }) - if (industry) { - await initializeProjectFactorStates( - kvStore, - industry, - QUICK_CONSULT_CATEGORY_FACTOR_KEY, - QUICK_MAJOR_FACTOR_KEY - ) + quickSubmitting.value = true + try { + const currentInfo = await kvStore.getItem(QUICK_PROJECT_INFO_KEY) + await kvStore.setItem(QUICK_PROJECT_INFO_KEY, { + ...currentInfo, + projectIndustry: industry, + projectName: t('quickCalc.projectName') + }) + await kvStore.setItem(QUICK_CONTRACT_META_KEY, { + id: QUICK_CONTRACT_ID, + name: contractName, + updatedAt: new Date().toISOString() + }) + if (industry) { + await initializeProjectFactorStates( + kvStore, + industry, + QUICK_CONSULT_CATEGORY_FACTOR_KEY, + QUICK_MAJOR_FACTOR_KEY + ) + } + enterQuickCalc(contractName) + } finally { + quickSubmitting.value = false } - enterQuickCalc(contractName) - } finally { - quickSubmitting.value = false - } + }) } const handleHomeImportChange = (event: Event) => { @@ -364,7 +397,33 @@ const handleHomeImportChange = (event: Event) => { } const openHomeImport = () => { - homeImportInputRef.value?.click() + void runWithDisclaimerGuard({ type: 'import' }, () => { + homeImportInputRef.value?.click() + }) +} + +const replayPendingDisclaimerAction = async () => { + if (!disclaimerRequired.value || !hasAcceptedRestrictedDisclaimer()) return + const pendingAction = consumePendingDisclaimerAction() + if (!pendingAction) return + if (pendingAction.type === 'project') { + await loadProjectDefaults() + projectDialogOpen.value = true + return + } + if (pendingAction.type === 'quick') { + await openQuickCalc() + return + } + if (pendingAction.type === 'import') { + homeImportInputRef.value?.click() + return + } + if (pendingAction.type === 'existing-project') { + existingProjectDialogOpen.value = true + await refreshExistingProjects() + startExistingProjectPolling() + } } const cancelHomeImportConfirm = () => { @@ -394,18 +453,16 @@ const handleHomeVisibilityChange = () => { } const openDisclaimerPage = () => { - try { - const href = new URL('disclaimer.html', window.location.href).toString() - window.open(href, '_blank', 'noopener') - } catch { - window.open('./disclaimer.html', '_blank', 'noopener') - } + const href = buildDisclaimerUrl(window.location.href) + window.open(href, '_blank', 'noopener') } onMounted(() => { + syncDisclaimerRequirement() void refreshExistingProjects() void loadProjectDefaults() void loadQuickDefaults() + void replayPendingDisclaimerAction() window.addEventListener('focus', handleHomeWindowFocus) document.addEventListener('visibilitychange', handleHomeVisibilityChange) try { diff --git a/src/layout/tab.vue b/src/layout/tab.vue index 05c5c33..020789f 100644 --- a/src/layout/tab.vue +++ b/src/layout/tab.vue @@ -96,6 +96,7 @@ import type { } from '@/features/tab/types' import { buildProjectUrl, + DISCLAIMER_ACCEPTANCE_STORAGE_KEY, getProjectDbName, readCurrentProjectId, PROJECT_TAB_ID, @@ -1706,6 +1707,8 @@ const handleReset = async () => { typeof localStorage !== 'undefined' ? localStorage.getItem(I18N_LOCALE_KEY) : null const uiPrefsSnapshot = typeof localStorage !== 'undefined' ? localStorage.getItem(UI_PREFS_STORAGE_KEY) : null + const disclaimerAcceptanceSnapshot = + typeof localStorage !== 'undefined' ? localStorage.getItem(DISCLAIMER_ACCEPTANCE_STORAGE_KEY) : null // 1) 仅清持久化层,不先改当前页面内存态,避免“先切首页再刷新”的二次闪烁。 localStorage.clear() @@ -1718,6 +1721,9 @@ const handleReset = async () => { if (uiPrefsSnapshot != null) { localStorage.setItem(UI_PREFS_STORAGE_KEY, uiPrefsSnapshot) } + if (disclaimerAcceptanceSnapshot != null) { + localStorage.setItem(DISCLAIMER_ACCEPTANCE_STORAGE_KEY, disclaimerAcceptanceSnapshot) + } sessionStorage.clear() await projectDefaultForage.clear() diff --git a/src/lib/workspace.ts b/src/lib/workspace.ts index fc1fb39..959cdf3 100644 --- a/src/lib/workspace.ts +++ b/src/lib/workspace.ts @@ -13,9 +13,15 @@ export const PROJECT_ID_QUERY_KEY = 'projectId' export const NEW_PROJECT_QUERY_KEY = 'newProject' export const OPEN_PROJECT_DIALOG_QUERY_KEY = 'openProjectDialog' export const FORCE_HOME_QUERY_KEY = 'forceHome' +export const DISCLAIMER_ENTRY_QUERY_KEY = 'from' +export const DISCLAIMER_ENTRY_QUERY_VALUE = 'gov' export const DEFAULT_PROJECT_ID = 'default' export const QUICK_PROJECT_ID = 'quick' export const PROJECT_DB_NAME_PREFIX = 'DB' +export const DISCLAIMER_ACCEPTANCE_STORAGE_KEY = 'jgjs-disclaimer-accepted-v1' +export const DISCLAIMER_ACCEPTED_EVENT = 'jgjs-disclaimer-accepted' +export const DISCLAIMER_PENDING_ACTION_STORAGE_KEY = 'jgjs-disclaimer-pending-action-v1' +export const DISCLAIMER_RETURN_URL_QUERY_KEY = 'returnUrl' export const QUICK_CONTRACT_ID = 'quick-contract-default' export const QUICK_CONTRACT_META_KEY = 'quick-contract-meta-v1' @@ -42,6 +48,11 @@ export interface QuickContractMeta { updatedAt: string } +export interface DisclaimerPendingAction { + type: 'project' | 'quick' | 'import' | 'existing-project' + projectId?: string +} + export const readWorkspaceMode = (): WorkspaceMode => { @@ -185,6 +196,102 @@ export const buildProjectUrl = ( } } +const readDisclaimerAcceptedEntries = () => { + try { + const raw = window.localStorage.getItem(DISCLAIMER_ACCEPTANCE_STORAGE_KEY) + if (!raw) return {} + const parsed = JSON.parse(raw) as Record + if (!parsed || typeof parsed !== 'object') return {} + return parsed + } catch { + return {} + } +} + +export const readRestrictedEntryCodeFromUrl = (href?: string | URL | null) => { + try { + const url = href instanceof URL ? href : new URL(href || window.location.href, window.location.href) + const entry = String(url.searchParams.get(DISCLAIMER_ENTRY_QUERY_KEY) || '').trim() + return entry + } catch { + return '' + } +} + +export const isRestrictedDisclaimerEntry = (entryRaw: string) => + String(entryRaw || '').trim() === DISCLAIMER_ENTRY_QUERY_VALUE + +export const isDisclaimerAcceptanceRequired = (href?: string | URL | null) => + isRestrictedDisclaimerEntry(readRestrictedEntryCodeFromUrl(href)) + +export const hasAcceptedRestrictedDisclaimer = (entryRaw?: string) => { + const entry = String(entryRaw || readRestrictedEntryCodeFromUrl() || '').trim() + if (!isRestrictedDisclaimerEntry(entry)) return false + const acceptedMap = readDisclaimerAcceptedEntries() + return acceptedMap[entry] === true +} + +export const persistRestrictedDisclaimerAcceptance = (entryRaw?: string) => { + const entry = String(entryRaw || readRestrictedEntryCodeFromUrl() || '').trim() + if (!isRestrictedDisclaimerEntry(entry)) return false + try { + const acceptedMap = readDisclaimerAcceptedEntries() + acceptedMap[entry] = true + window.localStorage.setItem(DISCLAIMER_ACCEPTANCE_STORAGE_KEY, JSON.stringify(acceptedMap)) + window.dispatchEvent(new CustomEvent(DISCLAIMER_ACCEPTED_EVENT, { + detail: { + entry + } + })) + return true + } catch { + return false + } +} + +export const setPendingDisclaimerAction = (action: DisclaimerPendingAction | null) => { + try { + if (!action) { + window.sessionStorage.removeItem(DISCLAIMER_PENDING_ACTION_STORAGE_KEY) + return + } + window.sessionStorage.setItem(DISCLAIMER_PENDING_ACTION_STORAGE_KEY, JSON.stringify(action)) + } catch { + // ignore session storage errors + } +} + +export const consumePendingDisclaimerAction = () => { + try { + const raw = window.sessionStorage.getItem(DISCLAIMER_PENDING_ACTION_STORAGE_KEY) + window.sessionStorage.removeItem(DISCLAIMER_PENDING_ACTION_STORAGE_KEY) + if (!raw) return null + const parsed = JSON.parse(raw) as DisclaimerPendingAction + if (!parsed || typeof parsed !== 'object') return null + const type = String(parsed.type || '').trim() + if (!type) return null + if (!['project', 'quick', 'import', 'existing-project'].includes(type)) return null + return { + type: type as DisclaimerPendingAction['type'], + projectId: typeof parsed.projectId === 'string' ? parsed.projectId.trim() : undefined + } + } catch { + return null + } +} + +export const buildDisclaimerUrl = (returnUrl?: string) => { + try { + const url = new URL('disclaimer.html', window.location.href) + if (returnUrl) { + url.searchParams.set(DISCLAIMER_RETURN_URL_QUERY_KEY, returnUrl) + } + return url.toString() + } catch { + return './disclaimer.html' + } +} + export const getProjectDbName = (projectIdRaw: string) => { const projectId = normalizeProjectId(projectIdRaw) return `${PROJECT_DB_NAME_PREFIX}-${projectId}`