'use client'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useTextAnimation } from '../hooks/useGsapAnimation'; import { ButtonProps, styled, TextField, Button, Stack, Box, alpha, lighten, } from '@mui/material'; import { StyledTopicBox } from '../component/styledCommon'; const StyledBanner = styled('div')(({ theme }) => ({ backgroundColor: alpha(theme.palette.primary.main, 0.03), backgroundImage: `radial-gradient(${alpha(theme.palette.primary.main, 0.08)} 2px, transparent 1px)`, backgroundSize: '36px 36px', // dot spacing backgroundPosition: '0 0', backgroundRepeat: 'repeat', marginTop: theme.spacing(-10), })); const StyledTitle = styled('h1')(({ theme }) => ({ fontSize: 60, fontWeight: 700, wordBreak: 'break-all', color: theme.palette.primary.main, marginBottom: theme.spacing(3), [theme.breakpoints.down('md')]: { fontSize: 50, }, [theme.breakpoints.down('sm')]: { fontSize: 40, }, })); const StyledSubTitle = styled('h2')(({ theme }) => ({ fontWeight: 400, marginBottom: theme.spacing(5), color: theme.palette.text.primary, })); const StyledSearchBox = styled(Box)(({ theme }) => ({ position: 'relative', width: '100%', padding: theme.spacing(2), boxShadow: `0 2px 10px 0px ${alpha(theme.palette.text.primary, 0.1)}`, border: `1px solid transparent`, borderRadius: '10px', backgroundColor: theme.palette.background.default, '&:hover': { borderColor: alpha(theme.palette.primary.main, 0.4), }, '&:focus-within': { borderColor: theme.palette.primary.main, }, })); const StyledTextField = styled(TextField)(({ theme }) => ({ '.MuiInputBase-root': { padding: 0, }, fieldset: { border: 'none', }, '& input::placeholder, & textarea::placeholder': { color: alpha(theme.palette.text.primary, 0.5), opacity: 1, }, })); // 闪烁光标样式 const blinkAnimation = ` @keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } } `; const StyledCursor = styled('span')(({ theme }) => ({ display: 'inline-block', width: '1px', height: '18px', backgroundColor: alpha(theme.palette.text.primary, 1), marginLeft: '2px', animation: 'blink 1s infinite', flexShrink: 0, })); const StyledHotItem = styled(Box)(({ theme }) => ({ color: theme.palette.text.primary, padding: theme.spacing(0.75, 2), borderRadius: '16px', border: `1px solid ${alpha(theme.palette.text.primary, 0.1)}`, fontSize: 12, cursor: 'pointer', transition: 'all 0.2s', '&:hover': { borderColor: alpha(theme.palette.primary.main, 0.1), color: theme.palette.primary.main, }, })); interface SearchSuggestion { id: string; title: string; description?: string; type?: 'recent' | 'suggestion' | 'trending'; } interface BannerProps { title: { text: string; fontSize: string; color: string; }; subtitle: { text: string; fontSize: string; color: string; }; bg_url?: string; search: { placeholder: string; hot: string[]; }; btns: { type: ButtonProps['variant']; text: string; href: string; }[]; onSearch?: (value: string, type?: 'search' | 'chat') => void; onSearchSuggestions?: (query: string) => Promise; basePath?: string; } const Banner = React.memo( ({ title, subtitle, bg_url, search, btns = [], onSearch, onSearchSuggestions, basePath = '', }: BannerProps) => { const [searchText, setSearchText] = useState(''); const [suggestions, setSuggestions] = useState([]); const [isLoading, setIsLoading] = useState(false); const [anchorEl, setAnchorEl] = useState(null); const [anchorElWidth, setAnchorElWidth] = useState(null); const [selectedIndex, setSelectedIndex] = useState(-1); const [isFocused, setIsFocused] = useState(false); const [typedText, setTypedText] = useState(''); const debounceTimer = useRef(null); const typewriterTimer = useRef(null); // 添加文字动画效果 const titleRef = useTextAnimation(0, 0.1); const subtitleRef = useTextAnimation(0.2, 0.1); // 打字机效果 useEffect(() => { if (isFocused || !search.hot || search.hot.length === 0) { return; } let currentIndex = 0; let currentCharIndex = 0; let isDeleting = false; let isPaused = false; const typeWriter = () => { const currentWord = search.hot[currentIndex]; if (isPaused) { typewriterTimer.current = setTimeout(() => { isPaused = false; typeWriter(); }, 1000); // 暂停1秒 return; } if (!isDeleting) { // 打字阶段 if (currentCharIndex < currentWord.length) { setTypedText(currentWord.substring(0, currentCharIndex + 1)); currentCharIndex++; typewriterTimer.current = setTimeout(typeWriter, 100); // 打字速度(调慢) } else { // 打完了,暂停后开始删除 isPaused = true; isDeleting = true; typeWriter(); } } else { // 删除阶段 if (currentCharIndex > 0) { currentCharIndex--; setTypedText(currentWord.substring(0, currentCharIndex)); typewriterTimer.current = setTimeout(typeWriter, 80); // 删除速度(调慢) } else { // 删完了,切换到下一个词 isDeleting = false; currentIndex = (currentIndex + 1) % search.hot.length; typewriterTimer.current = setTimeout(typeWriter, 200); // 切换词之间的延迟 } } }; typeWriter(); return () => { if (typewriterTimer.current) { clearTimeout(typewriterTimer.current); } }; }, [isFocused, search.hot]); // 防抖搜索 const debouncedSearch = useCallback( (query: string) => { if (debounceTimer.current) { clearTimeout(debounceTimer.current); } debounceTimer.current = setTimeout(async () => { if (query.trim() && onSearchSuggestions) { setIsLoading(true); try { const results = await onSearchSuggestions(query); setSuggestions(results); } catch (error) { console.error('搜索建议获取失败:', error); setSuggestions([]); } finally { setIsLoading(false); } } else { setSuggestions([]); } }, 300); }, [onSearchSuggestions], ); // 处理输入变化 const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; setSearchText(value); setSelectedIndex(-1); if (value.trim()) { debouncedSearch(value); if (onSearch) { setAnchorEl(e.currentTarget.parentElement); setAnchorElWidth(e.currentTarget.parentElement?.offsetWidth || 0); } } else { setSuggestions([]); setAnchorEl(null); } }; // 处理键盘事件 const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { // e.preventDefault(); // if (selectedIndex >= 0 && suggestions[selectedIndex]) { // const selectedSuggestion = suggestions[selectedIndex]; // setSearchText(selectedSuggestion.title); // onSearch?.(selectedSuggestion.title); // } else { // onSearch?.(searchText); // } onSearch?.(searchText, 'chat'); setSearchText(''); setAnchorEl(null); setSelectedIndex(-1); } else if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex(prev => prev < suggestions.length - 1 ? prev + 1 : prev, ); } else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex(prev => (prev > 0 ? prev - 1 : -1)); } else if (e.key === 'Escape') { setAnchorEl(null); setSelectedIndex(-1); } }; // 处理输入框聚焦 const handleInputFocus = (e: React.FocusEvent) => { setIsFocused(true); setTypedText(''); // 清空打字机文本 if (searchText.trim()) { setAnchorEl(e.currentTarget.parentElement); setAnchorElWidth(e.currentTarget.parentElement?.offsetWidth || 0); } }; // 处理输入框失焦 const handleInputBlur = () => { setIsFocused(false); }; // 清理定时器 useEffect(() => { return () => { if (debounceTimer.current) { clearTimeout(debounceTimer.current); } }; }, []); return ( {title.text} {/* {subtitle.text && ( */} {subtitle.text} {/* )} */} {!isFocused && !searchText && typedText && ( alpha(theme.palette.text.primary, 0.85), fontSize: '16px', lineHeight: 1.5, display: 'inline-flex', alignItems: 'center', whiteSpace: 'pre-wrap', wordBreak: 'break-word', }} > {typedText} )} {search.hot?.map(hot => ( onSearch?.(hot)}> {hot} ))} {btns.length > 0 && ( {btns.map(btn => ( ))} )} ); }, ); export default Banner;