Keywords.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { useEffect, useState, useCallback } from 'react'
  2. import {
  3. Table,
  4. Button,
  5. Modal,
  6. Form,
  7. Input,
  8. Select,
  9. Switch,
  10. Space,
  11. message,
  12. Popconfirm,
  13. Tag,
  14. Row,
  15. Col,
  16. } from 'antd'
  17. import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
  18. import { getKeywords, createKeywords, updateKeyword, deleteKeyword, type Keyword } from '../api'
  19. const { Option } = Select
  20. const { TextArea } = Input
  21. function formatDateTime(dateStr: string) {
  22. return new Date(dateStr).toLocaleString('zh-CN')
  23. }
  24. const tagColors: Record<string, string> = {
  25. seed: 'volcano',
  26. '机场': 'blue',
  27. VPN: 'green',
  28. }
  29. const industryOptions = [
  30. { label: '搜索关键词', value: '机场' },
  31. { label: '种子频道', value: 'seed' },
  32. { label: 'VPN', value: 'VPN' },
  33. ]
  34. interface BatchFormValues {
  35. keywords_text: string
  36. industry_tag: string
  37. }
  38. export default function Keywords() {
  39. const [data, setData] = useState<Keyword[]>([])
  40. const [total, setTotal] = useState(0)
  41. const [page, setPage] = useState(1)
  42. const [loading, setLoading] = useState(false)
  43. const [modalOpen, setModalOpen] = useState(false)
  44. const [saving, setSaving] = useState(false)
  45. const [filterTag, setFilterTag] = useState('')
  46. const [form] = Form.useForm<BatchFormValues>()
  47. const fetchData = useCallback(async (currentPage = 1) => {
  48. setLoading(true)
  49. try {
  50. const params: Record<string, unknown> = { page: currentPage, page_size: 20 }
  51. if (filterTag) params.industry_tag = filterTag
  52. const res = await getKeywords(params)
  53. setData(res.data.items)
  54. setTotal(res.data.total)
  55. } catch {
  56. message.error('获取关键词列表失败')
  57. } finally {
  58. setLoading(false)
  59. }
  60. }, [filterTag])
  61. useEffect(() => {
  62. setPage(1)
  63. fetchData(1)
  64. }, [filterTag, fetchData])
  65. const handleBatchAdd = () => {
  66. form.resetFields()
  67. setModalOpen(true)
  68. }
  69. const handleSave = async () => {
  70. try {
  71. const values = await form.validateFields()
  72. const keywords = values.keywords_text
  73. .split('\n')
  74. .map((k: string) => k.trim())
  75. .filter((k: string) => k.length > 0)
  76. if (keywords.length === 0) {
  77. message.warning('请输入至少一个关键词')
  78. return
  79. }
  80. setSaving(true)
  81. await createKeywords({ keywords, industry_tag: values.industry_tag })
  82. message.success(`成功添加 ${keywords.length} 个条目`)
  83. setModalOpen(false)
  84. fetchData(page)
  85. } catch (err) {
  86. if (err && typeof err === 'object' && 'errorFields' in err) return
  87. message.error('添加失败')
  88. } finally {
  89. setSaving(false)
  90. }
  91. }
  92. const handleDelete = async (id: number) => {
  93. try {
  94. await deleteKeyword(id)
  95. message.success('删除成功')
  96. fetchData(page)
  97. } catch {
  98. message.error('删除失败')
  99. }
  100. }
  101. const handleToggle = async (record: Keyword, checked: boolean) => {
  102. try {
  103. await updateKeyword(record.id, { enabled: checked })
  104. message.success('状态已更新')
  105. fetchData(page)
  106. } catch {
  107. message.error('状态更新失败')
  108. }
  109. }
  110. const columns = [
  111. { title: 'ID', dataIndex: 'id', key: 'id', width: 70 },
  112. {
  113. title: '关键词/频道名',
  114. dataIndex: 'keyword',
  115. key: 'keyword',
  116. },
  117. {
  118. title: '类型',
  119. dataIndex: 'industry_tag',
  120. key: 'industry_tag',
  121. render: (v: string) => <Tag color={tagColors[v] ?? 'default'}>{v || '未分类'}</Tag>,
  122. },
  123. {
  124. title: '启用',
  125. dataIndex: 'enabled',
  126. key: 'enabled',
  127. render: (v: boolean, record: Keyword) => (
  128. <Switch
  129. checked={v}
  130. onChange={(checked) => handleToggle(record, checked)}
  131. checkedChildren="启用"
  132. unCheckedChildren="禁用"
  133. />
  134. ),
  135. },
  136. {
  137. title: '创建时间',
  138. dataIndex: 'created_at',
  139. key: 'created_at',
  140. render: (t: string) => formatDateTime(t),
  141. },
  142. {
  143. title: '操作',
  144. key: 'action',
  145. render: (_: unknown, record: Keyword) => (
  146. <Space>
  147. <Popconfirm
  148. title="确认删除?"
  149. onConfirm={() => handleDelete(record.id)}
  150. okText="确认"
  151. cancelText="取消"
  152. >
  153. <Button size="small" danger icon={<DeleteOutlined />}>删除</Button>
  154. </Popconfirm>
  155. </Space>
  156. ),
  157. },
  158. ]
  159. return (
  160. <div>
  161. <Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
  162. <Col>
  163. <Button type="primary" icon={<PlusOutlined />} onClick={handleBatchAdd}>
  164. 批量添加
  165. </Button>
  166. </Col>
  167. <Col>
  168. <Select
  169. style={{ width: 160 }}
  170. value={filterTag}
  171. onChange={setFilterTag}
  172. placeholder="按类型筛选"
  173. allowClear
  174. >
  175. <Option value="">全部</Option>
  176. <Option value="seed">种子频道</Option>
  177. <Option value="机场">机场</Option>
  178. <Option value="VPN">VPN</Option>
  179. </Select>
  180. </Col>
  181. </Row>
  182. <Table
  183. dataSource={data}
  184. columns={columns}
  185. rowKey="id"
  186. loading={loading}
  187. pagination={{
  188. current: page,
  189. pageSize: 20,
  190. total,
  191. onChange: (p) => {
  192. setPage(p)
  193. fetchData(p)
  194. },
  195. showTotal: (t) => `共 ${t} 条`,
  196. }}
  197. />
  198. <Modal
  199. title="批量添加"
  200. open={modalOpen}
  201. onOk={handleSave}
  202. onCancel={() => setModalOpen(false)}
  203. confirmLoading={saving}
  204. okText="添加"
  205. cancelText="取消"
  206. >
  207. <Form form={form} layout="vertical" style={{ marginTop: 16 }}>
  208. <Form.Item
  209. name="industry_tag"
  210. label="类型"
  211. rules={[{ required: true, message: '请选择类型' }]}
  212. >
  213. <Select placeholder="选择类型">
  214. {industryOptions.map((o) => (
  215. <Option key={o.value} value={o.value}>{o.label}</Option>
  216. ))}
  217. </Select>
  218. </Form.Item>
  219. <Form.Item
  220. name="keywords_text"
  221. label="内容(每行一个)"
  222. rules={[{ required: true, message: '请输入内容' }]}
  223. >
  224. <TextArea
  225. rows={8}
  226. placeholder="种子频道填频道名(如 bbs3000),关键词填搜索词(如 机场推荐)&#10;每行一个"
  227. />
  228. </Form.Item>
  229. </Form>
  230. </Modal>
  231. </div>
  232. )
  233. }