refactor(ui): replace modal dialogs with toast/sheet components
Remove DialogVant/NotifyVant and add Toast and Sheet components with useToast, useReveal, and usePageTransition composables. Rework Navigation, RegisterView, and styles across both apps; rebuild dist assets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
148b99c4a6
commit
c05352d6fd
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Lets the router hold its scroll until MasterLayout's outgoing page has
|
||||
* faded out.
|
||||
*
|
||||
* vue-router runs scrollBehavior right after the new route renders, which with
|
||||
* an out-in transition is while the OLD page is still fading — so a scrolled
|
||||
* page would visibly snap to the top on its way out. MasterLayout reports the
|
||||
* leave here, and scrollBehavior awaits `afterPageLeave()`, so the scroll lands
|
||||
* while the new page is still fully transparent.
|
||||
*
|
||||
* Routes in FullPageLayout have no transition: no leave is ever reported for
|
||||
* them and `afterPageLeave()` resolves at once.
|
||||
*/
|
||||
|
||||
// Never hold the scroll longer than this, even if a leave hook goes missing.
|
||||
const MAX_WAIT_MS = 400
|
||||
|
||||
let leaving: Promise<void> | null = null
|
||||
let finish: (() => void) | null = null
|
||||
|
||||
export function pageLeaveStarted() {
|
||||
finish?.()
|
||||
leaving = new Promise<void>((resolve) => {
|
||||
const timer = setTimeout(done, MAX_WAIT_MS)
|
||||
function done() {
|
||||
clearTimeout(timer)
|
||||
if (finish === done) {
|
||||
finish = null
|
||||
leaving = null
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
finish = done
|
||||
})
|
||||
}
|
||||
|
||||
export function pageLeaveEnded() {
|
||||
finish?.()
|
||||
}
|
||||
|
||||
export function afterPageLeave(): Promise<void> {
|
||||
return leaving ?? Promise.resolve()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
/**
|
||||
* First-paint entrance shared by the tab screens (Home, Dashboard, Cart):
|
||||
* once a screen's first data load lands, its sections rise and fade in one
|
||||
* after another. The styles are the global `.reveal` / `.is-revealed` pair in
|
||||
* assets/styles/style.less.
|
||||
*
|
||||
* MasterLayout renders its routes without keep-alive, so a screen remounts on
|
||||
* every return to its tab. Which screens have already played is kept here in
|
||||
* module scope, so the entrance is a first-run flourish that resets only on a
|
||||
* full reload — replaying it on each tab switch would fight the page
|
||||
* transition rather than add to it. (A `let` at the top of <script setup>
|
||||
* would not do this: that code runs once per mount.)
|
||||
*/
|
||||
const played = new Set<string>()
|
||||
|
||||
export function useReveal(key: string, stepMs = 70) {
|
||||
const revealed = ref(played.has(key))
|
||||
|
||||
// Each item starts a beat after the one before it. Kept small, so the whole
|
||||
// sequence finishes well under a second and reads as the page arriving.
|
||||
function revealDelay(i: number) {
|
||||
return { '--reveal-delay': `${i * stepMs}ms` }
|
||||
}
|
||||
|
||||
// Call once the first load has populated the DOM. Waits for the new nodes
|
||||
// to be mounted, then for one painted frame in their "before" state — with
|
||||
// a single frame the browser can resolve both states in the same style pass
|
||||
// and there is nothing to animate.
|
||||
async function reveal() {
|
||||
if (revealed.value) return
|
||||
await nextTick()
|
||||
requestAnimationFrame(() =>
|
||||
requestAnimationFrame(() => {
|
||||
revealed.value = true
|
||||
played.add(key)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return { revealed, revealDelay, reveal }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
/**
|
||||
* App-wide toasts. One shared queue, rendered by <ToastHost> (mounted once in
|
||||
* App.vue), so any view or store can call `toast.success(...)` without placing
|
||||
* a component in its own template.
|
||||
*/
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'info' | 'warning'
|
||||
|
||||
export interface ToastOptions {
|
||||
title?: string
|
||||
/** ms before it closes by itself; 0 keeps it until the user dismisses it. */
|
||||
duration?: number
|
||||
/** Custom icon name; defaults to one per type. */
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface ToastItem {
|
||||
id: number
|
||||
type: ToastType
|
||||
message: string
|
||||
title?: string
|
||||
icon: string
|
||||
duration: number
|
||||
}
|
||||
|
||||
const DEFAULT_DURATION = 3200
|
||||
// Older toasts beyond this are dropped so the stack never covers the screen.
|
||||
const MAX_VISIBLE = 3
|
||||
|
||||
const ICONS: Record<ToastType, string> = {
|
||||
success: 'vuesax-linear:tick-circle',
|
||||
error: 'vuesax-linear:close-circle',
|
||||
info: 'vuesax-linear:info-circle',
|
||||
warning: 'vuesax-linear:warning-2',
|
||||
}
|
||||
|
||||
const state = reactive<{ items: ToastItem[] }>({ items: [] })
|
||||
let nextId = 1
|
||||
|
||||
function show(type: ToastType, message: string, options: ToastOptions = {}): number {
|
||||
const id = nextId++
|
||||
state.items.push({
|
||||
id,
|
||||
type,
|
||||
message,
|
||||
title: options.title,
|
||||
icon: options.icon ?? ICONS[type],
|
||||
duration: options.duration ?? DEFAULT_DURATION,
|
||||
})
|
||||
if (state.items.length > MAX_VISIBLE) state.items.splice(0, state.items.length - MAX_VISIBLE)
|
||||
return id
|
||||
}
|
||||
|
||||
function dismiss(id: number) {
|
||||
const i = state.items.findIndex((t) => t.id === id)
|
||||
if (i !== -1) state.items.splice(i, 1)
|
||||
}
|
||||
|
||||
export const toast = {
|
||||
show,
|
||||
dismiss,
|
||||
success: (message: string, options?: ToastOptions) => show('success', message, options),
|
||||
error: (message: string, options?: ToastOptions) => show('error', message, options),
|
||||
info: (message: string, options?: ToastOptions) => show('info', message, options),
|
||||
warning: (message: string, options?: ToastOptions) => show('warning', message, options),
|
||||
}
|
||||
|
||||
export function useToast() {
|
||||
return { toasts: state.items, ...toast }
|
||||
}
|
||||
Reference in New Issue
Block a user