iconsulting/packages/admin-client/src/features/knowledge/presentation/pages/KnowledgePage.tsx

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>
);
}