'use client'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useAppStore } from '../store/useAppStore'; import { useI18n } from '../lib/useI18n'; const ONBOARDING_KEY = 'author-onboarding-done'; export default function TourOverlay({ onOpenHelp }) { const { startTour, setStartTour, language, visualTheme } = useAppStore(); const { t } = useI18n(); const [status, setStatus] = useState('hidden'); // hidden, intro, tour const [currentStepIndex, setCurrentStepIndex] = useState(0); const [targetRect, setTargetRect] = useState(null); const [windowSize, setWindowSize] = useState({ w: 0, h: 0 }); // 根据当前语言动态生成 TOUR_STEPS const TOUR_STEPS = useMemo(() => [ { targetId: 'tour-new-chapter', title: t('tour.step1Title'), content: t('tour.step1Content'), placement: 'right' }, { targetId: 'tour-editor', title: t('tour.step2Title'), content: t('tour.step2Content'), placement: 'center' }, { targetId: 'tour-ai-btn', title: t('tour.step3Title'), content: t('tour.step3Content'), placement: 'left' }, { targetId: 'tour-settings', title: t('tour.step4Title'), content: t('tour.step4Content'), placement: 'top' }, { targetId: 'tour-help', title: t('tour.step5Title'), content: t('tour.step5Content'), placement: 'top' }, { targetId: 'tour-github', title: t('tour.step6Title'), content: t('tour.step6Content'), placement: 'top' } ], [t]); useEffect(() => { if (typeof window !== 'undefined') { const isDone = localStorage.getItem(ONBOARDING_KEY); if (!isDone) { if (language && visualTheme) { setStatus('intro'); } } setWindowSize({ w: window.innerWidth, h: window.innerHeight }); } }, [language, visualTheme]); const updateRect = useCallback((index) => { const step = TOUR_STEPS[index]; if (!step) return; let el = document.getElementById(step.targetId); if (!el && step.targetId === 'tour-editor') { el = document.querySelector('.editor-container'); } if (el) { const rect = el.getBoundingClientRect(); setTargetRect({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }); el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); } else { setTimeout(() => { const retryEl = document.getElementById(step.targetId) || document.querySelector('.editor-container'); if (retryEl) { const r = retryEl.getBoundingClientRect(); setTargetRect({ x: r.x, y: r.y, width: r.width, height: r.height }); } else { // Fallback: show tooltip centered when target element not found const cx = window.innerWidth / 2; const cy = window.innerHeight / 2; setTargetRect({ x: cx - 1, y: cy - 1, width: 2, height: 2 }); } }, 300); } }, [TOUR_STEPS]); const finishTour = () => { localStorage.setItem(ONBOARDING_KEY, 'true'); setStatus('hidden'); }; const skipToHelp = () => { setStatus('tour'); const helpStepIndex = TOUR_STEPS.length - 2; setCurrentStepIndex(helpStepIndex); updateRect(helpStepIndex); }; const beginTour = useCallback(() => { setStatus('tour'); setCurrentStepIndex(0); updateRect(0); }, [updateRect]); useEffect(() => { if (startTour) { beginTour(); setStartTour(false); } }, [startTour, beginTour, setStartTour]); // 监听窗口缩放和滚动 useEffect(() => { if (status !== 'tour') return; const handleResize = () => { setWindowSize({ w: window.innerWidth, h: window.innerHeight }); updateRect(currentStepIndex); }; window.addEventListener('resize', handleResize); window.addEventListener('scroll', handleResize, true); return () => { window.removeEventListener('resize', handleResize); window.removeEventListener('scroll', handleResize, true); }; }, [status, currentStepIndex, updateRect]); const nextStep = () => { if (currentStepIndex === TOUR_STEPS.length - 1) { finishTour(); } else { setCurrentStepIndex(i => i + 1); updateRect(currentStepIndex + 1); } }; const prevStep = () => { if (currentStepIndex > 0) { setCurrentStepIndex(i => i - 1); updateRect(currentStepIndex - 1); } }; if (status === 'hidden') return null; // ================= Intro 弹窗 ================= if (status === 'intro') { return (
✨

{t('tour.introTitle')}

{t('tour.introSubtitle')}

{t('tour.introHint')}

); } // ================= 具体步骤向导 (Tour) ================= const step = TOUR_STEPS[currentStepIndex]; if (!step) return null; const padding = 8; const rX = targetRect ? targetRect.x - padding : 0; const rY = targetRect ? targetRect.y - padding : 0; const rW = targetRect ? targetRect.width + padding * 2 : 0; const rH = targetRect ? targetRect.height + padding * 2 : 0; const radius = 8; let tooltipStyle = { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }; if (targetRect) { if (step.placement === 'right') { tooltipStyle = { top: rY + rH / 2, left: rX + rW + 20, transform: 'translateY(-50%)' }; } else if (step.placement === 'left') { tooltipStyle = { top: rY + rH / 2, right: windowSize.w - rX + 20, transform: 'translateY(-50%)' }; } else if (step.placement === 'bottom') { tooltipStyle = { top: rY + rH + 20, left: rX + rW / 2, transform: 'translateX(-50%)' }; } else if (step.placement === 'top') { tooltipStyle = { bottom: windowSize.h - rY + 20, left: rX + rW / 2, transform: 'translateX(-50%)' }; } else if (step.placement === 'center') { tooltipStyle = { top: rY + rH / 2, left: rX + rW / 2, transform: 'translate(-50%, -50%)' }; } } const tooltipHeightGuess = 200; if (tooltipStyle.top && typeof tooltipStyle.top === 'number') { if (tooltipStyle.top < tooltipHeightGuess / 2) { tooltipStyle.top = tooltipHeightGuess / 2; } else if (tooltipStyle.top > windowSize.h - tooltipHeightGuess / 2) { tooltipStyle.top = windowSize.h - tooltipHeightGuess / 2; } } if (tooltipStyle.bottom && typeof tooltipStyle.bottom === 'number') { if (tooltipStyle.bottom < 20) { tooltipStyle.bottom = 20; } } const validateStyle = { ...tooltipStyle }; return (
{targetRect && ( )} {targetRect && ( )} {targetRect && (

{step.title}

{currentStepIndex + 1} / {TOUR_STEPS.length}
{step.content}
{currentStepIndex === TOUR_STEPS.length - 1 ? (
) : ( )}
{currentStepIndex > 0 && ( )} {currentStepIndex === TOUR_STEPS.length - 1 ? ( ) : ( )}
)}
); }