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 DEFAULT_PROJECT_ID = 'default' export const QUICK_PROJECT_ID = 'quick' export const PROJECT_DB_NAME_PREFIX = 'DB' 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 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))}` } } export const getProjectDbName = (projectIdRaw: string) => { const projectId = normalizeProjectId(projectIdRaw) return `${PROJECT_DB_NAME_PREFIX}-${projectId}` }