import React, { useCallback, useEffect, useState } from 'react'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; import Switch from '@mui/material/Switch'; import FormControlLabel from '@mui/material/FormControlLabel'; import TextField from '@mui/material/TextField'; import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import IconButton from '@mui/material/IconButton'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import RssFeedIcon from '@mui/icons-material/RssFeed'; import { getSettings, saveSettings } from '../../api/settings.js'; import { listCategories, updateCategoryProfile } from '../../api/forum.js'; import { showSnack } from '../snack.jsx'; /** 完整订阅 URL(运行时拼 origin,不硬编码域名) */ function feedUrl(path) { return window.location.origin + path; } /** 版块图标小方块:icon 非 http 渲染 emoji/首字 */ function CatIcon({ c }) { const isImg = /^https?:\/\//i.test(c.icon || ''); const letter = isImg ? (c.name || '?').charAt(0) : (c.icon || (c.name || '?').charAt(0)); return ( {letter} ); } /** * RSS 订阅管理(admin,路由 /admin/rss): * 1) 源管理:博客(常开只读)/ 论坛全站(feed_forum_enabled)/ 各版块(feed_enabled,即时保存) * 2) 内容设置:feed_show_full(全文/摘要)+ feed_max_items(1-100)→ saveSettings */ export default function RssManage() { const [cats, setCats] = useState([]); const [forumFeed, setForumFeed] = useState(true); const [showFull, setShowFull] = useState('0'); const [maxItems, setMaxItems] = useState('20'); const [saving, setSaving] = useState(false); const load = useCallback(() => { getSettings() .then((s) => { setForumFeed(s.feed_forum_enabled !== '0'); // 默认开启 setShowFull(s.feed_show_full === '1' ? '1' : '0'); setMaxItems(s.feed_max_items || '20'); }) .catch((e) => showSnack(e.message, 'error')); listCategories() .then((cs) => setCats(cs || [])) .catch((e) => showSnack(e.message, 'error')); }, []); useEffect(() => { load(); }, [load]); const copyUrl = async (url) => { try { await navigator.clipboard.writeText(url); showSnack('链接已复制'); } catch { showSnack('复制失败,请手动复制', 'error'); } }; // 论坛全站开关:即时保存 const toggleForumFeed = async (next) => { setForumFeed(next); try { await saveSettings({ feed_forum_enabled: next ? '1' : '0' }); showSnack(next ? '已开启论坛订阅' : '已关闭论坛订阅'); } catch (e) { setForumFeed(!next); showSnack(e.message, 'error'); } }; // 版块级开关:即时保存(feed_enabled 已进 profile 白名单) const toggleCatFeed = async (cat, next) => { const prev = cat.feed_enabled !== 0 && cat.feed_enabled !== '0'; setCats((cs) => cs.map((c) => (c.id === cat.id ? { ...c, feed_enabled: next ? 1 : 0 } : c))); try { await updateCategoryProfile(cat.id, { feed_enabled: next ? '1' : '0' }); showSnack(next ? `已开启「${cat.name}」订阅` : `已关闭「${cat.name}」订阅`); } catch (e) { setCats((cs) => cs.map((c) => (c.id === cat.id ? { ...c, feed_enabled: prev ? 1 : 0 } : c))); showSnack(e.message, 'error'); } }; const saveContent = async () => { const n = parseInt(maxItems, 10); if (isNaN(n) || n < 1 || n > 100) { showSnack('条目数需为 1-100 的数字', 'error'); return; } setSaving(true); try { await saveSettings({ feed_show_full: showFull, feed_max_items: String(n) }); showSnack('内容设置已保存'); } catch (e) { showSnack(e.message, 'error'); } setSaving(false); }; /** 源行:名称/描述 + URL(复制)+ 开关 */ const SourceRow = ({ name, desc, path, onToggle, checked, disabled, badge }) => ( {name} {badge} {desc && {desc}} {feedUrl(path)} copyUrl(feedUrl(path))} title="复制链接" sx={{ flexShrink: 0 }}> {disabled ? ( ) : ( onToggle(e.target.checked)} />} label="" sx={{ m: 0, flexShrink: 0 }} /> )} ); return ( RSS 订阅 管理各位置的订阅源:订阅器(如 Feedly / Inoreader)可关注这些链接获取最新内容 {/* 源管理 */} 订阅源 {cats.length > 0 && ( 版块级订阅(关闭后该版块不进入全站源,其独立源返回 404) {cats.map((c) => { const on = c.feed_enabled !== 0 && c.feed_enabled !== '0'; return ( {c.name} {feedUrl(`/feed/forum/c/${c.id}.xml`)} copyUrl(feedUrl(`/feed/forum/c/${c.id}.xml`))} title="复制链接" sx={{ flexShrink: 0 }}> toggleCatFeed(c, e.target.checked)} />} label="" sx={{ m: 0, flexShrink: 0 }} /> ); })} )} {/* 内容设置 */} 内容设置 setShowFull(e.target.checked ? '1' : '0')} />} label={( 输出全文 关闭则仅输出摘要(博客用文章摘要,论坛用内容截断) )} /> setMaxItems(e.target.value)} inputProps={{ min: 1, max: 100 }} helperText="1-100,默认 20" sx={{ mt: 1.5, maxWidth: 220 }} /> ); }