feat: 更改加载时间
This commit is contained in:
@@ -48,6 +48,13 @@ export type CaseListPage = {
|
|||||||
results: ClinicalCase[]
|
results: ClinicalCase[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CaseListPrefetch = {
|
||||||
|
source: CaseListSource
|
||||||
|
query: CaseListQuery
|
||||||
|
page: CaseListPage
|
||||||
|
cachedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
type QueryValue = string | number | boolean | null | undefined
|
type QueryValue = string | number | boolean | null | undefined
|
||||||
|
|
||||||
type ServerCaseListPage = {
|
type ServerCaseListPage = {
|
||||||
@@ -88,6 +95,8 @@ const CASE_LIST_PATHS: Record<CaseListSource, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TONES: CaseTone[] = ['blue', 'teal', 'pink', 'orange', 'purple', 'green']
|
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[]> {
|
export async function fetchCaseList(source: CaseListSource = 'recommended', query: CaseListQuery = {}): Promise<ClinicalCase[]> {
|
||||||
const page = await fetchCaseListPage(source, query)
|
const page = await fetchCaseListPage(source, query)
|
||||||
@@ -102,6 +111,30 @@ export async function fetchCaseListPage(source: CaseListSource = 'recommended',
|
|||||||
return page
|
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() {
|
export function readStoredClinicalCase() {
|
||||||
const value = uni.getStorageSync('clinical-thinking-selected-case')
|
const value = uni.getStorageSync('clinical-thinking-selected-case')
|
||||||
if (value && typeof value === 'object') return value as ClinicalCase
|
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
|
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) {
|
function readErrorMessage(data: unknown, fallback: string) {
|
||||||
if (data && typeof data === 'object') {
|
if (data && typeof data === 'object') {
|
||||||
const payload = data as Record<string, unknown>
|
const payload = data as Record<string, unknown>
|
||||||
|
|||||||
+11
-2
@@ -141,7 +141,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
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'
|
import { createHomeNavigator, createProfileOpener, createSettingsOpener } from '../../api/navigation'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -284,9 +290,12 @@ async function loadCases(page = 1) {
|
|||||||
loadingMore.value = !isFirstPage
|
loadingMore.value = !isFirstPage
|
||||||
loadFailed.value = false
|
loadFailed.value = false
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
const query = buildQuery(page)
|
||||||
|
|
||||||
try {
|
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
|
if (seq !== requestSeq) return
|
||||||
|
|
||||||
cases.value = isFirstPage ? result.results : [...cases.value, ...result.results]
|
cases.value = isFirstPage ? result.results : [...cases.value, ...result.results]
|
||||||
|
|||||||
@@ -64,7 +64,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
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'
|
import { fetchMatchingProfile, type MatchingProfile } from '../../api/matching'
|
||||||
|
|
||||||
type Particle = {
|
type Particle = {
|
||||||
@@ -86,13 +92,22 @@ const source = ref<CaseListSource>('recommended')
|
|||||||
let particleId = 0
|
let particleId = 0
|
||||||
let particleTimer: ReturnType<typeof setInterval> | null = null
|
let particleTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let progressTimer: 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 PROGRESS_INTERVAL_MS = 100
|
||||||
|
const PREFETCH_QUERY: CaseListQuery = {
|
||||||
|
page: 1,
|
||||||
|
page_size: 10
|
||||||
|
}
|
||||||
|
|
||||||
function loadMatchingProfile() {
|
function loadMatchingProfile() {
|
||||||
fetchMatchingProfile().then(result => {
|
fetchMatchingProfile().then(result => {
|
||||||
Object.assign(profile, result)
|
Object.assign(profile, result)
|
||||||
startProgress()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,18 +148,60 @@ function startProgress() {
|
|||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
progressTimer = setInterval(() => {
|
progressTimer = setInterval(() => {
|
||||||
const elapsed = Date.now() - startedAt
|
const elapsed = Date.now() - startedAt
|
||||||
const ratio = Math.min(1, elapsed / MATCHING_DURATION_MS)
|
const minimumRatio = Math.min(1, elapsed / MIN_MATCHING_DURATION_MS)
|
||||||
progress.value = Math.round(profile.progressTarget * ratio)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
if (ratio >= 1) {
|
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)
|
if (progressTimer) clearInterval(progressTimer)
|
||||||
progressTimer = null
|
progressTimer = null
|
||||||
|
navigationTimer = setTimeout(() => {
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url: `/pages/cases/cases?source=${encodeURIComponent(source.value)}`
|
url: `/pages/cases/cases?source=${encodeURIComponent(source.value)}`
|
||||||
})
|
})
|
||||||
return
|
}, 120)
|
||||||
}
|
}
|
||||||
}, PROGRESS_INTERVAL_MS)
|
|
||||||
|
function wait(ms: number) {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
setTimeout(resolve, ms)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(query => {
|
onLoad(query => {
|
||||||
@@ -155,13 +212,17 @@ onLoad(query => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
isActive = true
|
||||||
loadMatchingProfile()
|
loadMatchingProfile()
|
||||||
startParticles()
|
startParticles()
|
||||||
|
startMatchingFlow()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
isActive = false
|
||||||
if (particleTimer) clearInterval(particleTimer)
|
if (particleTimer) clearInterval(particleTimer)
|
||||||
if (progressTimer) clearInterval(progressTimer)
|
if (progressTimer) clearInterval(progressTimer)
|
||||||
|
if (navigationTimer) clearTimeout(navigationTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
function isCaseListSource(value: unknown): value is CaseListSource {
|
function isCaseListSource(value: unknown): value is CaseListSource {
|
||||||
|
|||||||
Reference in New Issue
Block a user