更新依赖项及历史人物相关内容资源

- 更新 package.json 和 package-lock.json 中的依赖项
- 新增/更新陈云、黄炎培、鲁迅、孙中山的音频文件
- 新增场景和组件的图片资源
- 清理未使用的音频和图片文件
- 优化多个组件和页面的实现逻辑
This commit is contained in:
zhongyidian committed 2026-04-20 13:27:18 +08:00
1 parent b1d55b9464
commit e0fa47d21a
629 files changed
+24390 -2991

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
import { useEffect, useMemo, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { cn, getBackgroundImage } from '@/lib/utils'
export type HintPositionOffset = {
/** 相对窗口中心点的水平偏移(px) */
x?: number
/** 相对窗口中心点的垂直偏移(px) */
y?: number
}
export interface ShowHintOptions {
/** 自动消失时间(ms)。默认 2000 */
durationMs?: number
/** 是否常驻(常驻时不会自动消失) */
persistent?: boolean
/** 相对中心点微调位置 */
offset?: HintPositionOffset
/** 自定义层级(默认 60) */
zIndex?: number
}
type HintState = {
visible: boolean
text: string
options: Required<Pick<ShowHintOptions, 'durationMs' | 'persistent' | 'offset' | 'zIndex'>>
}
const DEFAULT_OPTIONS: HintState['options'] = {
durationMs: 2000,
persistent: false,
offset: { x: 0, y: 0 },
zIndex: 60,
}
let current: HintState = {
visible: false,
text: '',
options: DEFAULT_OPTIONS,
}
let hideTimer: number | null = null
const listeners = new Set<(s: HintState) => void>()
function emit(next: HintState) {
current = next
listeners.forEach((l) => l(current))
}
function clearHideTimer() {
if (hideTimer) {
window.clearTimeout(hideTimer)
hideTimer = null
}
}
export function showHint(text: string, options?: ShowHintOptions) {
if (typeof window === 'undefined') return
clearHideTimer()
const mergedOptions: HintState['options'] = {
...DEFAULT_OPTIONS,
...(options ?? {}),
offset: {
...DEFAULT_OPTIONS.offset,
...(options?.offset ?? {}),
},
}
emit({
visible: true,
text,
options: mergedOptions,
})
if (!mergedOptions.persistent) {
hideTimer = window.setTimeout(() => {
hideHint()
}, mergedOptions.durationMs)
}
}
export function hideHint() {
if (typeof window === 'undefined') return
clearHideTimer()
emit({ ...current, visible: false })
}
export function HintHost() {
const [state, setState] = useState<HintState>(current)
useEffect(() => {
listeners.add(setState)
return () => {
listeners.delete(setState)
}
}, [])
const offsetX = state.options.offset.x ?? 0
const offsetY = state.options.offset.y ?? 0
return (
<div className="fixed inset-0 pointer-events-none" style={{ zIndex: state.options.zIndex }}>
<AnimatePresence>
{state.visible && state.text && (
<motion.div
key="hint"
className="absolute left-1/2"
style={{
top: `calc(50% + ${offsetY}px)`,
x: `calc(-50% + ${offsetX}px)`,
zIndex: state.options.zIndex,
}}
initial={{ opacity: 0, y: 8, filter: 'blur(2px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
exit={{ opacity: 0, y: 8, filter: 'blur(2px)' }}
transition={{ duration: 0.22, ease: 'easeOut' }}
>
<div
className={cn(
'inline-flex items-center justify-center',
'px-30 py-2',
// 'bg-linear-to-r from-transparent from-1% via-[#C0945A] via-50% to-transparent to-99% text-white',
'whitespace-nowrap text-white',
'text-3xl font-semibold tracking-wide',
'select-none'
)}
style={{
backgroundImage: getBackgroundImage('components/提示条'),
backgroundSize: '100% 100%',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
>
{state.text}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
}
+99
View File
@@ -0,0 +1,99 @@
import { useEffect, useMemo, useRef } from 'react'
import { hideHint, showHint } from './HintHost'
import type { HintPositionOffset, ShowHintOptions } from './HintHost'
export type SceneHintProps = {
/** 是否展示提示 */
when: boolean
/** 提示文本(建议直接传 string children) */
children: string
/** 延迟展示(ms) */
delayMs?: number
/** 自动消失时间(ms)。不传则使用默认值(2000) */
durationMs?: number
/** 是否常驻 */
persistent?: boolean
/** 相对中心点微调位置 */
offset?: HintPositionOffset
/** 自定义层级 */
zIndex?: number
/** 进入 when=true 时只展示一次(后续 when=true 的 rerender 不会重复触发) */
once?: boolean
/**
* 触发 key:当此值变化时(且 when=true)会重新触发一次 showHint。
* 用于“同一个提示需要再次弹出”的场景。
*/
triggerKey?: string | number
/** when 变为 false / 卸载时是否自动 hide */
hideOnCleanup?: boolean
}
export function SceneHint({
when,
children,
delayMs = 0,
durationMs,
persistent = false,
offset,
zIndex,
once = false,
triggerKey,
hideOnCleanup = true,
}: SceneHintProps) {
const shownOnceRef = useRef(false)
const delayTimerRef = useRef<number | null>(null)
const text = useMemo(() => {
return typeof children === 'string' ? children : String(children ?? '')
}, [children])
useEffect(() => {
if (!when) {
shownOnceRef.current = false
if (hideOnCleanup) hideHint()
return
}
if (once && shownOnceRef.current) return
const options: ShowHintOptions = {
persistent,
...(durationMs !== undefined ? { durationMs } : null),
...(offset ? { offset } : null),
...(zIndex !== undefined ? { zIndex } : null),
}
if (delayMs > 0) {
delayTimerRef.current = window.setTimeout(() => {
showHint(text, options)
shownOnceRef.current = true
}, delayMs)
} else {
showHint(text, options)
shownOnceRef.current = true
}
return () => {
if (delayTimerRef.current) {
window.clearTimeout(delayTimerRef.current)
delayTimerRef.current = null
}
if (hideOnCleanup) hideHint()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
when,
text,
delayMs,
durationMs,
persistent,
offset?.x,
offset?.y,
zIndex,
once,
triggerKey,
hideOnCleanup,
])
return null
}
+5
View File
@@ -0,0 +1,5 @@
export { HintHost, showHint, hideHint } from './HintHost'
export type { ShowHintOptions, HintPositionOffset } from './HintHost'
export { SceneHint } from './SceneHint'
export type { SceneHintProps } from './SceneHint'