2026-07-30 16:04:34 +08:00
|
|
|
|
import { getPoiRepository } from '@/data/poi'
|
|
|
|
|
|
import {
|
|
|
|
|
|
clonePlanResponse,
|
|
|
|
|
|
cloneTravelPreferences,
|
|
|
|
|
|
normalizePlanResponse,
|
|
|
|
|
|
normalizeTravelPreferences,
|
2026-07-30 16:33:39 +08:00
|
|
|
|
type PlanningRequest,
|
2026-07-30 16:04:34 +08:00
|
|
|
|
type PlanResponse,
|
|
|
|
|
|
type TravelPreferences,
|
|
|
|
|
|
} from '@/domain/travel'
|
|
|
|
|
|
|
|
|
|
|
|
export const PLAN_STORAGE_KEY = 'guangming:last-plan'
|
|
|
|
|
|
export const PREFERENCES_STORAGE_KEY = 'guangming:last-preferences'
|
2026-07-30 16:33:39 +08:00
|
|
|
|
export const PLANNING_REQUEST_STORAGE_KEY = 'guangming:last-planning-request'
|
2026-07-30 16:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
const STORAGE_SCHEMA_VERSION = 1
|
|
|
|
|
|
|
|
|
|
|
|
interface StorageEnvelope<T> {
|
|
|
|
|
|
schemaVersion: typeof STORAGE_SCHEMA_VERSION
|
|
|
|
|
|
datasetVersion: string
|
|
|
|
|
|
savedAt: string
|
|
|
|
|
|
payload: T
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createEnvelope<T>(payload: T): StorageEnvelope<T> {
|
|
|
|
|
|
return {
|
|
|
|
|
|
schemaVersion: STORAGE_SCHEMA_VERSION,
|
|
|
|
|
|
datasetVersion: getPoiRepository().getDatasetMeta().datasetVersion,
|
|
|
|
|
|
savedAt: new Date().toISOString(),
|
|
|
|
|
|
payload,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readEnvelope(value: unknown): StorageEnvelope<unknown> | null {
|
|
|
|
|
|
if (!isRecord(value) || value.schemaVersion !== STORAGE_SCHEMA_VERSION)
|
|
|
|
|
|
return null
|
|
|
|
|
|
if (typeof value.datasetVersion !== 'string' || !value.datasetVersion.trim())
|
|
|
|
|
|
return null
|
|
|
|
|
|
if (typeof value.savedAt !== 'string' || !Number.isFinite(Date.parse(value.savedAt)))
|
|
|
|
|
|
return null
|
|
|
|
|
|
if (!('payload' in value))
|
|
|
|
|
|
return null
|
|
|
|
|
|
return value as unknown as StorageEnvelope<unknown>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readStorage(key: string): unknown {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return uni.getStorageSync(key)
|
|
|
|
|
|
}
|
|
|
|
|
|
catch {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function writeStorage(key: string, value: unknown): void {
|
|
|
|
|
|
try {
|
|
|
|
|
|
uni.setStorageSync(key, value)
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
const reason = error instanceof Error && error.message ? `:${error.message}` : ''
|
|
|
|
|
|
throw new Error(`无法保存本机行程数据${reason}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function savePlan(plan: PlanResponse): void {
|
|
|
|
|
|
const repository = getPoiRepository()
|
|
|
|
|
|
const normalized = normalizePlanResponse(plan, repository)
|
|
|
|
|
|
writeStorage(PLAN_STORAGE_KEY, createEnvelope(clonePlanResponse(normalized, repository)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function loadPlan(): PlanResponse | null {
|
|
|
|
|
|
const repository = getPoiRepository()
|
|
|
|
|
|
const envelope = readEnvelope(readStorage(PLAN_STORAGE_KEY))
|
|
|
|
|
|
if (!envelope || envelope.datasetVersion !== repository.getDatasetMeta().datasetVersion)
|
|
|
|
|
|
return null
|
|
|
|
|
|
try {
|
|
|
|
|
|
return normalizePlanResponse(envelope.payload, repository)
|
|
|
|
|
|
}
|
|
|
|
|
|
catch {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function savePreferences(preferences: TravelPreferences): void {
|
|
|
|
|
|
const normalized = normalizeTravelPreferences(preferences)
|
|
|
|
|
|
writeStorage(PREFERENCES_STORAGE_KEY, createEnvelope(cloneTravelPreferences(normalized)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function loadPreferences(): TravelPreferences | null {
|
|
|
|
|
|
const envelope = readEnvelope(readStorage(PREFERENCES_STORAGE_KEY))
|
|
|
|
|
|
if (!envelope)
|
|
|
|
|
|
return null
|
|
|
|
|
|
try {
|
|
|
|
|
|
return normalizeTravelPreferences(envelope.payload)
|
|
|
|
|
|
}
|
|
|
|
|
|
catch {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-30 16:33:39 +08:00
|
|
|
|
|
|
|
|
|
|
export function savePlanningRequest(request: PlanningRequest): void {
|
|
|
|
|
|
const repository = getPoiRepository()
|
|
|
|
|
|
const sanitized: PlanningRequest = {
|
|
|
|
|
|
mode: request.mode,
|
|
|
|
|
|
preferences: normalizeTravelPreferences(request.preferences),
|
|
|
|
|
|
durationMinutes: request.durationMinutes,
|
|
|
|
|
|
selectedPoiIds: [...request.selectedPoiIds],
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sanitized.mode !== 'quick' && sanitized.mode !== 'custom')
|
|
|
|
|
|
throw new Error('无法保存规划条件:规划模式无效')
|
|
|
|
|
|
if (!Number.isInteger(sanitized.durationMinutes)
|
|
|
|
|
|
|| sanitized.durationMinutes < 120
|
|
|
|
|
|
|| sanitized.durationMinutes > 600
|
|
|
|
|
|
|| sanitized.durationMinutes % 30 !== 0) {
|
|
|
|
|
|
throw new Error('无法保存规划条件:规划时长无效')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sanitized.mode === 'quick' && sanitized.selectedPoiIds.length > 0)
|
|
|
|
|
|
throw new Error('无法保存规划条件:快速规划不能预选点位')
|
|
|
|
|
|
if (sanitized.mode === 'custom'
|
|
|
|
|
|
&& (sanitized.selectedPoiIds.length < 2 || sanitized.selectedPoiIds.length > 8)) {
|
|
|
|
|
|
throw new Error('无法保存规划条件:自选点位数量无效')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (new Set(sanitized.selectedPoiIds).size !== sanitized.selectedPoiIds.length
|
|
|
|
|
|
|| sanitized.selectedPoiIds.some(poiId => !repository.getPoiById(poiId))) {
|
|
|
|
|
|
throw new Error('无法保存规划条件:自选点位无效')
|
|
|
|
|
|
}
|
|
|
|
|
|
writeStorage(PLANNING_REQUEST_STORAGE_KEY, createEnvelope(sanitized))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function loadPlanningRequest(): PlanningRequest | null {
|
|
|
|
|
|
const repository = getPoiRepository()
|
|
|
|
|
|
const envelope = readEnvelope(readStorage(PLANNING_REQUEST_STORAGE_KEY))
|
|
|
|
|
|
if (!envelope || envelope.datasetVersion !== repository.getDatasetMeta().datasetVersion || !isRecord(envelope.payload))
|
|
|
|
|
|
return null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = envelope.payload
|
|
|
|
|
|
const request: PlanningRequest = {
|
|
|
|
|
|
mode: payload.mode as PlanningRequest['mode'],
|
|
|
|
|
|
preferences: normalizeTravelPreferences(payload.preferences),
|
|
|
|
|
|
durationMinutes: Number(payload.durationMinutes),
|
|
|
|
|
|
selectedPoiIds: Array.isArray(payload.selectedPoiIds) ? payload.selectedPoiIds.map(String) : [],
|
|
|
|
|
|
}
|
|
|
|
|
|
if (request.mode !== 'quick' && request.mode !== 'custom')
|
|
|
|
|
|
return null
|
|
|
|
|
|
if (!Number.isInteger(request.durationMinutes)
|
|
|
|
|
|
|| request.durationMinutes < 120
|
|
|
|
|
|
|| request.durationMinutes > 600
|
|
|
|
|
|
|| request.durationMinutes % 30 !== 0) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (request.mode === 'quick' && request.selectedPoiIds.length > 0)
|
|
|
|
|
|
return null
|
|
|
|
|
|
if (request.mode === 'custom' && (request.selectedPoiIds.length < 2 || request.selectedPoiIds.length > 8))
|
|
|
|
|
|
return null
|
|
|
|
|
|
if (new Set(request.selectedPoiIds).size !== request.selectedPoiIds.length
|
|
|
|
|
|
|| request.selectedPoiIds.some(poiId => !repository.getPoiById(poiId))) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return request
|
|
|
|
|
|
}
|
|
|
|
|
|
catch {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|