ConfigPage.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import { useCallback, useEffect, useState } from "react";
  2. import { useTranslation } from "react-i18next";
  3. import {
  4. Typography,
  5. Button,
  6. Table,
  7. Popconfirm,
  8. message,
  9. Card,
  10. Modal,
  11. Form,
  12. Input,
  13. Space,
  14. } from "antd";
  15. import type { TableProps } from "antd";
  16. import {
  17. PlusOutlined,
  18. EditOutlined,
  19. DeleteOutlined,
  20. ReloadOutlined,
  21. SearchOutlined,
  22. } from "@ant-design/icons";
  23. import type { ConfigForm, ConfigItem } from "@/api/config";
  24. import {
  25. addConfig,
  26. deleteConfig,
  27. getConfig,
  28. getConfigPage,
  29. updateConfig,
  30. } from "@/api/config";
  31. const { Title, Text } = Typography;
  32. const { TextArea } = Input;
  33. export function ConfigPage() {
  34. const { t } = useTranslation();
  35. const [data, setData] = useState<ConfigItem[]>([]);
  36. const [loading, setLoading] = useState(false);
  37. const [submitting, setSubmitting] = useState(false);
  38. const [modalOpen, setModalOpen] = useState(false);
  39. const [editingKey, setEditingKey] = useState<string | null>(null);
  40. const [total, setTotal] = useState(0);
  41. const [keyword, setKeyword] = useState("");
  42. const [query, setQuery] = useState({ page: 1, size: 10, keyword: undefined as string | undefined });
  43. const [form] = Form.useForm<ConfigForm>();
  44. const fetchData = useCallback(async () => {
  45. setLoading(true);
  46. try {
  47. const res = await getConfigPage(query);
  48. setData(res.records);
  49. setTotal(res.total);
  50. } catch {
  51. // handled by request util
  52. } finally {
  53. setLoading(false);
  54. }
  55. }, [query]);
  56. useEffect(() => {
  57. fetchData();
  58. }, [fetchData]);
  59. function handleSearch() {
  60. setQuery((prev) => ({
  61. ...prev,
  62. page: 1,
  63. keyword: keyword.trim() || undefined,
  64. }));
  65. }
  66. function handleReset() {
  67. setKeyword("");
  68. setQuery({ page: 1, size: 10, keyword: undefined });
  69. }
  70. function openAdd() {
  71. setEditingKey(null);
  72. form.resetFields();
  73. setModalOpen(true);
  74. }
  75. async function openEdit(record: ConfigItem) {
  76. setEditingKey(record.configKey);
  77. setModalOpen(true);
  78. try {
  79. const detail = await getConfig(record.configKey);
  80. form.setFieldsValue({
  81. configKey: detail.configKey,
  82. configValue: detail.configValue,
  83. remark: detail.remark,
  84. });
  85. } catch {
  86. setModalOpen(false);
  87. setEditingKey(null);
  88. message.error(t("config.fetchFailed"));
  89. }
  90. }
  91. async function handleSubmit() {
  92. try {
  93. const values = await form.validateFields();
  94. setSubmitting(true);
  95. if (editingKey) {
  96. await updateConfig(values);
  97. message.success(t("config.updateSuccess"));
  98. } else {
  99. await addConfig(values);
  100. message.success(t("config.addSuccess"));
  101. }
  102. setModalOpen(false);
  103. setEditingKey(null);
  104. form.resetFields();
  105. fetchData();
  106. } catch {
  107. // validation or request error
  108. } finally {
  109. setSubmitting(false);
  110. }
  111. }
  112. async function handleDelete(configKey: string) {
  113. try {
  114. await deleteConfig(configKey);
  115. message.success(t("config.deleteSuccess"));
  116. fetchData();
  117. } catch {
  118. // handled by request util
  119. }
  120. }
  121. const columns: TableProps<ConfigItem>["columns"] = [
  122. {
  123. title: t("config.colKey"),
  124. dataIndex: "configKey",
  125. width: 220,
  126. fixed: "left",
  127. render: (val: string) => <span className="font-mono text-xs">{val}</span>,
  128. },
  129. {
  130. title: t("config.colValue"),
  131. dataIndex: "configValue",
  132. width: 320,
  133. render: (val?: string) => (
  134. <div
  135. className="text-sm"
  136. style={{
  137. lineHeight: "20px",
  138. whiteSpace: "pre-wrap",
  139. wordBreak: "break-word",
  140. }}
  141. >
  142. {val || "—"}
  143. </div>
  144. ),
  145. },
  146. {
  147. title: t("config.colRemark"),
  148. dataIndex: "remark",
  149. width: 240,
  150. render: (val?: string) => <span>{val || "—"}</span>,
  151. },
  152. {
  153. title: t("config.colCreateTime"),
  154. dataIndex: "createTime",
  155. width: 170,
  156. render: (val?: string) => val || "—",
  157. },
  158. {
  159. title: t("config.colUpdateTime"),
  160. dataIndex: "updateTime",
  161. width: 170,
  162. render: (val?: string) => val || "—",
  163. },
  164. {
  165. title: t("config.colActions"),
  166. key: "actions",
  167. width: 140,
  168. fixed: "right",
  169. align: "center",
  170. render: (_, record) => (
  171. <Space size={0}>
  172. <Button
  173. type="link"
  174. size="small"
  175. icon={<EditOutlined />}
  176. onClick={() => openEdit(record)}
  177. >
  178. {t("common.edit")}
  179. </Button>
  180. <Popconfirm
  181. title={t("config.deleteConfirm")}
  182. onConfirm={() => handleDelete(record.configKey)}
  183. okText={t("common.confirm")}
  184. cancelText={t("common.cancel")}
  185. >
  186. <Button type="link" size="small" danger icon={<DeleteOutlined />}>
  187. {t("common.delete")}
  188. </Button>
  189. </Popconfirm>
  190. </Space>
  191. ),
  192. },
  193. ];
  194. return (
  195. <div>
  196. <div className="flex items-center justify-between mb-4">
  197. <Title level={4} style={{ margin: 0 }}>
  198. {t("config.title")}
  199. </Title>
  200. <Space>
  201. <Button icon={<ReloadOutlined />} onClick={fetchData}>
  202. {t("config.refresh")}
  203. </Button>
  204. <Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
  205. {t("config.addBtn")}
  206. </Button>
  207. </Space>
  208. </div>
  209. <Card style={{ marginBottom: 16 }}>
  210. <div className="flex flex-wrap items-center gap-3">
  211. <Input
  212. placeholder={t("config.searchPlaceholder")}
  213. prefix={<SearchOutlined />}
  214. value={keyword}
  215. onChange={(e) => setKeyword(e.target.value)}
  216. onPressEnter={handleSearch}
  217. style={{ width: 320 }}
  218. allowClear
  219. />
  220. <Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
  221. {t("common.search")}
  222. </Button>
  223. <Button icon={<ReloadOutlined />} onClick={handleReset}>
  224. {t("config.reset")}
  225. </Button>
  226. </div>
  227. <Text type="secondary" style={{ display: "block", marginTop: 12, fontSize: 12 }}>
  228. {t("config.searchTip")}
  229. </Text>
  230. </Card>
  231. <Card>
  232. <Table<ConfigItem>
  233. rowKey="configKey"
  234. columns={columns}
  235. dataSource={data}
  236. loading={loading}
  237. scroll={{ x: 1200 }}
  238. size="middle"
  239. pagination={{
  240. current: query.page,
  241. pageSize: query.size,
  242. total,
  243. showSizeChanger: true,
  244. showQuickJumper: true,
  245. showTotal: (count) => t("common.paginationTotal", { total: count }),
  246. onChange: (page, size) => setQuery((prev) => ({ ...prev, page, size })),
  247. }}
  248. />
  249. </Card>
  250. <Modal
  251. open={modalOpen}
  252. title={editingKey ? t("config.editTitle") : t("config.addTitle")}
  253. onCancel={() => {
  254. setModalOpen(false);
  255. setEditingKey(null);
  256. form.resetFields();
  257. }}
  258. onOk={handleSubmit}
  259. confirmLoading={submitting}
  260. destroyOnClose
  261. >
  262. <Form form={form} layout="vertical" style={{ marginTop: 16 }}>
  263. <Form.Item
  264. name="configKey"
  265. label={t("config.fieldKey")}
  266. rules={[{ required: true, message: t("config.required") }]}
  267. >
  268. <Input
  269. placeholder={t("config.keyPlaceholder")}
  270. disabled={!!editingKey}
  271. />
  272. </Form.Item>
  273. <Form.Item name="configValue" label={t("config.fieldValue")}>
  274. <TextArea
  275. rows={4}
  276. placeholder={t("config.valuePlaceholder")}
  277. />
  278. </Form.Item>
  279. <Form.Item name="remark" label={t("config.fieldRemark")}>
  280. <TextArea
  281. rows={3}
  282. placeholder={t("config.remarkPlaceholder")}
  283. />
  284. </Form.Item>
  285. </Form>
  286. </Modal>
  287. </div>
  288. );
  289. }