pages_know_info.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import React, { useState } from 'react';
  2. import { useQueryClient } from '@tanstack/react-query';
  3. import {
  4. Button, Table, Space,
  5. Form, Input, Select, message, Modal,
  6. Card, Row, Col,
  7. Popconfirm, Tag
  8. } from 'antd';
  9. import {
  10. useQuery,
  11. } from '@tanstack/react-query';
  12. import dayjs from 'dayjs';
  13. import weekday from 'dayjs/plugin/weekday';
  14. import localeData from 'dayjs/plugin/localeData';
  15. import 'dayjs/locale/zh-cn';
  16. import type {
  17. KnowInfo
  18. } from '../share/types.ts';
  19. import {
  20. AuditStatus,AuditStatusNameMap,
  21. } from '../share/types.ts';
  22. import { getEnumOptions } from './utils.ts';
  23. import {
  24. KnowInfoAPI,
  25. type KnowInfoListResponse
  26. } from './api/index.ts';
  27. // 配置 dayjs 插件
  28. dayjs.extend(weekday);
  29. dayjs.extend(localeData);
  30. // 设置 dayjs 语言
  31. dayjs.locale('zh-cn');
  32. // 知识库管理页面组件
  33. export const KnowInfoPage = () => {
  34. const queryClient = useQueryClient();
  35. const [modalVisible, setModalVisible] = useState(false);
  36. const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
  37. const [editingId, setEditingId] = useState<number | null>(null);
  38. const [form] = Form.useForm();
  39. const [searchForm] = Form.useForm();
  40. const [searchParams, setSearchParams] = useState({
  41. title: '',
  42. category: '',
  43. page: 1,
  44. limit: 10,
  45. });
  46. // 使用React Query获取知识库文章列表
  47. const { data: articlesData, isLoading: isListLoading, refetch } = useQuery({
  48. queryKey: ['knowInfos', searchParams],
  49. queryFn: () => KnowInfoAPI.getKnowInfos({
  50. page: searchParams.page,
  51. pageSize: searchParams.limit,
  52. title: searchParams.title,
  53. category: searchParams.category
  54. }),
  55. placeholderData: {
  56. data: [],
  57. pagination: {
  58. current: 1,
  59. pageSize: 10,
  60. total: 0,
  61. totalPages: 1
  62. }
  63. }
  64. });
  65. const articles = React.useMemo(() => (articlesData as KnowInfoListResponse)?.data || [], [articlesData]);
  66. const pagination = React.useMemo(() => ({
  67. current: (articlesData as KnowInfoListResponse)?.pagination?.current || 1,
  68. pageSize: (articlesData as KnowInfoListResponse)?.pagination?.pageSize || 10,
  69. total: (articlesData as KnowInfoListResponse)?.pagination?.total || 0,
  70. totalPages: (articlesData as KnowInfoListResponse)?.pagination?.totalPages || 1
  71. }), [articlesData]);
  72. // 获取单个知识库文章
  73. const fetchArticle = async (id: number) => {
  74. try {
  75. const response = await KnowInfoAPI.getKnowInfo(id);
  76. return response.data;
  77. } catch (error) {
  78. message.error('获取知识库文章详情失败');
  79. return null;
  80. }
  81. };
  82. // 处理表单提交
  83. const handleSubmit = async (values: Partial<KnowInfo>) => {
  84. console.log('handleSubmit', values)
  85. try {
  86. const response = formMode === 'create'
  87. ? await KnowInfoAPI.createKnowInfo(values)
  88. : await KnowInfoAPI.updateKnowInfo(editingId!, values);
  89. message.success(formMode === 'create' ? '创建知识库文章成功' : '更新知识库文章成功');
  90. setModalVisible(false);
  91. form.resetFields();
  92. refetch();
  93. } catch (error) {
  94. message.error((error as Error).message);
  95. }
  96. };
  97. // 处理编辑
  98. const handleEdit = async (id: number) => {
  99. const article = await fetchArticle(id);
  100. console.log('article', article)
  101. if (article) {
  102. setFormMode('edit');
  103. setEditingId(id);
  104. form.setFieldsValue(article);
  105. setModalVisible(true);
  106. }
  107. };
  108. // 处理删除
  109. const handleDelete = async (id: number) => {
  110. try {
  111. await KnowInfoAPI.deleteKnowInfo(id);
  112. message.success('删除知识库文章成功');
  113. refetch();
  114. } catch (error) {
  115. message.error((error as Error).message);
  116. }
  117. };
  118. // 处理搜索
  119. const handleSearch = async (values: any) => {
  120. try {
  121. queryClient.removeQueries({ queryKey: ['knowInfos'] });
  122. setSearchParams({
  123. title: values.title || '',
  124. category: values.category || '',
  125. page: 1,
  126. limit: searchParams.limit,
  127. });
  128. } catch (error) {
  129. message.error('搜索失败');
  130. }
  131. };
  132. // 处理分页
  133. const handlePageChange = (page: number, pageSize?: number) => {
  134. setSearchParams(prev => ({
  135. ...prev,
  136. page,
  137. limit: pageSize || prev.limit,
  138. }));
  139. };
  140. // 处理添加
  141. const handleAdd = () => {
  142. setFormMode('create');
  143. setEditingId(null);
  144. form.resetFields();
  145. setModalVisible(true);
  146. };
  147. // 审核状态映射
  148. const auditStatusOptions = getEnumOptions(AuditStatus, AuditStatusNameMap);
  149. // 表格列定义
  150. const columns = [
  151. {
  152. title: 'ID',
  153. dataIndex: 'id',
  154. key: 'id',
  155. width: 80,
  156. },
  157. {
  158. title: '标题',
  159. dataIndex: 'title',
  160. key: 'title',
  161. },
  162. {
  163. title: '分类',
  164. dataIndex: 'category',
  165. key: 'category',
  166. },
  167. {
  168. title: '标签',
  169. dataIndex: 'tags',
  170. key: 'tags',
  171. render: (tags: string) => tags ? tags.split(',').map(tag => (
  172. <Tag key={tag}>{tag}</Tag>
  173. )) : null,
  174. },
  175. {
  176. title: '作者',
  177. dataIndex: 'author',
  178. key: 'author',
  179. },
  180. {
  181. title: '审核状态',
  182. dataIndex: 'audit_status',
  183. key: 'audit_status',
  184. render: (status: AuditStatus) => {
  185. let color = '';
  186. let text = '';
  187. switch(status) {
  188. case AuditStatus.PENDING:
  189. color = 'orange';
  190. text = '待审核';
  191. break;
  192. case AuditStatus.APPROVED:
  193. color = 'green';
  194. text = '已通过';
  195. break;
  196. case AuditStatus.REJECTED:
  197. color = 'red';
  198. text = '已拒绝';
  199. break;
  200. default:
  201. color = 'default';
  202. text = '未知';
  203. }
  204. return <Tag color={color}>{text}</Tag>;
  205. },
  206. },
  207. {
  208. title: '创建时间',
  209. dataIndex: 'created_at',
  210. key: 'created_at',
  211. render: (date: string) => new Date(date).toLocaleString(),
  212. },
  213. {
  214. title: '操作',
  215. key: 'action',
  216. render: (_: any, record: KnowInfo) => (
  217. <Space size="middle">
  218. <Button type="link" onClick={() => handleEdit(record.id)}>编辑</Button>
  219. <Popconfirm
  220. title="确定要删除这篇文章吗?"
  221. onConfirm={() => handleDelete(record.id)}
  222. okText="确定"
  223. cancelText="取消"
  224. >
  225. <Button type="link" danger>删除</Button>
  226. </Popconfirm>
  227. </Space>
  228. ),
  229. },
  230. ];
  231. return (
  232. <div>
  233. <Card title="知识库管理" className="mb-4">
  234. <Form
  235. form={searchForm}
  236. layout="inline"
  237. onFinish={handleSearch}
  238. style={{ marginBottom: '16px' }}
  239. >
  240. <Form.Item name="title" label="标题">
  241. <Input placeholder="要搜索的文章标题" />
  242. </Form.Item>
  243. <Form.Item name="category" label="分类">
  244. <Input placeholder="要搜索的文章分类" />
  245. </Form.Item>
  246. <Form.Item>
  247. <Space>
  248. <Button type="primary" htmlType="submit">
  249. 搜索
  250. </Button>
  251. <Button htmlType="reset" onClick={() => {
  252. searchForm.resetFields();
  253. setSearchParams({
  254. title: '',
  255. category: '',
  256. page: 1,
  257. limit: 10,
  258. });
  259. }}>
  260. 重置
  261. </Button>
  262. <Button type="primary" onClick={handleAdd}>
  263. 添加文章
  264. </Button>
  265. </Space>
  266. </Form.Item>
  267. </Form>
  268. <Table
  269. columns={columns}
  270. dataSource={articles}
  271. rowKey="id"
  272. loading={{
  273. spinning: isListLoading,
  274. tip: '正在加载数据...',
  275. }}
  276. pagination={{
  277. current: pagination.current,
  278. pageSize: pagination.pageSize,
  279. total: pagination.total,
  280. onChange: handlePageChange,
  281. showSizeChanger: true,
  282. showTotal: (total) => `共 ${total} 条`,
  283. }}
  284. />
  285. </Card>
  286. <Modal
  287. title={formMode === 'create' ? '添加知识库文章' : '编辑知识库文章'}
  288. open={modalVisible}
  289. onOk={() => {
  290. form.validateFields()
  291. .then(values => {
  292. handleSubmit(values);
  293. })
  294. .catch(info => {
  295. console.log('表单验证失败:', info);
  296. });
  297. }}
  298. onCancel={() => setModalVisible(false)}
  299. width={800}
  300. okText="确定"
  301. cancelText="取消"
  302. destroyOnClose
  303. >
  304. <Form
  305. form={form}
  306. layout="vertical"
  307. initialValues={{
  308. audit_status: AuditStatus.PENDING,
  309. }}
  310. >
  311. <Row gutter={16}>
  312. <Col span={12}>
  313. <Form.Item
  314. name="title"
  315. label="文章标题"
  316. rules={[{ required: true, message: '请输入文章标题' }]}
  317. >
  318. <Input placeholder="请输入文章标题" />
  319. </Form.Item>
  320. </Col>
  321. <Col span={12}>
  322. <Form.Item
  323. name="category"
  324. label="文章分类"
  325. >
  326. <Input placeholder="请输入文章分类" />
  327. </Form.Item>
  328. </Col>
  329. </Row>
  330. <Form.Item
  331. name="tags"
  332. label="文章标签"
  333. help="多个标签请用英文逗号分隔,如: 服务器,网络,故障"
  334. >
  335. <Input placeholder="请输入文章标签,多个标签请用英文逗号分隔" />
  336. </Form.Item>
  337. <Form.Item
  338. name="content"
  339. label="文章内容"
  340. // rules={[{ required: true, message: '请输入文章内容' }]}
  341. >
  342. <Input.TextArea rows={15} placeholder="请输入文章内容,支持Markdown格式" />
  343. </Form.Item>
  344. <Row gutter={16}>
  345. <Col span={12}>
  346. <Form.Item
  347. name="author"
  348. label="文章作者"
  349. >
  350. <Input placeholder="请输入文章作者" />
  351. </Form.Item>
  352. </Col>
  353. <Col span={12}>
  354. <Form.Item
  355. name="cover_url"
  356. label="封面图片URL"
  357. >
  358. <Input placeholder="请输入封面图片URL" />
  359. </Form.Item>
  360. </Col>
  361. </Row>
  362. <Form.Item
  363. name="audit_status"
  364. label="审核状态"
  365. >
  366. <Select options={auditStatusOptions} />
  367. </Form.Item>
  368. </Form>
  369. </Modal>
  370. </div>
  371. );
  372. };