免责确认

This commit is contained in:
zwgczx committed 2026-04-13 14:40:46 +08:00
1 parent aed6fe2bfa
commit aa67a66047
4 files changed
+500 -61

No files matched your search

@@ -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<File | null>(null)
const pendingHomeImportFileName = ref('')
const existingProjectDialogOpen = ref(false)
const disclaimerRequired = ref(false)
const existingProjects = ref<Array<{ id: string; name: string; updatedAt: string }>>([])
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<void>
) => {
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<QuickProjectInfoState>(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<QuickProjectInfoState>(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 {
+6
View File
@@ -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()
+107
View File
@@ -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<string, unknown>
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}`