feat: 更改加载时间
This commit is contained in:
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user