免责确认
This commit is contained in:
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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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}`
|
||||
|
||||
Reference in new issue
Block a user