299 lines
9.7 KiB
TypeScript
299 lines
9.7 KiB
TypeScript
import { i18n } from '@/i18n'
|
|
import localforage from 'localforage'
|
|
|
|
export type WorkspaceMode = 'project' | 'quick'
|
|
|
|
export const PROJECT_TAB_ID = 'ProjectCalcView'
|
|
export const QUICK_TAB_ID = 'QuickCalcView'
|
|
export const LEGACY_PROJECT_TAB_ID = 'ProjectCalcView'
|
|
export const FIXED_WORKSPACE_TAB_IDS = [PROJECT_TAB_ID, QUICK_TAB_ID] as const
|
|
|
|
export const WORKSPACE_MODE_STORAGE_KEY = 'jgjs-workspace-mode-v1'
|
|
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'
|
|
export const QUICK_CONTRACT_FALLBACK_NAME = i18n.global.t('quickCalc.projectName')
|
|
export const QUICK_CONTRACT_TAB_ID = `contract-${QUICK_CONTRACT_ID}`
|
|
|
|
export const QUICK_PROJECT_INFO_KEY = 'quick-xm-base-info-v1'
|
|
export const QUICK_PROJECT_SCALE_KEY = 'quick-xm-info-v3'
|
|
export const QUICK_CONSULT_CATEGORY_FACTOR_KEY = 'quick-xm-consult-category-factor-v1'
|
|
export const QUICK_MAJOR_FACTOR_KEY = 'quick-xm-major-factor-v1'
|
|
|
|
const HOME_IMPORT_TEMP_DB_NAME = 'JGJS-HOME-IMPORT-TEMP'
|
|
const HOME_IMPORT_TEMP_STORE_NAME = 'home-import'
|
|
const HOME_IMPORT_TEMP_FILE_KEY = 'pending-file'
|
|
const HOME_IMPORT_SKIP_CONFIRM_KEY = 'jgjs-home-import-skip-confirm'
|
|
const homeImportTempForage = localforage.createInstance({
|
|
name: HOME_IMPORT_TEMP_DB_NAME,
|
|
storeName: HOME_IMPORT_TEMP_STORE_NAME
|
|
})
|
|
|
|
export interface QuickContractMeta {
|
|
id: string
|
|
name: string
|
|
updatedAt: string
|
|
}
|
|
|
|
export interface DisclaimerPendingAction {
|
|
type: 'project' | 'quick' | 'import' | 'existing-project'
|
|
projectId?: string
|
|
}
|
|
|
|
|
|
|
|
export const readWorkspaceMode = (): WorkspaceMode => {
|
|
try {
|
|
return window.localStorage.getItem(WORKSPACE_MODE_STORAGE_KEY) as WorkspaceMode
|
|
} catch {
|
|
return 'project'
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export const createDefaultQuickContractMeta = (): QuickContractMeta => ({
|
|
id: QUICK_CONTRACT_ID,
|
|
name: QUICK_CONTRACT_FALLBACK_NAME,
|
|
updatedAt: new Date().toISOString()
|
|
})
|
|
export const writeWorkspaceMode = (mode: WorkspaceMode) => {
|
|
try {
|
|
window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, mode)
|
|
} catch {
|
|
// 忽略只读或隐私模式下的写入失败。
|
|
}
|
|
}
|
|
|
|
export const setPendingHomeImportFile = (
|
|
file: File | null,
|
|
options?: { skipWorkspaceConfirm?: boolean }
|
|
) => {
|
|
return (async () => {
|
|
try {
|
|
if (file) {
|
|
await homeImportTempForage.setItem(HOME_IMPORT_TEMP_FILE_KEY, {
|
|
name: file.name,
|
|
type: file.type,
|
|
lastModified: file.lastModified,
|
|
blob: file
|
|
})
|
|
} else {
|
|
await homeImportTempForage.removeItem(HOME_IMPORT_TEMP_FILE_KEY)
|
|
}
|
|
} catch {
|
|
// ignore temp file persistence errors
|
|
}
|
|
try {
|
|
window.sessionStorage.setItem(HOME_IMPORT_SKIP_CONFIRM_KEY, options?.skipWorkspaceConfirm ? '1' : '0')
|
|
} catch {
|
|
// ignore session storage errors
|
|
}
|
|
})()
|
|
}
|
|
|
|
export const consumePendingHomeImportFile = async () => {
|
|
try {
|
|
const payload = await homeImportTempForage.getItem<{
|
|
name?: string
|
|
type?: string
|
|
lastModified?: number
|
|
blob?: Blob
|
|
}>(HOME_IMPORT_TEMP_FILE_KEY)
|
|
await homeImportTempForage.removeItem(HOME_IMPORT_TEMP_FILE_KEY)
|
|
if (!payload?.blob) return null
|
|
const name = String(payload.name || 'import.zw').trim() || 'import.zw'
|
|
return new File([payload.blob], name, {
|
|
type: typeof payload.type === 'string' ? payload.type : '',
|
|
lastModified: typeof payload.lastModified === 'number' ? payload.lastModified : Date.now()
|
|
})
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const consumePendingHomeImportSkipConfirm = () => {
|
|
try {
|
|
const skip = window.sessionStorage.getItem(HOME_IMPORT_SKIP_CONFIRM_KEY) === '1'
|
|
window.sessionStorage.removeItem(HOME_IMPORT_SKIP_CONFIRM_KEY)
|
|
return skip
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export const normalizeProjectId = (value: unknown) => {
|
|
const raw = String(value || '').trim()
|
|
if (!raw) return DEFAULT_PROJECT_ID
|
|
return raw.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64) || DEFAULT_PROJECT_ID
|
|
}
|
|
|
|
export const readCurrentProjectId = () => {
|
|
try {
|
|
const url = new URL(window.location.href)
|
|
return normalizeProjectId(url.searchParams.get(PROJECT_ID_QUERY_KEY))
|
|
} catch {
|
|
return DEFAULT_PROJECT_ID
|
|
}
|
|
}
|
|
|
|
export const writeProjectIdToUrl = (projectIdRaw: string) => {
|
|
try {
|
|
const projectId = normalizeProjectId(projectIdRaw)
|
|
const url = new URL(window.location.href)
|
|
url.searchParams.set(PROJECT_ID_QUERY_KEY, projectId)
|
|
window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`)
|
|
} catch (error) {
|
|
console.error('write project id to url failed:', error)
|
|
}
|
|
}
|
|
|
|
export const ensureProjectIdInUrl = () => {
|
|
const id = readCurrentProjectId()
|
|
writeProjectIdToUrl(id)
|
|
return id
|
|
}
|
|
|
|
export const buildProjectUrl = (
|
|
projectIdRaw: string,
|
|
options?: { newProject?: boolean; openProjectDialog?: boolean; forceHome?: boolean }
|
|
) => {
|
|
try {
|
|
const url = new URL(window.location.href)
|
|
url.searchParams.set(PROJECT_ID_QUERY_KEY, normalizeProjectId(projectIdRaw))
|
|
if (options?.newProject) {
|
|
url.searchParams.set(NEW_PROJECT_QUERY_KEY, '1')
|
|
if (options?.openProjectDialog === false) {
|
|
url.searchParams.set(OPEN_PROJECT_DIALOG_QUERY_KEY, '0')
|
|
} else {
|
|
url.searchParams.delete(OPEN_PROJECT_DIALOG_QUERY_KEY)
|
|
}
|
|
} else {
|
|
url.searchParams.delete(NEW_PROJECT_QUERY_KEY)
|
|
url.searchParams.delete(OPEN_PROJECT_DIALOG_QUERY_KEY)
|
|
}
|
|
if (options?.forceHome) {
|
|
url.searchParams.set(FORCE_HOME_QUERY_KEY, '1')
|
|
} else {
|
|
url.searchParams.delete(FORCE_HOME_QUERY_KEY)
|
|
}
|
|
return `${url.pathname}${url.search}${url.hash}`
|
|
} catch {
|
|
return `/?${PROJECT_ID_QUERY_KEY}=${encodeURIComponent(normalizeProjectId(projectIdRaw))}`
|
|
}
|
|
}
|
|
|
|
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}`
|
|
}
|