perf: 优化订阅刷新逻辑

This commit is contained in:
richarjiang
2026-09-07 14:06:19 +08:00
parent 88cd8419c8
commit c9e1ac1140
3 changed files with 64 additions and 10 deletions

View File

@@ -146,6 +146,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import type { BookingWithUser, BookingStatusHistory } from '@mp-pilates/shared' import type { BookingWithUser, BookingStatusHistory } from '@mp-pilates/shared'
import { BookingStatus } from '@mp-pilates/shared' import { BookingStatus } from '@mp-pilates/shared'
import { useBookingStore } from '../../stores/booking' import { useBookingStore } from '../../stores/booking'
@@ -165,6 +166,7 @@ const bookingStore = useBookingStore()
const navBarHeight = ref('64px') const navBarHeight = ref('64px')
const refreshing = ref(false) const refreshing = ref(false)
const loading = ref(false) const loading = ref(false)
const hasLoadedOnce = ref(false)
// ─── Filter state ───────────────────────────────────────────────────────── // ─── Filter state ─────────────────────────────────────────────────────────
type FilterValue = string | null type FilterValue = string | null
@@ -215,9 +217,9 @@ function formatTimelineText(h: BookingStatusHistory): string {
} }
// ─── Data loading ───────────────────────────────────────────────────────── // ─── Data loading ─────────────────────────────────────────────────────────
async function loadBookings(append = false) { async function loadBookings(append = false, opts: { silent?: boolean } = {}) {
if (loading.value) return if (loading.value) return
loading.value = true if (!opts.silent) loading.value = true
try { try {
const page = append ? currentPage.value + 1 : 1 const page = append ? currentPage.value + 1 : 1
@@ -234,8 +236,9 @@ async function loadBookings(append = false) {
totalCount.value = result.total totalCount.value = result.total
hasMore.value = bookings.value.length < result.total hasMore.value = bookings.value.length < result.total
// Fetch history for each booking // Fetch history for each booking. Skip in silent mode — history is only
if (!append) { // shown as a small inline preview, and the detail page has the full one.
if (!append && !opts.silent) {
await Promise.all( await Promise.all(
bookings.value.map((b) => fetchHistory(b.id)), bookings.value.map((b) => fetchHistory(b.id)),
) )
@@ -247,9 +250,9 @@ async function loadBookings(append = false) {
} }
} catch (err) { } catch (err) {
console.error('Load bookings failed:', err) console.error('Load bookings failed:', err)
uni.showToast({ title: '加载失败', icon: 'none' }) if (!opts.silent) uni.showToast({ title: '加载失败', icon: 'none' })
} finally { } finally {
loading.value = false if (!opts.silent) loading.value = false
} }
} }
@@ -391,6 +394,24 @@ onMounted(() => {
navBarHeight.value = `${getSystemLayout().navBarHeight}px` navBarHeight.value = `${getSystemLayout().navBarHeight}px`
loadBookings(false) loadBookings(false)
loadAllForStats() loadAllForStats()
hasLoadedOnce.value = true
})
// After returning from booking detail (where status may have changed),
// re-sync the list with the server. Local row actions still call onRefresh
// directly — this onShow is the safety net for the navigate-back path.
// Uses silent mode so the list stays visible (no skeleton flash).
onShow(() => {
if (!hasLoadedOnce.value) return
// Skip while a refresh is already in flight to avoid overlap.
if (refreshing.value || loading.value) return
Promise.all([
loadBookings(false, { silent: true }),
loadAllForStats(),
]).catch(() => {
// Errors are non-fatal here — list keeps showing stale data until
// the next explicit refresh.
})
}) })
</script> </script>

View File

@@ -163,6 +163,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import type { BookingWithDetails } from '@mp-pilates/shared' import type { BookingWithDetails } from '@mp-pilates/shared'
import { BookingStatus } from '@mp-pilates/shared' import { BookingStatus } from '@mp-pilates/shared'
import { useBookingStore } from '../../stores/booking' import { useBookingStore } from '../../stores/booking'
@@ -295,6 +296,8 @@ function formatDateDisplay(dateStr: string): string {
} }
// ─── Actions ────────────────────────────────────────────── // ─── Actions ──────────────────────────────────────────────
const hasLoadedOnce = ref(false)
function selectTab(key: TabKey) { function selectTab(key: TabKey) {
activeTab.value = key activeTab.value = key
} }
@@ -351,7 +354,19 @@ onMounted(() => {
const windowInfo = uni.getWindowInfo() const windowInfo = uni.getWindowInfo()
const statusBarH = windowInfo.statusBarHeight ?? 20 const statusBarH = windowInfo.statusBarHeight ?? 20
navBarHeight.value = `${statusBarH + Math.round(88 * windowInfo.windowWidth / 750)}px` navBarHeight.value = `${statusBarH + Math.round(88 * windowInfo.windowWidth / 750)}px`
bookingStore.fetchMyBookings() bookingStore.fetchMyBookings().then(() => {
hasLoadedOnce.value = true
})
})
// After returning from booking detail (where status may have changed),
// silently re-sync without flipping loading state — keeps the list visible
// and avoids the skeleton flash. Store action `replaceBooking` already
// keeps `myBookings` in sync for the common case; this is the safety net
// for any state we didn't locally patch (e.g. server-side cascading fields).
onShow(() => {
if (!hasLoadedOnce.value) return
bookingStore.fetchMyBookings(undefined, { silent: true })
}) })
</script> </script>

View File

@@ -44,13 +44,27 @@ export const useBookingStore = defineStore('booking', () => {
return result return result
} }
/**
* Replace a booking in `myBookings` by id. Preserves immutability: always
* returns a new array reference so Vue's computed/watchers pick up the change.
* If the booking isn't in the list (e.g. paginated out), leaves state untouched.
*/
function replaceBooking(updated: BookingWithDetails) {
const idx = myBookings.value.findIndex((b) => b.id === updated.id)
if (idx === -1) return
const next = myBookings.value.slice()
next[idx] = updated
myBookings.value = next
}
async function cancelBooking(bookingId: string) { async function cancelBooking(bookingId: string) {
const result = await put<BookingWithDetails>(`/booking/${bookingId}/cancel`) const result = await put<BookingWithDetails>(`/booking/${bookingId}/cancel`)
replaceBooking(result)
return result return result
} }
async function fetchMyBookings(status?: string) { async function fetchMyBookings(status?: string, opts: { silent?: boolean } = {}) {
loadingBookings.value = true if (!opts.silent) loadingBookings.value = true
try { try {
const params: Record<string, unknown> = status ? { status } : {} const params: Record<string, unknown> = status ? { status } : {}
const paginated = await get<ServerPaginatedResult<BookingWithDetails>>('/booking/my', params) const paginated = await get<ServerPaginatedResult<BookingWithDetails>>('/booking/my', params)
@@ -59,7 +73,7 @@ export const useBookingStore = defineStore('booking', () => {
console.error('Fetch bookings failed:', err) console.error('Fetch bookings failed:', err)
myBookings.value = [] myBookings.value = []
} finally { } finally {
loadingBookings.value = false if (!opts.silent) loadingBookings.value = false
} }
} }
@@ -106,6 +120,7 @@ export const useBookingStore = defineStore('booking', () => {
const result = await put<BookingWithDetails>(`/booking/${bookingId}/confirm`, { const result = await put<BookingWithDetails>(`/booking/${bookingId}/confirm`, {
remark, remark,
}) })
replaceBooking(result)
return result return result
} }
@@ -113,6 +128,7 @@ export const useBookingStore = defineStore('booking', () => {
const result = await put<BookingWithDetails>(`/booking/${bookingId}/complete`, { const result = await put<BookingWithDetails>(`/booking/${bookingId}/complete`, {
remark, remark,
}) })
replaceBooking(result)
return result return result
} }
@@ -120,6 +136,7 @@ export const useBookingStore = defineStore('booking', () => {
const result = await put<BookingWithDetails>(`/booking/${bookingId}/noshow`, { const result = await put<BookingWithDetails>(`/booking/${bookingId}/noshow`, {
remark, remark,
}) })
replaceBooking(result)
return result return result
} }
@@ -159,5 +176,6 @@ export const useBookingStore = defineStore('booking', () => {
fetchBookingHistory, fetchBookingHistory,
fetchSlotById, fetchSlotById,
fetchBookingById, fetchBookingById,
replaceBooking,
} }
}) })