forked from 77media/video-flow
117 lines
3.8 KiB
TypeScript
117 lines
3.8 KiB
TypeScript
'use client'
|
||
|
||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||
|
||
interface ProgressState {
|
||
open: boolean
|
||
title: string
|
||
progress: number
|
||
}
|
||
|
||
interface ProgressToastContextValue {
|
||
/** 显示或更新进度提示 */
|
||
show: (params: { title?: string; progress?: number }) => void
|
||
/** 仅更新进度或标题 */
|
||
update: (params: { title?: string; progress?: number }) => void
|
||
/** 隐藏提示 */
|
||
hide: () => void
|
||
/** 当前状态(只读) */
|
||
state: ProgressState
|
||
}
|
||
|
||
const ProgressToastContext = createContext<ProgressToastContextValue | null>(null)
|
||
|
||
export const H5ProgressToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||
const [state, setState] = useState<ProgressState>({ open: false, title: 'AI生成中…', progress: 0 })
|
||
|
||
const hide = useCallback(() => setState(prev => ({ ...prev, open: false })), [])
|
||
|
||
const show = useCallback((params: { title?: string; progress?: number }) => {
|
||
setState(prev => ({
|
||
open: true,
|
||
title: params.title ?? prev.title,
|
||
progress: typeof params.progress === 'number' ? params.progress : prev.progress,
|
||
}))
|
||
}, [])
|
||
|
||
const update = useCallback((params: { title?: string; progress?: number }) => {
|
||
setState(prev => ({
|
||
open: prev.open || true,
|
||
title: params.title ?? prev.title,
|
||
progress: typeof params.progress === 'number' ? params.progress : prev.progress,
|
||
}))
|
||
}, [])
|
||
|
||
// 进度到100自动隐藏
|
||
useEffect(() => {
|
||
if (!state.open) return
|
||
if (state.progress >= 100) {
|
||
const timer = setTimeout(() => hide(), 600)
|
||
return () => clearTimeout(timer)
|
||
}
|
||
}, [state.open, state.progress, hide])
|
||
|
||
const value = useMemo<ProgressToastContextValue>(() => ({ show, update, hide, state }), [show, update, hide, state])
|
||
|
||
return (
|
||
<ProgressToastContext.Provider value={value}>
|
||
{children}
|
||
<H5ProgressToastUI open={state.open} title={state.title} progress={state.progress} />
|
||
</ProgressToastContext.Provider>
|
||
)
|
||
}
|
||
|
||
export function useH5ProgressToast() {
|
||
const ctx = useContext(ProgressToastContext)
|
||
if (!ctx) throw new Error('useH5ProgressToast must be used within H5ProgressToastProvider')
|
||
return ctx
|
||
}
|
||
|
||
interface UIProps {
|
||
open: boolean
|
||
title: string
|
||
progress: number
|
||
}
|
||
|
||
/**
|
||
* H5样式的顶部居中进度提示,贴合截图风格。
|
||
* 无遮罩;进度为0-100。
|
||
*/
|
||
const H5ProgressToastUI: React.FC<UIProps> = ({ open, title, progress }) => {
|
||
if (!open) return null
|
||
const pct = Math.max(0, Math.min(100, Math.round(progress)))
|
||
return (
|
||
<div
|
||
data-alt="progress-toast"
|
||
className="fixed right-4 top-16 sm:top-20 z-[100]"
|
||
>
|
||
<div
|
||
data-alt="toast-card"
|
||
className="px-4 py-3 rounded-2xl bg-[#1f1b2e]/95 shadow-2xl border border-white/10 min-w-[240px] max-w-[86vw]"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<span className="w-3 h-3 rounded-full bg-purple-500 shadow-[0_0_12px_rgba(168,85,247,0.8)]" data-alt="dot" />
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-white text-sm font-semibold truncate" data-alt="title-text">{title}</div>
|
||
<div className="mt-2 h-2 rounded-full bg-purple-500/30 overflow-hidden" data-alt="progress-bar">
|
||
<div
|
||
className="h-full bg-gradient-to-r from-purple-400 to-purple-600 rounded-full transition-[width] duration-300 ease-out"
|
||
style={{ width: `${pct}%` }}
|
||
data-alt="progress-inner"
|
||
/>
|
||
</div>
|
||
<div className="mt-1 flex items-center justify-between text-xs text-white/70">
|
||
<span data-alt="percent">{pct}%</span>
|
||
<span data-alt="hint">{pct >= 100 ? '即将完成' : ''}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default H5ProgressToastProvider
|
||
|
||
|