| import { Alert, Button, Card, Empty, Input, Space, Table, Typography } from 'antd' |
| import { useCallback, useEffect, useMemo, useState } from 'react' |
| import { useSearchParams } from 'react-router-dom' |
| import type { ApiError } from '../lib/api' |
| import { isOrchestratorDbUnavailable, listRawNotes, type RawNoteRecord } from '../lib/content' |
|
|
| export default function RawNotesPage() { |
| const [searchParams, setSearchParams] = useSearchParams() |
| const [loading, setLoading] = useState(false) |
| const [error, setError] = useState<ApiError | null>(null) |
| const [notes, setNotes] = useState<RawNoteRecord[]>([]) |
| const [total, setTotal] = useState(0) |
|
|
| const page = useMemo(() => { |
| const raw = Number(searchParams.get('page') || '1') |
| return Number.isFinite(raw) && raw > 0 ? raw : 1 |
| }, [searchParams]) |
|
|
| const pageSize = useMemo(() => { |
| const raw = Number(searchParams.get('page_size') || '20') |
| return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 200) : 20 |
| }, [searchParams]) |
|
|
| const query = useMemo(() => String(searchParams.get('query') || '').trim(), [searchParams]) |
| const [queryInput, setQueryInput] = useState(query) |
|
|
| useEffect(() => { |
| setQueryInput(query) |
| }, [query]) |
|
|
| const setParam = useCallback( |
| (patch: Record<string, string | undefined>) => { |
| const next = new URLSearchParams(searchParams) |
| Object.entries(patch).forEach(([k, v]) => { |
| const val = String(v || '').trim() |
| if (val) next.set(k, val) |
| else next.delete(k) |
| }) |
| setSearchParams(next) |
| }, |
| [searchParams, setSearchParams], |
| ) |
|
|
| const loadList = useCallback(async () => { |
| setLoading(true) |
| setError(null) |
| try { |
| const res = await listRawNotes({ |
| limit: pageSize, |
| offset: (page - 1) * pageSize, |
| query: query || undefined, |
| }) |
| setNotes(res.notes || []) |
| setTotal(res.total || 0) |
| } catch (e) { |
| setError(e as ApiError) |
| } finally { |
| setLoading(false) |
| } |
| }, [page, pageSize, query]) |
|
|
| useEffect(() => { |
| void loadList() |
| }, [loadList]) |
|
|
| const columns = useMemo( |
| () => [ |
| { title: 'ID', dataIndex: 'id', width: 90 }, |
| { |
| title: '作者', |
| dataIndex: 'author', |
| width: 160, |
| render: (v: unknown) => (v ? String(v) : '—'), |
| }, |
| { |
| title: 'URL', |
| dataIndex: 'url', |
| width: 340, |
| ellipsis: true, |
| render: (v: unknown) => { |
| const url = String(v || '').trim() |
| if (!url) return '—' |
| return ( |
| <a href={url} target="_blank" rel="noreferrer"> |
| {url} |
| </a> |
| ) |
| }, |
| }, |
| { |
| title: '内容', |
| dataIndex: 'content', |
| ellipsis: true, |
| render: (v: unknown) => { |
| const text = String(v || '').trim() |
| return text ? <Typography.Text>{text}</Typography.Text> : '—' |
| }, |
| }, |
| { |
| title: '来源', |
| dataIndex: 'source_platform', |
| width: 120, |
| render: (v: unknown) => (v ? String(v) : '—'), |
| }, |
| { |
| title: '创建时间', |
| dataIndex: 'created_at', |
| width: 200, |
| render: (v: unknown) => (v ? String(v) : '—'), |
| }, |
| ], |
| [], |
| ) |
|
|
| const dbMissing = isOrchestratorDbUnavailable(error) |
|
|
| return ( |
| <div style={{ display: 'grid', gap: 16 }}> |
| <Typography.Title level={3} style={{ margin: 0 }}> |
| 原始笔记库 |
| </Typography.Title> |
| |
| <Card |
| title="GET /api/v1/content/raw-notes" |
| extra={ |
| <Space wrap> |
| <Input.Search |
| allowClear |
| placeholder="搜索 author / url / content" |
| value={queryInput} |
| onChange={(e) => setQueryInput(e.target.value)} |
| onSearch={(value) => setParam({ query: value.trim() || undefined, page: '1' })} |
| style={{ width: 320 }} |
| /> |
| <Button onClick={loadList} disabled={loading}> |
| 刷新 |
| </Button> |
| </Space> |
| } |
| loading={loading} |
| > |
| {dbMissing ? ( |
| <Empty |
| image={Empty.PRESENTED_IMAGE_SIMPLE} |
| description={ |
| <div style={{ maxWidth: 680 }}> |
| <Typography.Text strong>内容库数据库不可用</Typography.Text> |
| <Typography.Paragraph style={{ marginTop: 8, marginBottom: 0 }}> |
| 服务端返回 503,通常表示 ORCHESTRATOR_DB_PATH 未配置、路径不存在或 DB 尚未初始化。 |
| <br /> |
| 处理方式: |
| <br /> |
| 1)配置 ORCHESTRATOR_DB_PATH 指向可读的 sqlite 文件(默认 orchestrator/data/mvp.db) |
| <br /> |
| 2)初始化 DB:python Spider_XHS/orchestrator/db_init.py |
| </Typography.Paragraph> |
| </div> |
| } |
| /> |
| ) : error ? ( |
| <Alert |
| type="error" |
| showIcon |
| message={error.status ? `HTTP ${error.status}` : '请求失败'} |
| description={error.message} |
| style={{ marginBottom: 16 }} |
| /> |
| ) : null} |
| |
| <Table |
| size="middle" |
| rowKey="id" |
| columns={columns} |
| dataSource={notes} |
| pagination={{ |
| current: page, |
| pageSize, |
| total, |
| showSizeChanger: true, |
| pageSizeOptions: [10, 20, 50, 100, 200], |
| onChange: (nextPage, nextSize) => setParam({ page: String(nextPage), page_size: String(nextSize) }), |
| }} |
| /> |
| </Card> |
| </div> |
| ) |
| } |
|
|