forked from zhouruizhe/gmTouringMiniApp
Update planner, check-in records, location service and tests
AI Code Review / review (pull_request) Successful in 6m54s
AI Code Review / review (pull_request) Successful in 6m54s
This commit is contained in:
@@ -12,6 +12,7 @@ interface BadgeView extends CheckInBadgeDefinition {
|
||||
|
||||
const profile = ref<CheckInProfile>({ points: 0, badges: [], checkIns: [] })
|
||||
const storageError = ref('')
|
||||
const poiDirectoryError = ref('')
|
||||
const availablePois = shallowRef<PoiResolved[]>([])
|
||||
const badgeViews = computed<BadgeView[]>(() => CHECK_IN_BADGES.map(definition => ({
|
||||
...definition,
|
||||
@@ -31,7 +32,20 @@ function formatTime(value: string): string {
|
||||
}
|
||||
|
||||
function openPoi(poiId: string) {
|
||||
if (!getPoiRepository().getPoiById(poiId)) {
|
||||
let poi: PoiResolved | null = null
|
||||
try {
|
||||
poi = getPoiRepository().getPoiById(poiId)
|
||||
}
|
||||
catch (error) {
|
||||
poiDirectoryError.value = error instanceof Error ? error.message : '开放打卡点位暂时无法读取'
|
||||
uni.showModal({
|
||||
title: '暂时无法打开点位',
|
||||
content: '开放点位目录暂时无法读取,请稍后重新加载。',
|
||||
showCancel: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!poi) {
|
||||
uni.showModal({
|
||||
title: '点位已下线',
|
||||
content: '该点位暂不在当前开放列表中,本机历史打卡记录仍会保留。',
|
||||
@@ -42,12 +56,35 @@ function openPoi(poiId: string) {
|
||||
uni.navigateTo({ url: `/pages/poi/detail?poiId=${encodeURIComponent(poiId)}` })
|
||||
}
|
||||
|
||||
function loadProfile() {
|
||||
try {
|
||||
profile.value = getCheckInProfile()
|
||||
storageError.value = ''
|
||||
}
|
||||
catch (error) {
|
||||
profile.value = { points: 0, badges: [], checkIns: [] }
|
||||
storageError.value = error instanceof Error ? error.message : '本机打卡记录暂时无法读取'
|
||||
}
|
||||
}
|
||||
|
||||
function loadAvailablePois() {
|
||||
const repository = getPoiRepository()
|
||||
availablePois.value = getCheckInTasks().flatMap((task) => {
|
||||
const poi = repository.getPoiById(task.poiId)
|
||||
return poi ? [poi] : []
|
||||
})
|
||||
try {
|
||||
const repository = getPoiRepository()
|
||||
availablePois.value = getCheckInTasks().flatMap((task) => {
|
||||
const poi = repository.getPoiById(task.poiId)
|
||||
return poi ? [poi] : []
|
||||
})
|
||||
poiDirectoryError.value = ''
|
||||
}
|
||||
catch (error) {
|
||||
availablePois.value = []
|
||||
poiDirectoryError.value = error instanceof Error ? error.message : '开放打卡点位暂时无法读取'
|
||||
}
|
||||
}
|
||||
|
||||
function loadRecords() {
|
||||
loadProfile()
|
||||
loadAvailablePois()
|
||||
}
|
||||
|
||||
function poiCheckedIn(poiId: string): boolean {
|
||||
@@ -80,16 +117,7 @@ function clearLocalRecords() {
|
||||
})
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
loadAvailablePois()
|
||||
try {
|
||||
profile.value = getCheckInProfile()
|
||||
storageError.value = ''
|
||||
}
|
||||
catch (error) {
|
||||
storageError.value = error instanceof Error ? error.message : '本机打卡记录暂时无法读取'
|
||||
}
|
||||
})
|
||||
onShow(loadRecords)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -98,9 +126,9 @@ onShow(() => {
|
||||
<text class="records-hero__eyebrow">LOCAL JOURNEY</text>
|
||||
<text class="records-hero__title">我的光明足迹</text>
|
||||
<view class="records-stats">
|
||||
<view><text>{{ profile.points }}</text><text>本机积分</text></view>
|
||||
<view><text>{{ profile.checkIns.length }}</text><text>打卡点位</text></view>
|
||||
<view><text>{{ profile.badges.length }}</text><text>已获徽章</text></view>
|
||||
<view><text>{{ storageError ? '--' : profile.points }}</text><text>本机积分</text></view>
|
||||
<view><text>{{ storageError ? '--' : profile.checkIns.length }}</text><text>打卡点位</text></view>
|
||||
<view><text>{{ storageError ? '--' : profile.badges.length }}</text><text>已获徽章</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -111,7 +139,13 @@ onShow(() => {
|
||||
<button @click="clearLocalRecords">清除异常打卡数据</button>
|
||||
</view>
|
||||
|
||||
<view class="records-section">
|
||||
<view v-if="poiDirectoryError" class="records-error">
|
||||
<text class="records-error__title">开放点位暂时无法读取</text>
|
||||
<text>{{ poiDirectoryError }}</text>
|
||||
<button @click="loadAvailablePois">重新加载开放点位</button>
|
||||
</view>
|
||||
|
||||
<view v-if="!storageError" class="records-section">
|
||||
<view class="records-section__heading">
|
||||
<view><text class="records-section__kicker">BADGES</text><text class="records-section__title">光明徽章</text></view>
|
||||
<text>{{ profile.badges.length }} / {{ badgeViews.length }}</text>
|
||||
@@ -131,7 +165,7 @@ onShow(() => {
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="records-section">
|
||||
<view v-if="!poiDirectoryError && !storageError" class="records-section">
|
||||
<view class="records-section__heading">
|
||||
<view><text class="records-section__kicker">PLACES</text><text class="records-section__title">开放打卡点位</text></view>
|
||||
<text>{{ availableCheckInCount }} / {{ availablePois.length }}</text>
|
||||
@@ -150,7 +184,7 @@ onShow(() => {
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="records-section">
|
||||
<view v-if="!storageError" class="records-section">
|
||||
<view class="records-section__heading">
|
||||
<view><text class="records-section__kicker">HISTORY</text><text class="records-section__title">打卡记录</text></view>
|
||||
<text>{{ profile.checkIns.length }} 条</text>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import type { PoiSummary } from '@/domain/poi'
|
||||
import type { BudgetLevel, Interest, Pace, PlanningRequest, Theme, Transport, TravelPreferences } from '@/domain/travel'
|
||||
import { getPoiRepository } from '@/data/poi'
|
||||
import { TravelValidationError } from '@/domain/travel'
|
||||
import { isLocationSnapshotFresh } from '@/services/location'
|
||||
import { createPlan, loadPlanningRequest, loadPreferences, savePlan, savePreferences } from '@/services/travel-assistant'
|
||||
import { useLocationStore } from '@/stores'
|
||||
|
||||
@@ -56,14 +58,18 @@ const selectedPoiIds = ref<string[]>([])
|
||||
const customStartsFromCurrentLocation = ref(false)
|
||||
const locationFreshnessCheckedAt = ref(Date.now())
|
||||
const submitting = ref(false)
|
||||
let locationFreshnessTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let pageVisible = false
|
||||
const locationStore = useLocationStore()
|
||||
const locationAvailable = computed(() => {
|
||||
const checkedAt = locationFreshnessCheckedAt.value
|
||||
const snapshot = locationStore.snapshot
|
||||
return locationStore.status === 'ready'
|
||||
&& Boolean(snapshot)
|
||||
&& Number.isFinite(Date.parse(snapshot?.capturedAt ?? ''))
|
||||
&& checkedAt - Date.parse(snapshot?.capturedAt ?? '') <= LOCATION_MAX_AGE_MS
|
||||
&& isLocationSnapshotFresh(
|
||||
snapshot?.capturedAt ?? '',
|
||||
locationFreshnessCheckedAt.value,
|
||||
LOCATION_MAX_AGE_MS,
|
||||
)
|
||||
})
|
||||
const poiSummaries = shallowRef<PoiSummary[]>(getPoiRepository().getPoiSummaries())
|
||||
const poiGroups = computed(() => {
|
||||
@@ -217,6 +223,32 @@ function isPermissionDenied(error: unknown): boolean {
|
||||
return /auth deny|auth denied|authorize:fail|permission/i.test(message)
|
||||
}
|
||||
|
||||
function clearLocationFreshnessTimer() {
|
||||
if (locationFreshnessTimer === null)
|
||||
return
|
||||
clearTimeout(locationFreshnessTimer)
|
||||
locationFreshnessTimer = null
|
||||
}
|
||||
|
||||
function refreshLocationFreshness() {
|
||||
clearLocationFreshnessTimer()
|
||||
const checkedAt = Date.now()
|
||||
locationFreshnessCheckedAt.value = checkedAt
|
||||
const snapshot = locationStore.snapshot
|
||||
if (locationStore.status !== 'ready' || !snapshot)
|
||||
return
|
||||
if (!pageVisible)
|
||||
return
|
||||
|
||||
const capturedAt = Date.parse(snapshot.capturedAt)
|
||||
if (!Number.isFinite(capturedAt) || capturedAt > checkedAt)
|
||||
return
|
||||
const expiresInMs = capturedAt + LOCATION_MAX_AGE_MS - checkedAt
|
||||
if (expiresInMs < 0)
|
||||
return
|
||||
locationFreshnessTimer = setTimeout(refreshLocationFreshness, expiresInMs + 1)
|
||||
}
|
||||
|
||||
function requestCurrentLocation() {
|
||||
if (locationStore.status === 'locating')
|
||||
return
|
||||
@@ -234,6 +266,7 @@ function requestCurrentLocation() {
|
||||
|
||||
function locateFromUserAction() {
|
||||
locationStore.startLocating()
|
||||
refreshLocationFreshness()
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
isHighAccuracy: true,
|
||||
@@ -249,7 +282,7 @@ function locateFromUserAction() {
|
||||
uni.showToast({ title: '定位结果无效', icon: 'none' })
|
||||
return
|
||||
}
|
||||
locationFreshnessCheckedAt.value = Date.now()
|
||||
refreshLocationFreshness()
|
||||
if (planningMode.value === 'custom')
|
||||
customStartsFromCurrentLocation.value = true
|
||||
uni.showToast({ title: '已更新当前位置', icon: 'success' })
|
||||
@@ -286,7 +319,7 @@ function showDurationFitNotice(status: 'overflow' | 'underfilled'): Promise<void
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
locationFreshnessCheckedAt.value = Date.now()
|
||||
refreshLocationFreshness()
|
||||
if (!form.themes.length && !form.interests.length) {
|
||||
uni.showToast({ title: '请至少选择一项主题或偏好', icon: 'none' })
|
||||
return
|
||||
@@ -343,9 +376,13 @@ async function submit() {
|
||||
uni.navigateTo({ url: '/pages/itinerary/index' })
|
||||
}
|
||||
catch (error) {
|
||||
const quickRouteUnreachable = error instanceof TravelValidationError
|
||||
&& error.code === 'quick_route_unreachable'
|
||||
uni.showModal({
|
||||
title: '暂时无法生成路线',
|
||||
content: error instanceof Error ? error.message : '请检查选择后重试',
|
||||
title: quickRouteUnreachable ? '当前路线无法完成' : '暂时无法生成路线',
|
||||
content: quickRouteUnreachable
|
||||
? '当前起点、交通方式与所选时长无法容纳首个点位。请靠近光明区、延长游玩时长,或改用自选点位规划。'
|
||||
: error instanceof Error ? error.message : '请检查选择后重试',
|
||||
showCancel: false,
|
||||
})
|
||||
}
|
||||
@@ -355,7 +392,8 @@ async function submit() {
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
locationFreshnessCheckedAt.value = Date.now()
|
||||
pageVisible = true
|
||||
refreshLocationFreshness()
|
||||
const storedRequest = loadPlanningRequest()
|
||||
const storedPreferences = storedRequest?.preferences ?? loadPreferences()
|
||||
if (storedPreferences)
|
||||
@@ -371,6 +409,14 @@ onShow(() => {
|
||||
}
|
||||
applyPreset()
|
||||
})
|
||||
|
||||
function deactivatePlannerPage() {
|
||||
pageVisible = false
|
||||
clearLocationFreshnessTimer()
|
||||
}
|
||||
|
||||
onHide(deactivatePlannerPage)
|
||||
onUnload(deactivatePlannerPage)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
Reference in New Issue
Block a user