XHS / frontend /src /pages /RawNotesPage.tsx
Trae Bot
Upload Spider_XHS project
c481f8a
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>
)
}