import { useCallback, useEffect, useMemo, useState } from 'react' import '@fortawesome/fontawesome-free/css/all.min.css' import './App.css' const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || '/api').replace(/\/$/, '') const ADMIN_URL = import.meta.env.VITE_ADMIN_URL || 'http://localhost:5002/admin/login' const DEFAULT_FORM = Object.freeze({ name: '', message: '', gender: '保密', qq: '', }) const BACKGROUND_IMAGES = Array.from({ length: 29 }, (_, index) => `/background/image${index + 1}.png`) const formatCount = (value) => { const num = typeof value === 'number' ? value : Number(value || 0) return Number.isNaN(num) ? 0 : num } function App() { const [formData, setFormData] = useState({ ...DEFAULT_FORM }) const [limits, setLimits] = useState({ name: 7, message: 100 }) const [stats, setStats] = useState({ total_bottles: 0 }) const [motto, setMotto] = useState('载入中...') const [throwStatus, setThrowStatus] = useState('') const [pickupStatus, setPickupStatus] = useState('') const [currentBottle, setCurrentBottle] = useState(null) const [cooldowns, setCooldowns] = useState({ throw: 0, pickup: 0 }) const [loadingAction, setLoadingAction] = useState({ throw: false, pickup: false }) const [reactionDisabled, setReactionDisabled] = useState(false) const isThrowing = loadingAction.throw const isPicking = loadingAction.pickup const randomBackground = useMemo(() => { if (!BACKGROUND_IMAGES.length) { return '' } const index = Math.floor(Math.random() * BACKGROUND_IMAGES.length) return BACKGROUND_IMAGES[index] }, []) useEffect(() => { if (randomBackground) { document.documentElement.style.setProperty('--app-background-image', `url(${randomBackground})`) } }, [randomBackground]) const startCooldown = useCallback((type, seconds = 5) => { const duration = Math.max(1, Math.ceil(seconds)) setCooldowns((prev) => ({ ...prev, [type]: duration })) }, []) useEffect(() => { const timer = setInterval(() => { setCooldowns((prev) => { const next = { throw: prev.throw > 0 ? prev.throw - 1 : 0, pickup: prev.pickup > 0 ? prev.pickup - 1 : 0, } if (next.throw === prev.throw && next.pickup === prev.pickup) { return prev } return next }) }, 1000) return () => clearInterval(timer) }, []) const fetchConfig = useCallback(async () => { try { const response = await fetch(`${API_BASE_URL}/config`) const data = await response.json() if (data.success && data.config) { setLimits({ name: data.config.name_limit ?? 7, message: data.config.message_limit ?? 100, }) } } catch (error) { console.error('Failed to fetch config', error) } }, []) const fetchStats = useCallback(async () => { try { const response = await fetch(`${API_BASE_URL}/stats`) const data = await response.json() if (data.success && data.stats) { setStats(data.stats) } } catch (error) { console.error('Failed to fetch stats', error) } }, []) const fetchMotto = useCallback(async () => { try { const response = await fetch(`${API_BASE_URL}/motto`) const data = await response.json() if (data.success) { setMotto(data.motto) } } catch (error) { console.error('Failed to fetch motto', error) setMotto('良言一句三冬暖,恶语伤人六月寒') } }, []) useEffect(() => { fetchConfig() fetchStats() fetchMotto() }, [fetchConfig, fetchStats, fetchMotto]) const handleInputChange = useCallback((event) => { const { name, value } = event.target if (name === 'qq' && value && /[^0-9]/.test(value)) { return } setFormData((prev) => ({ ...prev, [name]: value })) }, []) const resetForm = useCallback(() => { setFormData({ ...DEFAULT_FORM }) }, []) const handleThrowSubmit = useCallback( async (event) => { event.preventDefault() if (cooldowns.throw > 0 || isThrowing) { return } setLoadingAction((prev) => ({ ...prev, throw: true })) setThrowStatus('正在扔瓶子...') try { const response = await fetch(`${API_BASE_URL}/throw`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: formData.name, message: formData.message, gender: formData.gender, qq: formData.qq, }), }) const data = await response.json() if (response.ok && data.success) { setThrowStatus('瓶子已成功扔出!祝你好运~') resetForm() fetchStats() startCooldown('throw', 5) } else { const waitTime = data.wait_time ?? 0 setThrowStatus(`出错了: ${data.error || data.message || '未知错误'}`) if (waitTime) { startCooldown('throw', waitTime) } } } catch (error) { console.error('Throw request failed', error) setThrowStatus('请求失败,请检查网络连接。') } finally { setLoadingAction((prev) => ({ ...prev, throw: false })) } }, [cooldowns.throw, isThrowing, formData, fetchStats, resetForm, startCooldown], ) const handlePickup = useCallback(async () => { if (cooldowns.pickup > 0 || isPicking) { return } setLoadingAction((prev) => ({ ...prev, pickup: true })) setPickupStatus('正在打捞瓶子...') try { const response = await fetch(`${API_BASE_URL}/pickup`) const data = await response.json() if (response.ok && data.success && data.bottle) { setCurrentBottle(data.bottle) setReactionDisabled(false) setPickupStatus('捡到了一个瓶子!缘分来了~') startCooldown('pickup', 5) } else { setCurrentBottle(null) const waitTime = data.wait_time ?? 0 setPickupStatus(data.message || data.error || '海里没有瓶子了,或者出错了。') if (waitTime) { startCooldown('pickup', waitTime) } } } catch (error) { console.error('Pickup request failed', error) setPickupStatus('请求失败,请检查网络连接。') setCurrentBottle(null) } finally { setLoadingAction((prev) => ({ ...prev, pickup: false })) } }, [cooldowns.pickup, isPicking, startCooldown]) const handleReaction = useCallback( async (reaction) => { if (!currentBottle) { return } setReactionDisabled(true) try { const response = await fetch(`${API_BASE_URL}/react`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bottle_id: currentBottle.id, reaction }), }) const data = await response.json() if (response.ok && data.success) { setCurrentBottle((prev) => prev ? { ...prev, likes: reaction === 'like' ? formatCount(prev.likes) + 1 : prev.likes, dislikes: reaction === 'dislike' ? formatCount(prev.dislikes) + 1 : prev.dislikes, } : prev, ) setPickupStatus( reaction === 'like' ? '感谢您的点赞!' : '已记录您的反馈,该瓶子被捡起的概率将会降低。', ) } else { setPickupStatus(data.error || '记录反馈时出错。') setReactionDisabled(false) } } catch (error) { console.error('Reaction request failed', error) setPickupStatus('请求失败,请稍后再试。') setReactionDisabled(false) } }, [currentBottle], ) const nameCharCount = useMemo(() => formData.name.length, [formData.name]) const messageCharCount = useMemo(() => formData.message.length, [formData.message]) return (