perf: 优化进度条

This commit is contained in:
richarjiang
2026-09-09 19:19:18 +08:00
parent 6fab1155c7
commit d941f1b6a9
4 changed files with 33 additions and 15 deletions

View File

@@ -97,11 +97,16 @@ const instance = getCurrentInstance()
let sequence = 0, openedAt = 0, timer: ReturnType<typeof setTimeout> | undefined
async function load() {
if (props.admin && !props.userId) return
const seq = ++sequence; loading.value = true; error.value = ''
if (!editor.value) { photoUrls.value = {}; comparison.value = []; selectedPhotos.value = [] }
const seq = ++sequence
// 仅在首次加载archive 尚未就绪)时才切换 loading 态、清空本地缓存;
// 后台刷新保留 photoUrls / comparison / selectedPhotos避免图片被打回占位造成视觉抖动
const isInitial = !archive.value
if (isInitial) loading.value = true
error.value = ''
if (!editor.value && isInitial) { photoUrls.value = {}; comparison.value = []; selectedPhotos.value = [] }
try { const result = await get<ProgressArchive>(base.value); if (seq === sequence) archive.value = result }
catch (e) { if (seq === sequence) error.value = message(e) }
finally { if (seq === sequence) loading.value = false }
finally { if (seq === sequence && isInitial) loading.value = false }
}
function previewComparison(url: string) { uni.previewImage({ urls: comparison.value.map(p => p.url), current: url }) }
function changeShared(event: Event) { shared.value = (event as unknown as { detail: { value: boolean } }).detail.value }
@@ -157,6 +162,17 @@ function clearPhoto(id: string) { const next = { ...photoUrls.value }; delete ne
async function preview(photo: ProgressPhotoRecord) {
try { const { url } = await get<{ url: string }>(base.value + `/photos/${photo.id}/url`); photoUrls.value = { ...photoUrls.value, [photo.id]: url }; uni.previewImage({ urls: [url] }); if (timer) clearTimeout(timer); timer = setTimeout(() => { photoUrls.value = {}; comparison.value = [] }, 55000) } catch(e) { uni.showToast({ title: message(e), icon: 'none' }) }
}
// 后台拉取单张照片签名 URL学员侧全部自动加载馆主侧仅加载已授权照片。
// 与 preview() 走同一条接口,便于复用 60s 签名失效与手动重试的占位逻辑。
async function ensurePhotoUrl(photo: ProgressPhotoRecord) {
if (photoUrls.value[photo.id]) return
if (props.admin && !photo.consentedAt) return
try {
const { url } = await get<{ url: string }>(base.value + `/photos/${photo.id}/url`)
photoUrls.value = { ...photoUrls.value, [photo.id]: url }
if (timer) clearTimeout(timer); timer = setTimeout(() => { photoUrls.value = {}; comparison.value = [] }, 55000)
} catch (e) { /* 占位保留,学员/馆主可点击重试 */ }
}
function selectPhoto(photo: ProgressPhotoRecord) { if (selectedPhotos.value.includes(photo.id)) selectedPhotos.value = selectedPhotos.value.filter(id => id !== photo.id); else if (selectedPhotos.value.length < 2) selectedPhotos.value = [...selectedPhotos.value, photo.id]; else uni.showToast({ title: '最多选择两张照片', icon: 'none' }) }
async function compare() {
try { comparison.value = await Promise.all(selectedPhotos.value.map(async id => { const { url } = await get<{ url: string }>(base.value + `/photos/${id}/url`); return { id, url, date: formatChinaDate(archive.value!.photos.find(p => p.id === id)!.recordedAt) } })); if (timer) clearTimeout(timer); timer = setTimeout(() => { comparison.value = []; photoUrls.value = {} }, 55000) } catch (e) { uni.showToast({ title: message(e), icon: 'none' }) }
@@ -177,9 +193,15 @@ async function saveMilestone(n: number) {
}
watch(() => [props.userId, props.refreshKey], () => {
if (editor.value || busy.value) { void load(); return }
archive.value = null
void load()
}, { immediate: true })
// 学员切到「照片」Tab 或档案刷新后回到照片 Tab 时,自动后台拉取签名 URL。
// 这样学员无需点击「轻触查看照片」即可直接看到图片;馆主侧只加载已授权照片。
watch([() => tab.value, () => archive.value?.photos], ([currentTab, photos]) => {
if (currentTab === 'photos' && photos && photos.length) {
void Promise.all(photos.map(ensurePhotoUrl))
}
}, { immediate: true })
watch(() => props.bookingId, id => { if (id) { tab.value = 'notes'; openEditor('notes') } }, { immediate: true })
onBeforeUnmount(() => { sequence++; if (timer) clearTimeout(timer) })
</script>

View File

@@ -1,13 +1,12 @@
<template><view class="page" :style="{ paddingTop: navBarHeight + 'px' }"><CustomNavBar title="成长档案" show-back /><MemberProgress admin :user-id="userId" :booking-id="bookingId" :refresh-key="refreshKey" /></view></template>
<template><view class="page" :style="{ paddingTop: navBarHeight + 'px' }"><CustomNavBar title="成长档案" show-back /><MemberProgress admin :user-id="userId" :booking-id="bookingId" /></view></template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { onLoad } from '@dcloudio/uni-app'
import CustomNavBar from '../../components/CustomNavBar.vue'
import MemberProgress from '../../components/MemberProgress.vue'
import { getSystemLayout } from '../../utils/system'
const navBarHeight = getSystemLayout().navBarHeight
const userId = ref(''), bookingId = ref(''), refreshKey = ref(0)
const userId = ref(''), bookingId = ref('')
onLoad(query => { userId.value = String(query?.userId || ''); bookingId.value = String(query?.bookingId || '') })
onShow(() => { refreshKey.value++ })
</script>
<style scoped>.page{min-height:100vh;background:#fbf9f6;box-sizing:border-box;}</style>
<style scoped>.page{min-height:100vh;background:#fbf9f6;box-sizing:border-box;}</style>

View File

@@ -1,12 +1,8 @@
<template><view class="page" :style="{ paddingTop: navBarHeight + 'px' }"><CustomNavBar title="成长档案" show-back /><MemberProgress :refresh-key="refreshKey" /></view></template>
<template><view class="page" :style="{ paddingTop: navBarHeight + 'px' }"><CustomNavBar title="成长档案" show-back /><MemberProgress /></view></template>
<script setup lang="ts">
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import CustomNavBar from '../../components/CustomNavBar.vue'
import MemberProgress from '../../components/MemberProgress.vue'
import { getSystemLayout } from '../../utils/system'
const navBarHeight = getSystemLayout().navBarHeight
const refreshKey = ref(0)
onShow(() => { refreshKey.value++ })
</script>
<style scoped>.page{min-height:100vh;background:#fbf9f6;box-sizing:border-box;}</style>
<style scoped>.page{min-height:100vh;background:#fbf9f6;box-sizing:border-box;}</style>