码桶
发现社区成员的开源项目
RssFeedManager.tsx4.1 KB
import { useEffect, useState, useCallback } from 'react'
import { api } from '@/lib/api'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Loader2, Plus, Trash2, Rss } from 'lucide-react'
import { toast } from 'sonner'
interface Feed {
id: number
title: string
feed_url: string
}
/**
* RSS 订阅源管理:每个订阅源 = 名称 + 订阅地址,支持添加多个、删除。
* 后端接口:/api/market/rss_list / rss_add / rss_del(按 bot_id 隔离)。
*/
export function RssFeedManager({ botId }: { botId: number }) {
const [feeds, setFeeds] = useState<Feed[]>([])
const [title, setTitle] = useState('')
const [url, setUrl] = useState('')
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const load = useCallback(async () => {
setLoading(true)
try {
const d = await api.post('/api/market/rss_list', { bot_id: botId })
if (d.ok) setFeeds(d.feeds || [])
} catch (e: any) {
toast.error(e.message)
} finally {
setLoading(false)
}
}, [botId])
useEffect(() => { load() }, [load])
async function add() {
const t = title.trim()
const u = url.trim()
if (!/^https?:\/\//i.test(u)) {
toast.error('请输入有效的 http(s) 订阅源地址')
return
}
setSaving(true)
try {
const d = await api.post('/api/market/rss_add', { bot_id: botId, title: t, feed_url: u })
if (!d.ok) throw new Error(d.msg)
toast.success('已添加订阅')
setTitle('')
setUrl('')
load()
} catch (e: any) {
toast.error(e.message)
} finally {
setSaving(false)
}
}
async function del(id: number) {
try {
const d = await api.post('/api/market/rss_del', { bot_id: botId, id })
if (!d.ok) throw new Error(d.msg)
setFeeds(f => f.filter(x => x.id !== id))
} catch (e: any) {
toast.error(e.message)
}
}
return (
<div className="space-y-3">
<p className="text-[11px] leading-relaxed text-muted-foreground">
可添加多个订阅源(名称 + 地址)。向机器人发送「最新」即推送各订阅源的最新文章。
</p>
{loading ? (
<div className="flex justify-center py-2"><Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /></div>
) : feeds.length === 0 ? (
<p className="text-[11px] text-muted-foreground">尚未添加任何订阅源。</p>
) : (
<ul className="space-y-1.5">
{feeds.map(f => (
<li key={f.id} className="flex items-center gap-2 rounded-md border px-2 py-1.5">
<Rss className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium">{f.title || '(未命名)'}</div>
<div className="truncate text-[10px] text-muted-foreground">{f.feed_url}</div>
</div>
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0" onClick={() => del(f.id)} aria-label="删除订阅">
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button>
</li>
))}
</ul>
)}
<div className="space-y-1.5">
<Input
value={title}
placeholder="名称(如:少数派 / 自定义,可留空)"
onChange={e => setTitle(e.target.value)}
className="h-7 text-xs"
/>
<div className="flex items-center gap-1">
<Input
value={url}
placeholder="订阅地址(https://.../feed.xml)"
onChange={e => setUrl(e.target.value)}
className="h-7 text-xs"
/>
<Button size="sm" className="h-7 w-7 shrink-0 p-0" disabled={saving} onClick={add} aria-label="添加订阅">
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
)
}