405 lines
11 KiB
TypeScript
405 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
Card,
|
|
Table,
|
|
Button,
|
|
Input,
|
|
Select,
|
|
Tag,
|
|
Space,
|
|
Modal,
|
|
Form,
|
|
message,
|
|
Popconfirm,
|
|
Typography,
|
|
Drawer,
|
|
} from 'antd';
|
|
import {
|
|
PlusOutlined,
|
|
SearchOutlined,
|
|
EditOutlined,
|
|
DeleteOutlined,
|
|
EyeOutlined,
|
|
CheckOutlined,
|
|
StopOutlined,
|
|
} from '@ant-design/icons';
|
|
import api from '../../../../shared/utils/api';
|
|
|
|
const { Title, Paragraph } = Typography;
|
|
const { TextArea } = Input;
|
|
|
|
const CATEGORIES = [
|
|
{ value: 'QMAS', label: '优秀人才入境计划' },
|
|
{ value: 'GEP', label: '一般就业政策' },
|
|
{ value: 'IANG', label: '非本地毕业生留港/回港就业安排' },
|
|
{ value: 'TTPS', label: '科技人才入境计划' },
|
|
{ value: 'CIES', label: '资本投资者入境计划' },
|
|
{ value: 'TechTAS', label: '顶尖人才通行证计划' },
|
|
{ value: 'GENERAL', label: '通用知识' },
|
|
];
|
|
|
|
interface Article {
|
|
id: string;
|
|
title: string;
|
|
content: string;
|
|
summary: string;
|
|
category: string;
|
|
tags: string[];
|
|
source: string;
|
|
isPublished: boolean;
|
|
citationCount: number;
|
|
qualityScore: number;
|
|
createdAt: string;
|
|
}
|
|
|
|
export function KnowledgePage() {
|
|
const [searchText, setSearchText] = useState('');
|
|
const [categoryFilter, setCategoryFilter] = useState<string>();
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
|
|
const [form] = Form.useForm();
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['knowledge-articles', categoryFilter],
|
|
queryFn: async () => {
|
|
const params = new URLSearchParams();
|
|
if (categoryFilter) params.append('category', categoryFilter);
|
|
const response = await api.get(`/knowledge/articles?${params}`);
|
|
return response.data.data;
|
|
},
|
|
});
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: (values: Partial<Article>) =>
|
|
api.post('/knowledge/articles', values),
|
|
onSuccess: () => {
|
|
message.success('文章创建成功');
|
|
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
setIsModalOpen(false);
|
|
form.resetFields();
|
|
},
|
|
});
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ id, ...values }: { id: string } & Partial<Article>) =>
|
|
api.put(`/knowledge/articles/${id}`, values),
|
|
onSuccess: () => {
|
|
message.success('文章更新成功');
|
|
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
setIsModalOpen(false);
|
|
form.resetFields();
|
|
setSelectedArticle(null);
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => api.delete(`/knowledge/articles/${id}`),
|
|
onSuccess: () => {
|
|
message.success('文章已删除');
|
|
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
},
|
|
});
|
|
|
|
const publishMutation = useMutation({
|
|
mutationFn: (id: string) => api.post(`/knowledge/articles/${id}/publish`),
|
|
onSuccess: () => {
|
|
message.success('文章已发布');
|
|
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
},
|
|
});
|
|
|
|
const unpublishMutation = useMutation({
|
|
mutationFn: (id: string) => api.post(`/knowledge/articles/${id}/unpublish`),
|
|
onSuccess: () => {
|
|
message.success('文章已取消发布');
|
|
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
},
|
|
});
|
|
|
|
const handleEdit = (article: Article) => {
|
|
setSelectedArticle(article);
|
|
form.setFieldsValue({
|
|
title: article.title,
|
|
content: article.content,
|
|
category: article.category,
|
|
tags: article.tags,
|
|
});
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleView = (article: Article) => {
|
|
setSelectedArticle(article);
|
|
setIsDrawerOpen(true);
|
|
};
|
|
|
|
const handleSubmit = (values: Partial<Article>) => {
|
|
if (selectedArticle) {
|
|
updateMutation.mutate({ id: selectedArticle.id, ...values });
|
|
} else {
|
|
createMutation.mutate(values);
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '标题',
|
|
dataIndex: 'title',
|
|
key: 'title',
|
|
render: (text: string, record: Article) => (
|
|
<a onClick={() => handleView(record)}>{text}</a>
|
|
),
|
|
},
|
|
{
|
|
title: '类别',
|
|
dataIndex: 'category',
|
|
key: 'category',
|
|
render: (category: string) => {
|
|
const cat = CATEGORIES.find((c) => c.value === category);
|
|
return <Tag color="blue">{cat?.label || category}</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: '来源',
|
|
dataIndex: 'source',
|
|
key: 'source',
|
|
render: (source: string) => {
|
|
const sourceMap: Record<string, { color: string; label: string }> = {
|
|
MANUAL: { color: 'green', label: '手动' },
|
|
CRAWL: { color: 'orange', label: '爬取' },
|
|
EXTRACT: { color: 'purple', label: '提取' },
|
|
IMPORT: { color: 'cyan', label: '导入' },
|
|
};
|
|
const s = sourceMap[source] || { color: 'default', label: source };
|
|
return <Tag color={s.color}>{s.label}</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'isPublished',
|
|
key: 'isPublished',
|
|
render: (isPublished: boolean) =>
|
|
isPublished ? (
|
|
<Tag color="success">已发布</Tag>
|
|
) : (
|
|
<Tag color="default">草稿</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '质量分',
|
|
dataIndex: 'qualityScore',
|
|
key: 'qualityScore',
|
|
render: (score: number) => (
|
|
<span
|
|
className={
|
|
score >= 70
|
|
? 'text-green-600'
|
|
: score >= 40
|
|
? 'text-yellow-600'
|
|
: 'text-red-600'
|
|
}
|
|
>
|
|
{score}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
title: '引用次数',
|
|
dataIndex: 'citationCount',
|
|
key: 'citationCount',
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'action',
|
|
render: (_: unknown, record: Article) => (
|
|
<Space size="small">
|
|
<Button
|
|
type="text"
|
|
icon={<EyeOutlined />}
|
|
onClick={() => handleView(record)}
|
|
/>
|
|
<Button
|
|
type="text"
|
|
icon={<EditOutlined />}
|
|
onClick={() => handleEdit(record)}
|
|
/>
|
|
{record.isPublished ? (
|
|
<Popconfirm
|
|
title="确定取消发布?"
|
|
onConfirm={() => unpublishMutation.mutate(record.id)}
|
|
>
|
|
<Button type="text" icon={<StopOutlined />} />
|
|
</Popconfirm>
|
|
) : (
|
|
<Button
|
|
type="text"
|
|
icon={<CheckOutlined />}
|
|
onClick={() => publishMutation.mutate(record.id)}
|
|
/>
|
|
)}
|
|
<Popconfirm
|
|
title="确定删除此文章?"
|
|
onConfirm={() => deleteMutation.mutate(record.id)}
|
|
>
|
|
<Button type="text" danger icon={<DeleteOutlined />} />
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<Title level={4} className="mb-6">知识库管理</Title>
|
|
|
|
<Card>
|
|
<div className="flex justify-between mb-4">
|
|
<Space>
|
|
<Input
|
|
placeholder="搜索文章"
|
|
prefix={<SearchOutlined />}
|
|
value={searchText}
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
style={{ width: 200 }}
|
|
/>
|
|
<Select
|
|
placeholder="选择类别"
|
|
allowClear
|
|
style={{ width: 180 }}
|
|
value={categoryFilter}
|
|
onChange={setCategoryFilter}
|
|
options={CATEGORIES}
|
|
/>
|
|
</Space>
|
|
<Button
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => {
|
|
setSelectedArticle(null);
|
|
form.resetFields();
|
|
setIsModalOpen(true);
|
|
}}
|
|
>
|
|
新建文章
|
|
</Button>
|
|
</div>
|
|
|
|
<Table
|
|
columns={columns}
|
|
dataSource={data?.items || []}
|
|
rowKey="id"
|
|
loading={isLoading}
|
|
pagination={{
|
|
total: data?.total || 0,
|
|
pageSize: 20,
|
|
showSizeChanger: false,
|
|
}}
|
|
/>
|
|
</Card>
|
|
|
|
{/* 编辑/新建弹窗 */}
|
|
<Modal
|
|
title={selectedArticle ? '编辑文章' : '新建文章'}
|
|
open={isModalOpen}
|
|
onCancel={() => {
|
|
setIsModalOpen(false);
|
|
setSelectedArticle(null);
|
|
form.resetFields();
|
|
}}
|
|
footer={null}
|
|
width={800}
|
|
>
|
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
|
<Form.Item
|
|
name="title"
|
|
label="标题"
|
|
rules={[{ required: true, message: '请输入标题' }]}
|
|
>
|
|
<Input placeholder="文章标题" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="category"
|
|
label="类别"
|
|
rules={[{ required: true, message: '请选择类别' }]}
|
|
>
|
|
<Select options={CATEGORIES} placeholder="选择移民类别" />
|
|
</Form.Item>
|
|
|
|
<Form.Item name="tags" label="标签">
|
|
<Select mode="tags" placeholder="输入标签后回车" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="content"
|
|
label="内容"
|
|
rules={[{ required: true, message: '请输入内容' }]}
|
|
>
|
|
<TextArea rows={12} placeholder="支持Markdown格式" />
|
|
</Form.Item>
|
|
|
|
<Form.Item className="mb-0 text-right">
|
|
<Space>
|
|
<Button onClick={() => setIsModalOpen(false)}>取消</Button>
|
|
<Button
|
|
type="primary"
|
|
htmlType="submit"
|
|
loading={createMutation.isPending || updateMutation.isPending}
|
|
>
|
|
保存
|
|
</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
{/* 预览抽屉 */}
|
|
<Drawer
|
|
title={selectedArticle?.title}
|
|
open={isDrawerOpen}
|
|
onClose={() => {
|
|
setIsDrawerOpen(false);
|
|
setSelectedArticle(null);
|
|
}}
|
|
width={600}
|
|
>
|
|
{selectedArticle && (
|
|
<div>
|
|
<div className="mb-4">
|
|
<Tag color="blue">
|
|
{CATEGORIES.find((c) => c.value === selectedArticle.category)
|
|
?.label || selectedArticle.category}
|
|
</Tag>
|
|
{selectedArticle.isPublished ? (
|
|
<Tag color="success">已发布</Tag>
|
|
) : (
|
|
<Tag>草稿</Tag>
|
|
)}
|
|
<span className="ml-2 text-gray-500 text-sm">
|
|
质量分: {selectedArticle.qualityScore} | 引用:{' '}
|
|
{selectedArticle.citationCount}次
|
|
</span>
|
|
</div>
|
|
|
|
{selectedArticle.tags?.length > 0 && (
|
|
<div className="mb-4">
|
|
{selectedArticle.tags.map((tag) => (
|
|
<Tag key={tag}>{tag}</Tag>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<Paragraph className="text-gray-600 mb-4">
|
|
{selectedArticle.summary}
|
|
</Paragraph>
|
|
|
|
<div className="whitespace-pre-wrap">{selectedArticle.content}</div>
|
|
</div>
|
|
)}
|
|
</Drawer>
|
|
</div>
|
|
);
|
|
}
|