feat: 更改加载时间

This commit is contained in:
王天骄
2026-06-15 17:55:07 +08:00
parent 9b95789595
commit 4c130cee9d
3 changed files with 148 additions and 16 deletions
+62
View File
@@ -48,6 +48,13 @@ export type CaseListPage = {
results: ClinicalCase[]
}
export type CaseListPrefetch = {
source: CaseListSource
query: CaseListQuery
page: CaseListPage
cachedAt: number
}
type QueryValue = string | number | boolean | null | undefined
type ServerCaseListPage = {
@@ -88,6 +95,8 @@ const CASE_LIST_PATHS: Record<CaseListSource, string> = {
}
const TONES: CaseTone[] = ['blue', 'teal', 'pink', 'orange', 'purple', 'green']
const CASE_LIST_PREFETCH_STORAGE_KEY = 'clinical-thinking-prefetched-case-list'
const CASE_LIST_PREFETCH_MAX_AGE_MS = 60 * 1000
export async function fetchCaseList(source: CaseListSource = 'recommended', query: CaseListQuery = {}): Promise<ClinicalCase[]> {
const page = await fetchCaseListPage(source, query)
@@ -102,6 +111,30 @@ export async function fetchCaseListPage(source: CaseListSource = 'recommended',
return page
}
export function savePrefetchedCaseList(payload: CaseListPrefetch) {
uni.setStorageSync(CASE_LIST_PREFETCH_STORAGE_KEY, payload)
}
export function clearPrefetchedCaseList() {
uni.removeStorageSync(CASE_LIST_PREFETCH_STORAGE_KEY)
}
export function takePrefetchedCaseList(source: CaseListSource, query: CaseListQuery): CaseListPage | null {
const value = uni.getStorageSync(CASE_LIST_PREFETCH_STORAGE_KEY)
if (!isCaseListPrefetch(value)) return null
const isFresh = Date.now() - value.cachedAt <= CASE_LIST_PREFETCH_MAX_AGE_MS
if (!isFresh) {
clearPrefetchedCaseList()
return null
}
if (value.source !== source || !isSameCaseListQuery(value.query, query)) return null
clearPrefetchedCaseList()
return value.page
}
export function readStoredClinicalCase() {
const value = uni.getStorageSync('clinical-thinking-selected-case')
if (value && typeof value === 'object') return value as ClinicalCase
@@ -224,6 +257,35 @@ function withQuery(path: string, query: Record<string, QueryValue>) {
return params ? `${path}?${params}` : path
}
function isCaseListPrefetch(value: unknown): value is CaseListPrefetch {
if (!value || typeof value !== 'object') return false
const payload = value as Partial<CaseListPrefetch>
return isCaseListSource(payload.source) &&
typeof payload.query === 'object' &&
payload.query !== null &&
!!payload.page &&
typeof payload.page === 'object' &&
Array.isArray(payload.page.results) &&
typeof payload.cachedAt === 'number'
}
function isCaseListSource(value: unknown): value is CaseListSource {
return value === 'recommended' ||
value === 'specialty' ||
value === 'weak' ||
value === 'teaching' ||
value === 'teacher-task'
}
function isSameCaseListQuery(left: CaseListQuery, right: CaseListQuery) {
const keys: Array<keyof CaseListQuery> = ['search', 'case_type', 'difficulty', 'department', 'page', 'page_size']
return keys.every(key => normalizeQueryValue(left[key]) === normalizeQueryValue(right[key]))
}
function normalizeQueryValue(value: unknown) {
return value === undefined || value === null || value === '' ? '' : String(value)
}
function readErrorMessage(data: unknown, fallback: string) {
if (data && typeof data === 'object') {
const payload = data as Record<string, unknown>
+11 -2
View File
@@ -141,7 +141,13 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { fetchCaseListPage, type CaseListSource, type CaseMode, type ClinicalCase } from '../../api/cases'
import {
fetchCaseListPage,
takePrefetchedCaseList,
type CaseListSource,
type CaseMode,
type ClinicalCase
} from '../../api/cases'
import { createHomeNavigator, createProfileOpener, createSettingsOpener } from '../../api/navigation'
const emit = defineEmits<{
@@ -284,9 +290,12 @@ async function loadCases(page = 1) {
loadingMore.value = !isFirstPage
loadFailed.value = false
errorMessage.value = ''
const query = buildQuery(page)
try {
const result = await fetchCaseListPage(source.value, buildQuery(page))
const result = isFirstPage
? takePrefetchedCaseList(source.value, query) || await fetchCaseListPage(source.value, query)
: await fetchCaseListPage(source.value, query)
if (seq !== requestSeq) return
cases.value = isFirstPage ? result.results : [...cases.value, ...result.results]
+75 -14
View File
@@ -64,7 +64,13 @@
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import type { CaseListSource } from '../../api/cases'
import {
clearPrefetchedCaseList,
fetchCaseListPage,
savePrefetchedCaseList,
type CaseListQuery,
type CaseListSource
} from '../../api/cases'
import { fetchMatchingProfile, type MatchingProfile } from '../../api/matching'
type Particle = {
@@ -86,13 +92,22 @@ const source = ref<CaseListSource>('recommended')
let particleId = 0
let particleTimer: ReturnType<typeof setInterval> | null = null
let progressTimer: ReturnType<typeof setInterval> | null = null
const MATCHING_DURATION_MS = 10000
let navigationTimer: ReturnType<typeof setTimeout> | null = null
let minDurationFinished = false
let prefetchFinished = false
let hasRedirected = false
let isActive = false
const MIN_MATCHING_DURATION_MS = 3000
const PROGRESS_INTERVAL_MS = 100
const PREFETCH_QUERY: CaseListQuery = {
page: 1,
page_size: 10
}
function loadMatchingProfile() {
fetchMatchingProfile().then(result => {
Object.assign(profile, result)
startProgress()
})
}
@@ -133,20 +148,62 @@ function startProgress() {
const startedAt = Date.now()
progressTimer = setInterval(() => {
const elapsed = Date.now() - startedAt
const ratio = Math.min(1, elapsed / MATCHING_DURATION_MS)
progress.value = Math.round(profile.progressTarget * ratio)
if (ratio >= 1) {
if (progressTimer) clearInterval(progressTimer)
progressTimer = null
uni.redirectTo({
url: `/pages/cases/cases?source=${encodeURIComponent(source.value)}`
})
return
}
const minimumRatio = Math.min(1, elapsed / MIN_MATCHING_DURATION_MS)
const waitBonus = elapsed > MIN_MATCHING_DURATION_MS
? Math.min(7, Math.floor((elapsed - MIN_MATCHING_DURATION_MS) / 1000))
: 0
progress.value = Math.min(99, Math.round(profile.progressTarget * minimumRatio + waitBonus))
}, PROGRESS_INTERVAL_MS)
}
async function startMatchingFlow() {
clearPrefetchedCaseList()
minDurationFinished = false
prefetchFinished = false
hasRedirected = false
startProgress()
wait(MIN_MATCHING_DURATION_MS).then(() => {
minDurationFinished = true
redirectWhenReady()
})
try {
const page = await fetchCaseListPage(source.value, PREFETCH_QUERY)
if (!isActive) return
savePrefetchedCaseList({
source: source.value,
query: PREFETCH_QUERY,
page,
cachedAt: Date.now()
})
} catch {
// The case list page will retry and show its own error state if needed.
} finally {
prefetchFinished = true
redirectWhenReady()
}
}
function redirectWhenReady() {
if (!isActive || hasRedirected || !minDurationFinished || !prefetchFinished) return
hasRedirected = true
progress.value = 100
if (progressTimer) clearInterval(progressTimer)
progressTimer = null
navigationTimer = setTimeout(() => {
uni.redirectTo({
url: `/pages/cases/cases?source=${encodeURIComponent(source.value)}`
})
}, 120)
}
function wait(ms: number) {
return new Promise<void>(resolve => {
setTimeout(resolve, ms)
})
}
onLoad(query => {
const querySource = query?.source
if (isCaseListSource(querySource)) {
@@ -155,13 +212,17 @@ onLoad(query => {
})
onMounted(() => {
isActive = true
loadMatchingProfile()
startParticles()
startMatchingFlow()
})
onUnmounted(() => {
isActive = false
if (particleTimer) clearInterval(particleTimer)
if (progressTimer) clearInterval(progressTimer)
if (navigationTimer) clearTimeout(navigationTimer)
})
function isCaseListSource(value: unknown): value is CaseListSource {