import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import * as forumApi from '../api/forum.js'; import { me } from '../api/auth.js'; import { getToken } from '../api/client.js'; import { uploadFile } from '../api/upload.js'; import { required as captchaRequired, applyCaptchaResult } from '../api/captcha.js'; import { showCaptcha } from '../components/CaptchaModal.jsx'; import { showSnackbar, useDialog } from '../lib/utils.js'; /** 板块的子分类列表 */ function subCatsOf(cat) { if (!cat || !cat.sub_categories) return []; return cat.sub_categories.split(',').filter(Boolean).map((t) => t.trim()); } /** 论坛首页:分类导航(侧栏 + 子分类 chips 筛选)+ 帖子列表 + 发帖弹窗(含验证码) */ export default function Forum() { const navigate = useNavigate(); const [categories, setCategories] = useState([]); const [currentCatId, setCurrentCatId] = useState(null); // null=全部最新 const [filterSub, setFilterSub] = useState(''); const [posts, setPosts] = useState(null); // null=加载中 const [loadError, setLoadError] = useState(''); const [user, setUser] = useState(null); // 发帖弹窗状态 const [showNewPost, setShowNewPost] = useState(false); const [npCategory, setNpCategory] = useState(''); const [npSub, setNpSub] = useState(''); const [npTitle, setNpTitle] = useState(''); const [npContent, setNpContent] = useState(''); const [npStatus, setNpStatus] = useState(''); const [submitting, setSubmitting] = useState(false); const fileRef = useRef(null); const load = useCallback(async (catId, sub) => { setPosts(null); setLoadError(''); try { const ps = catId ? await forumApi.listPosts(catId) : await forumApi.listPosts(); const filtered = sub ? (ps || []).filter((p) => p.sub_category === sub) : (ps || []); setPosts(filtered); } catch (e) { setLoadError(e.message || '加载失败'); setPosts([]); } }, []); useEffect(() => { forumApi.listCategories().then((cs) => setCategories(cs || [])).catch(() => {}); if (getToken()) me().then(setUser).catch(() => setUser(null)); else setUser(null); load(null, ''); }, [load]); const selectCategory = (catId) => { setCurrentCatId(catId); setFilterSub(''); load(catId, ''); }; const showAllPosts = () => { setCurrentCatId(null); setFilterSub(''); load(null, ''); }; // 发帖按钮:未登录显示"登录发帖"并跳登录页(照 forum.js) const handleNewPostBtn = () => { if (user) { setNpCategory(categories.length ? String(categories[0].id) : ''); setNpSub(''); setNpTitle(''); setNpContent(''); setNpStatus(''); setShowNewPost(true); } else { navigate('/login.html'); } }; const handleCategoryChange = (e) => { setNpCategory(e.target.value); setNpSub(''); // 切换板块后重置子分类 }; const doUpload = async (file) => { try { const data = await uploadFile(file); setNpContent((c) => c + '\n' + data.tag + '\n'); setNpStatus('已插入: ' + data.tag); } catch (e) { showSnackbar(e.message); } }; const handleFileSelect = (e) => { const f = e.target.files && e.target.files[0]; if (f) doUpload(f); e.target.value = ''; }; const submitPost = async () => { if (!npTitle.trim() || !npContent.trim()) { showSnackbar('标题和内容不能为空'); return; } setSubmitting(true); try { const base = { category_id: parseInt(npCategory, 10), title: npTitle.trim(), content: npContent.trim(), sub_category: npSub, use_markdown: 1, }; // 验证码:captcha_forum 开启时走 showCaptcha 拿 proof / 第三方 token const cap = await captchaRequired('forum'); if (cap && cap.required) { const result = await showCaptcha('forum'); if (result === null) { setSubmitting(false); return; } // 取消 applyCaptchaResult(base, result); } await forumApi.createPost(base); showSnackbar('发布成功'); setShowNewPost(false); if (currentCatId) selectCategory(currentCatId); else showAllPosts(); } catch (e) { showSnackbar(e.message); } setSubmitting(false); }; const currentCat = categories.find((c) => c.id === currentCatId); const subCats = subCatsOf(currentCat); // 发帖弹窗焦点管理(B3):Esc 关闭、聚焦首个输入、关闭后焦点还给触发按钮 const { dialogRef: npDialogRef, onKeyDown: npDialogKey } = useDialog(showNewPost, () => setShowNewPost(false)); return (
{currentCat.description || ''}
{currentCat.announcement && (加载失败
暂无帖子