diff --git a/packages/app/src/components/MemberProgress.vue b/packages/app/src/components/MemberProgress.vue index 9e492d1..a0bed83 100644 --- a/packages/app/src/components/MemberProgress.vue +++ b/packages/app/src/components/MemberProgress.vue @@ -97,11 +97,16 @@ const instance = getCurrentInstance() let sequence = 0, openedAt = 0, timer: ReturnType | 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(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) }) diff --git a/packages/app/src/pages/admin/member-progress.vue b/packages/app/src/pages/admin/member-progress.vue index 2c75ab5..3a5d33c 100644 --- a/packages/app/src/pages/admin/member-progress.vue +++ b/packages/app/src/pages/admin/member-progress.vue @@ -1,13 +1,12 @@ - + - + \ No newline at end of file diff --git a/packages/app/src/pages/profile/progress.vue b/packages/app/src/pages/profile/progress.vue index 18eda12..6648fc3 100644 --- a/packages/app/src/pages/profile/progress.vue +++ b/packages/app/src/pages/profile/progress.vue @@ -1,12 +1,8 @@ - + - + \ No newline at end of file diff --git a/packages/server/.env b/packages/server/.env index 9e5be8b..e26e48e 100644 --- a/packages/server/.env +++ b/packages/server/.env @@ -21,6 +21,7 @@ API_BASE_URL=https://focus.richarjiang.com/ PORT=3000 WX_SUBSCRIBE_TEMPLATE_BOOKING_CONFIRMED=antYfc85gvwImFZ9kM4UiqMOywJxbqFVgKHLH3NikII +WX_SUBSCRIBE_TEMPLATE_CLASS_REVIEW=QJaTOSq_QpyL_spdNRTUfbkmeWDDi5iDAYZyXrFAPc8 # COS upload COS_SECRET_ID=AKIDwwulT3ub9f9bxFVdihcP4Z1S6qivMxmu