Initial commit: gmTouringMiniApp project
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from './marker'
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { PoiCategory, PoiSummary } from '@/domain/poi'
|
||||
|
||||
export interface PoiMapMarker {
|
||||
id: number
|
||||
latitude: number
|
||||
longitude: number
|
||||
iconPath: string
|
||||
width: number
|
||||
height: number
|
||||
anchor: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
callout: {
|
||||
content: string
|
||||
color: string
|
||||
fontSize: number
|
||||
borderRadius: number
|
||||
bgColor: string
|
||||
padding: number
|
||||
display: 'ALWAYS' | 'BYCLICK'
|
||||
textAlign: 'center'
|
||||
}
|
||||
zIndex: number
|
||||
}
|
||||
|
||||
function stableHash(input: string): number {
|
||||
let hash = 2166136261
|
||||
for (let index = 0; index < input.length; index += 1) {
|
||||
hash ^= input.charCodeAt(index)
|
||||
hash = Math.imul(hash, 16777619)
|
||||
}
|
||||
|
||||
return hash >>> 0
|
||||
}
|
||||
|
||||
/** Builds collision-free numeric marker IDs without coupling IDs to filter order. */
|
||||
export function buildMarkerIdMap(poiIds: readonly string[]): Map<number, string> {
|
||||
const result = new Map<number, string>()
|
||||
const sortedIds = Array.from(new Set(poiIds)).sort()
|
||||
|
||||
sortedIds.forEach((poiId) => {
|
||||
let markerId = stableHash(poiId) & 0x7FFFFFFF
|
||||
if (markerId === 0)
|
||||
markerId = 1
|
||||
|
||||
while (result.has(markerId))
|
||||
markerId = markerId === 0x7FFFFFFF ? 1 : markerId + 1
|
||||
|
||||
result.set(markerId, poiId)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function rpxToPx(rpx: number): number {
|
||||
return typeof uni !== 'undefined' && typeof uni.upx2px === 'function'
|
||||
? uni.upx2px(rpx)
|
||||
: Math.round(rpx / 2)
|
||||
}
|
||||
|
||||
function toMarkerDimensions(selected: boolean): { width: number, height: number } {
|
||||
const size = selected
|
||||
? { width: 80, height: 94 }
|
||||
: { width: 64, height: 78 }
|
||||
|
||||
return {
|
||||
width: rpxToPx(size.width),
|
||||
height: rpxToPx(size.height),
|
||||
}
|
||||
}
|
||||
|
||||
export function createPoiMarkers(
|
||||
pois: readonly PoiSummary[],
|
||||
categories: readonly PoiCategory[],
|
||||
markerIdMap: ReadonlyMap<number, string>,
|
||||
selectedPoiId: string | null,
|
||||
): PoiMapMarker[] {
|
||||
const categoryMap = new Map(categories.map(category => [category.code, category]))
|
||||
const idByPoi = new Map(Array.from(markerIdMap.entries()).map(([markerId, poiId]) => [poiId, markerId]))
|
||||
|
||||
return pois.flatMap((poi) => {
|
||||
const category = categoryMap.get(poi.categoryCode)
|
||||
const markerId = idByPoi.get(poi.id)
|
||||
if (!category || markerId === undefined)
|
||||
return []
|
||||
|
||||
const selected = poi.id === selectedPoiId
|
||||
const size = toMarkerDimensions(selected)
|
||||
return [{
|
||||
id: markerId,
|
||||
latitude: poi.latitude,
|
||||
longitude: poi.longitude,
|
||||
iconPath: selected ? category.selectedMarkerIcon : category.markerIcon,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
anchor: { x: 0.5, y: 1 },
|
||||
callout: {
|
||||
content: selected ? `${category.name} · ${poi.name}` : category.name,
|
||||
color: selected ? '#FFFFFF' : '#18201D',
|
||||
fontSize: 12,
|
||||
borderRadius: 8,
|
||||
bgColor: selected ? '#0E6247' : '#FFFFFF',
|
||||
padding: 6,
|
||||
display: selected ? 'ALWAYS' : 'BYCLICK',
|
||||
textAlign: 'center',
|
||||
},
|
||||
zIndex: selected ? 10 : 1,
|
||||
}]
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export { isRemoteTravelAssistantEnabled } from './remote'
|
||||
export { adjustPlan, createPlan } from './service'
|
||||
export {
|
||||
loadPlan,
|
||||
loadPreferences,
|
||||
PLAN_STORAGE_KEY,
|
||||
PREFERENCES_STORAGE_KEY,
|
||||
savePlan,
|
||||
savePreferences,
|
||||
} from './storage'
|
||||
|
||||
export type {
|
||||
BudgetLevel,
|
||||
Duration,
|
||||
Interest,
|
||||
Itinerary,
|
||||
ItineraryItem,
|
||||
ItinerarySource,
|
||||
Pace,
|
||||
PlanningOrigin,
|
||||
PlanResponse,
|
||||
Theme,
|
||||
Transport,
|
||||
TravelAssistantSource,
|
||||
TravelPreferences,
|
||||
} from '@/domain/travel'
|
||||
@@ -0,0 +1,210 @@
|
||||
import type { PlanningRequest } from '@/domain/travel'
|
||||
|
||||
interface RemoteAPIError {
|
||||
detail?: unknown
|
||||
}
|
||||
|
||||
export interface RemotePoiCandidate {
|
||||
id: string
|
||||
name: string
|
||||
categoryCode: string
|
||||
tagCodes: string[]
|
||||
summary: string
|
||||
recommendationIndex: number
|
||||
}
|
||||
|
||||
export interface RemotePoiCandidateInput {
|
||||
id: string
|
||||
name: string
|
||||
categoryCode: string
|
||||
tagCodes: readonly string[]
|
||||
summary: string
|
||||
recommendationIndex: number
|
||||
}
|
||||
|
||||
export interface RemoteRecommendationItem {
|
||||
poiId: string
|
||||
reason: string
|
||||
order: number
|
||||
}
|
||||
|
||||
export interface RemoteRecommendationResponse {
|
||||
requestId: string
|
||||
mode: PlanningRequest['mode']
|
||||
assistantMessage: string
|
||||
recommendations: RemoteRecommendationItem[]
|
||||
generationMode: 'mock' | 'real'
|
||||
}
|
||||
|
||||
function apiBaseUrl(): string {
|
||||
const value = import.meta.env.VITE_TRAVEL_ASSISTANT_API_BASE_URL
|
||||
return typeof value === 'string' ? value.trim().replace(/\/+$/, '') : ''
|
||||
}
|
||||
|
||||
export function isRemoteTravelAssistantEnabled(): boolean {
|
||||
return apiBaseUrl().length > 0
|
||||
}
|
||||
|
||||
function remoteUrl(path: string): string {
|
||||
const baseUrl = apiBaseUrl()
|
||||
if (!/^https?:\/\//i.test(baseUrl))
|
||||
throw new Error('VITE_TRAVEL_ASSISTANT_API_BASE_URL 必须是有效的 HTTP(S) 地址')
|
||||
return `${baseUrl}${path}`
|
||||
}
|
||||
|
||||
function errorDetail(data: unknown, statusCode: number): string {
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
const detail = (data as RemoteAPIError).detail
|
||||
if (typeof detail === 'string' && detail.trim())
|
||||
return detail.trim()
|
||||
}
|
||||
return `行程服务请求失败(${statusCode})`
|
||||
}
|
||||
|
||||
function request<T>(options: UniApp.RequestOptions): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
...options,
|
||||
success(response) {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
resolve(response.data as T)
|
||||
return
|
||||
}
|
||||
reject(new Error(errorDetail(response.data, response.statusCode)))
|
||||
},
|
||||
fail() {
|
||||
reject(new Error('无法连接行程服务,请检查网络、服务地址和微信合法域名配置'))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function readNonEmptyString(value: unknown, fieldName: string, maxLength: number): string {
|
||||
if (typeof value !== 'string' || !value.trim() || value.trim().length > maxLength)
|
||||
throw new Error(`AI 推荐响应中的${fieldName}格式无效`)
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
function safeCandidates(candidates: readonly RemotePoiCandidateInput[]): RemotePoiCandidate[] {
|
||||
if (candidates.length < 1 || candidates.length > 100)
|
||||
throw new Error('发送给 AI 的 POI 候选数量必须为 1 至 100 个')
|
||||
|
||||
const ids = new Set<string>()
|
||||
return candidates.map((candidate) => {
|
||||
const id = candidate.id.trim()
|
||||
if (!id || ids.has(id))
|
||||
throw new Error(`发送给 AI 的 POI ID 无效或重复:${id || '空值'}`)
|
||||
const name = candidate.name.trim()
|
||||
const categoryCode = candidate.categoryCode.trim()
|
||||
const summary = candidate.summary.trim()
|
||||
if (!name || name.length > 100)
|
||||
throw new Error(`发送给 AI 的 POI 名称无效:${id}`)
|
||||
if (!categoryCode || categoryCode.length > 64)
|
||||
throw new Error(`发送给 AI 的 POI 分类无效:${id}`)
|
||||
if (summary.length > 300)
|
||||
throw new Error(`发送给 AI 的 POI 摘要过长:${id}`)
|
||||
if (!Number.isInteger(candidate.recommendationIndex)
|
||||
|| candidate.recommendationIndex < 1
|
||||
|| candidate.recommendationIndex > 5) {
|
||||
throw new Error(`发送给 AI 的 POI 推荐指数无效:${id}`)
|
||||
}
|
||||
const tagCodes = [...new Set(candidate.tagCodes.map(tagCode => tagCode.trim()).filter(Boolean))]
|
||||
if (tagCodes.length > 20 || tagCodes.some(tagCode => tagCode.length > 64))
|
||||
throw new Error(`发送给 AI 的 POI 标签无效:${id}`)
|
||||
ids.add(id)
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
categoryCode,
|
||||
tagCodes,
|
||||
summary,
|
||||
recommendationIndex: candidate.recommendationIndex,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeRecommendations(
|
||||
value: unknown,
|
||||
planningRequest: PlanningRequest,
|
||||
candidates: readonly RemotePoiCandidate[],
|
||||
): RemoteRecommendationResponse {
|
||||
if (!isRecord(value))
|
||||
throw new Error('AI 推荐响应格式无效')
|
||||
if (value.mode !== planningRequest.mode)
|
||||
throw new Error('AI 推荐响应的规划模式不匹配')
|
||||
if (value.generationMode !== 'mock' && value.generationMode !== 'real')
|
||||
throw new Error('AI 推荐响应的生成模式无效')
|
||||
if (!Array.isArray(value.recommendations) || value.recommendations.length < 1 || value.recommendations.length > 8)
|
||||
throw new Error('AI 推荐响应必须包含 1 至 8 个 POI')
|
||||
|
||||
const allowedIds = new Set(candidates.map(candidate => candidate.id))
|
||||
const seenIds = new Set<string>()
|
||||
const seenOrders = new Set<number>()
|
||||
const recommendations = value.recommendations.map((item): RemoteRecommendationItem => {
|
||||
if (!isRecord(item))
|
||||
throw new Error('AI 推荐项格式无效')
|
||||
const poiId = readNonEmptyString(item.poiId, 'POI ID', 100)
|
||||
if (!allowedIds.has(poiId))
|
||||
throw new Error(`AI 返回了候选白名单外的 POI:${poiId}`)
|
||||
if (seenIds.has(poiId))
|
||||
throw new Error(`AI 返回了重复 POI:${poiId}`)
|
||||
if (!Number.isInteger(item.order) || (item.order as number) < 1 || (item.order as number) > 8)
|
||||
throw new Error('AI 推荐响应中的顺序格式无效')
|
||||
if (seenOrders.has(item.order as number))
|
||||
throw new Error('AI 推荐响应中存在重复顺序')
|
||||
seenIds.add(poiId)
|
||||
seenOrders.add(item.order as number)
|
||||
return {
|
||||
poiId,
|
||||
reason: readNonEmptyString(item.reason, '推荐理由', 300),
|
||||
order: item.order as number,
|
||||
}
|
||||
}).sort((left, right) => left.order - right.order)
|
||||
|
||||
if (recommendations.some((item, index) => item.order !== index + 1))
|
||||
throw new Error('AI 推荐响应中的顺序必须从 1 连续递增')
|
||||
if (planningRequest.mode === 'custom') {
|
||||
const selectedIds = new Set(planningRequest.selectedPoiIds)
|
||||
if (recommendations.length !== selectedIds.size
|
||||
|| recommendations.some(item => !selectedIds.has(item.poiId))) {
|
||||
throw new Error('AI 自选规划未完整保留用户选择的 POI')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requestId: readNonEmptyString(value.requestId, '请求 ID', 200),
|
||||
mode: planningRequest.mode,
|
||||
assistantMessage: readNonEmptyString(value.assistantMessage, '助手消息', 1000),
|
||||
recommendations,
|
||||
generationMode: value.generationMode,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests coordinate-free POI recommendations. The caller must calculate the
|
||||
* final route and duration locally; planningRequest.origin is never uploaded.
|
||||
*/
|
||||
export async function createRemoteRecommendations(
|
||||
planningRequest: PlanningRequest,
|
||||
candidateInputs: readonly RemotePoiCandidateInput[],
|
||||
): Promise<RemoteRecommendationResponse> {
|
||||
const candidates = safeCandidates(candidateInputs)
|
||||
const response = await request<unknown>({
|
||||
url: remoteUrl('/api/v1/plans'),
|
||||
method: 'POST',
|
||||
data: {
|
||||
mode: planningRequest.mode,
|
||||
preferences: planningRequest.preferences,
|
||||
durationMinutes: planningRequest.durationMinutes,
|
||||
selectedPoiIds: [...planningRequest.selectedPoiIds],
|
||||
candidates,
|
||||
},
|
||||
header: { 'content-type': 'application/json' },
|
||||
timeout: 50000,
|
||||
})
|
||||
return normalizeRecommendations(response, planningRequest, candidates)
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { getPoiRepository } from '@/data/poi'
|
||||
import {
|
||||
adjustLocalPlan,
|
||||
clonePlanResponse,
|
||||
cloneTravelPreferences,
|
||||
createLocalPlan,
|
||||
createRecommendedPlan,
|
||||
normalizePlanResponse,
|
||||
normalizePlanningRequest,
|
||||
normalizeTravelPreferences,
|
||||
type PlanningOrigin,
|
||||
type PlanningRequest,
|
||||
type PlanResponse,
|
||||
type TravelPreferences,
|
||||
TravelValidationError,
|
||||
} from '@/domain/travel'
|
||||
import { createRemoteRecommendations, isRemoteTravelAssistantEnabled } from './remote'
|
||||
import { loadPlan, loadPreferences, savePreferences } from './storage'
|
||||
|
||||
interface LocalSession {
|
||||
plan: PlanResponse
|
||||
preferences: TravelPreferences
|
||||
request: PlanningRequest
|
||||
planningOrigin: PlanningOrigin | null
|
||||
}
|
||||
|
||||
const localSessions = new Map<string, LocalSession>()
|
||||
|
||||
function clonePlanningRequest(request: PlanningRequest, includeOrigin = true): PlanningRequest {
|
||||
return {
|
||||
mode: request.mode,
|
||||
preferences: cloneTravelPreferences(request.preferences),
|
||||
durationMinutes: request.durationMinutes,
|
||||
selectedPoiIds: [...request.selectedPoiIds],
|
||||
...(includeOrigin && request.origin ? { origin: { ...request.origin } } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function rememberLocalSession(plan: PlanResponse, request: PlanningRequest): void {
|
||||
const repository = getPoiRepository()
|
||||
localSessions.set(plan.conversationId, {
|
||||
plan: clonePlanResponse(plan, repository),
|
||||
preferences: cloneTravelPreferences(request.preferences),
|
||||
request: clonePlanningRequest(request),
|
||||
planningOrigin: request.origin ? { ...request.origin } : null,
|
||||
})
|
||||
}
|
||||
|
||||
function restoreLocalSession(conversationId: string): LocalSession | null {
|
||||
const remembered = localSessions.get(conversationId)
|
||||
if (remembered)
|
||||
return remembered
|
||||
|
||||
const plan = loadPlan()
|
||||
const preferences = loadPreferences()
|
||||
if (!plan || !preferences || plan.conversationId !== conversationId)
|
||||
return null
|
||||
rememberLocalSession(plan, {
|
||||
mode: plan.itinerary.planningMode,
|
||||
preferences,
|
||||
durationMinutes: plan.itinerary.requestedMinutes,
|
||||
selectedPoiIds: plan.itinerary.planningMode === 'custom'
|
||||
? plan.itinerary.items.map(item => item.placeId)
|
||||
: [],
|
||||
})
|
||||
return localSessions.get(conversationId) ?? null
|
||||
}
|
||||
|
||||
function normalizePlanningOrigin(origin?: PlanningOrigin): PlanningOrigin | null {
|
||||
if (!origin
|
||||
|| origin.coordinateSystem !== 'GCJ02'
|
||||
|| !Number.isFinite(origin.longitude)
|
||||
|| !Number.isFinite(origin.latitude)
|
||||
|| origin.longitude < -180
|
||||
|| origin.longitude > 180
|
||||
|| origin.latitude < -90
|
||||
|| origin.latitude > 90) {
|
||||
return null
|
||||
}
|
||||
return { ...origin }
|
||||
}
|
||||
|
||||
function isPlanningRequest(input: TravelPreferences | PlanningRequest): input is PlanningRequest {
|
||||
return typeof input === 'object' && input !== null && 'mode' in input && 'preferences' in input
|
||||
}
|
||||
|
||||
export function createPlan(input: PlanningRequest): Promise<PlanResponse>
|
||||
export function createPlan(input: TravelPreferences, planningOrigin?: PlanningOrigin): Promise<PlanResponse>
|
||||
export async function createPlan(
|
||||
input: TravelPreferences | PlanningRequest,
|
||||
planningOrigin?: PlanningOrigin,
|
||||
): Promise<PlanResponse> {
|
||||
const repository = getPoiRepository()
|
||||
const request = normalizePlanningRequest(isPlanningRequest(input)
|
||||
? input
|
||||
: {
|
||||
mode: 'quick',
|
||||
preferences: normalizeTravelPreferences(input),
|
||||
durationMinutes: input.duration === 'half_day' ? 240 : 480,
|
||||
selectedPoiIds: [],
|
||||
...(normalizePlanningOrigin(planningOrigin) ? { origin: normalizePlanningOrigin(planningOrigin)! } : {}),
|
||||
}, repository)
|
||||
if (isRemoteTravelAssistantEnabled()) {
|
||||
const candidates = repository.getPublishedPois().map(poi => ({
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
categoryCode: poi.categoryCode,
|
||||
tagCodes: poi.tagCodes,
|
||||
summary: poi.summary,
|
||||
recommendationIndex: poi.recommendationIndex,
|
||||
}))
|
||||
const remote = await createRemoteRecommendations(request, candidates)
|
||||
const plan = createRecommendedPlan(
|
||||
request,
|
||||
remote.recommendations,
|
||||
repository,
|
||||
remote.assistantMessage,
|
||||
remote.generationMode === 'real' ? 'remote_ai' : 'remote_mock',
|
||||
)
|
||||
rememberLocalSession(plan, request)
|
||||
return clonePlanResponse(plan, repository)
|
||||
}
|
||||
|
||||
const plan = createLocalPlan(request, repository)
|
||||
rememberLocalSession(plan, request)
|
||||
return clonePlanResponse(plan, repository)
|
||||
}
|
||||
|
||||
export async function adjustPlan(conversationId: string, message: string): Promise<PlanResponse> {
|
||||
const normalizedConversationId = conversationId.trim()
|
||||
const normalizedMessage = message.trim()
|
||||
if (!normalizedConversationId)
|
||||
throw new TravelValidationError('会话 ID 不能为空')
|
||||
if (!normalizedMessage)
|
||||
throw new TravelValidationError('请填写需要调整的内容')
|
||||
if (normalizedMessage.length > 500)
|
||||
throw new TravelValidationError('调整内容不能超过 500 字')
|
||||
|
||||
const repository = getPoiRepository()
|
||||
const session = restoreLocalSession(normalizedConversationId)
|
||||
if (!session)
|
||||
throw new Error('本机行程会话不存在,请重新生成行程')
|
||||
const adjusted = adjustLocalPlan(
|
||||
session.plan,
|
||||
session.preferences,
|
||||
normalizedMessage,
|
||||
repository,
|
||||
session.planningOrigin ?? undefined,
|
||||
session.request,
|
||||
)
|
||||
if (session.plan.source !== 'local_poc') {
|
||||
adjusted.plan.assistantMessage = '已在本机按你的补充要求重新核算路线;如需 AI 重新选点,请返回规划页重新生成。'
|
||||
adjusted.plan.source = session.plan.source
|
||||
}
|
||||
rememberLocalSession(adjusted.plan, adjusted.request)
|
||||
savePreferences(adjusted.preferences)
|
||||
return clonePlanResponse(adjusted.plan, repository)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { getPoiRepository } from '@/data/poi'
|
||||
import {
|
||||
clonePlanResponse,
|
||||
cloneTravelPreferences,
|
||||
normalizePlanResponse,
|
||||
normalizeTravelPreferences,
|
||||
type PlanResponse,
|
||||
type TravelPreferences,
|
||||
} from '@/domain/travel'
|
||||
|
||||
export const PLAN_STORAGE_KEY = 'guangming:last-plan'
|
||||
export const PREFERENCES_STORAGE_KEY = 'guangming:last-preferences'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user