码桶

发现社区成员的开源项目

奶狗 /

ng-webot

公开
main
ng-webot/web/src/components/RssFeedManager.tsx
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>
  )
}