| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- import { useCallback, useEffect, useState } from "react";
- import { useTranslation } from "react-i18next";
- import {
- Typography,
- Button,
- Table,
- Popconfirm,
- message,
- Card,
- Modal,
- Form,
- Input,
- Space,
- } from "antd";
- import type { TableProps } from "antd";
- import {
- PlusOutlined,
- EditOutlined,
- DeleteOutlined,
- ReloadOutlined,
- SearchOutlined,
- } from "@ant-design/icons";
- import type { ConfigForm, ConfigItem } from "@/api/config";
- import {
- addConfig,
- deleteConfig,
- getConfig,
- getConfigPage,
- updateConfig,
- } from "@/api/config";
- const { Title, Text } = Typography;
- const { TextArea } = Input;
- export function ConfigPage() {
- const { t } = useTranslation();
- const [data, setData] = useState<ConfigItem[]>([]);
- const [loading, setLoading] = useState(false);
- const [submitting, setSubmitting] = useState(false);
- const [modalOpen, setModalOpen] = useState(false);
- const [editingKey, setEditingKey] = useState<string | null>(null);
- const [total, setTotal] = useState(0);
- const [keyword, setKeyword] = useState("");
- const [query, setQuery] = useState({ page: 1, size: 10, keyword: undefined as string | undefined });
- const [form] = Form.useForm<ConfigForm>();
- const fetchData = useCallback(async () => {
- setLoading(true);
- try {
- const res = await getConfigPage(query);
- setData(res.records);
- setTotal(res.total);
- } catch {
- // handled by request util
- } finally {
- setLoading(false);
- }
- }, [query]);
- useEffect(() => {
- fetchData();
- }, [fetchData]);
- function handleSearch() {
- setQuery((prev) => ({
- ...prev,
- page: 1,
- keyword: keyword.trim() || undefined,
- }));
- }
- function handleReset() {
- setKeyword("");
- setQuery({ page: 1, size: 10, keyword: undefined });
- }
- function openAdd() {
- setEditingKey(null);
- form.resetFields();
- setModalOpen(true);
- }
- async function openEdit(record: ConfigItem) {
- setEditingKey(record.configKey);
- setModalOpen(true);
- try {
- const detail = await getConfig(record.configKey);
- form.setFieldsValue({
- configKey: detail.configKey,
- configValue: detail.configValue,
- remark: detail.remark,
- });
- } catch {
- setModalOpen(false);
- setEditingKey(null);
- message.error(t("config.fetchFailed"));
- }
- }
- async function handleSubmit() {
- try {
- const values = await form.validateFields();
- setSubmitting(true);
- if (editingKey) {
- await updateConfig(values);
- message.success(t("config.updateSuccess"));
- } else {
- await addConfig(values);
- message.success(t("config.addSuccess"));
- }
- setModalOpen(false);
- setEditingKey(null);
- form.resetFields();
- fetchData();
- } catch {
- // validation or request error
- } finally {
- setSubmitting(false);
- }
- }
- async function handleDelete(configKey: string) {
- try {
- await deleteConfig(configKey);
- message.success(t("config.deleteSuccess"));
- fetchData();
- } catch {
- // handled by request util
- }
- }
- const columns: TableProps<ConfigItem>["columns"] = [
- {
- title: t("config.colKey"),
- dataIndex: "configKey",
- width: 220,
- fixed: "left",
- render: (val: string) => <span className="font-mono text-xs">{val}</span>,
- },
- {
- title: t("config.colValue"),
- dataIndex: "configValue",
- width: 320,
- render: (val?: string) => (
- <div
- className="text-sm"
- style={{
- lineHeight: "20px",
- whiteSpace: "pre-wrap",
- wordBreak: "break-word",
- }}
- >
- {val || "—"}
- </div>
- ),
- },
- {
- title: t("config.colRemark"),
- dataIndex: "remark",
- width: 240,
- render: (val?: string) => <span>{val || "—"}</span>,
- },
- {
- title: t("config.colCreateTime"),
- dataIndex: "createTime",
- width: 170,
- render: (val?: string) => val || "—",
- },
- {
- title: t("config.colUpdateTime"),
- dataIndex: "updateTime",
- width: 170,
- render: (val?: string) => val || "—",
- },
- {
- title: t("config.colActions"),
- key: "actions",
- width: 140,
- fixed: "right",
- align: "center",
- render: (_, record) => (
- <Space size={0}>
- <Button
- type="link"
- size="small"
- icon={<EditOutlined />}
- onClick={() => openEdit(record)}
- >
- {t("common.edit")}
- </Button>
- <Popconfirm
- title={t("config.deleteConfirm")}
- onConfirm={() => handleDelete(record.configKey)}
- okText={t("common.confirm")}
- cancelText={t("common.cancel")}
- >
- <Button type="link" size="small" danger icon={<DeleteOutlined />}>
- {t("common.delete")}
- </Button>
- </Popconfirm>
- </Space>
- ),
- },
- ];
- return (
- <div>
- <div className="flex items-center justify-between mb-4">
- <Title level={4} style={{ margin: 0 }}>
- {t("config.title")}
- </Title>
- <Space>
- <Button icon={<ReloadOutlined />} onClick={fetchData}>
- {t("config.refresh")}
- </Button>
- <Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
- {t("config.addBtn")}
- </Button>
- </Space>
- </div>
- <Card style={{ marginBottom: 16 }}>
- <div className="flex flex-wrap items-center gap-3">
- <Input
- placeholder={t("config.searchPlaceholder")}
- prefix={<SearchOutlined />}
- value={keyword}
- onChange={(e) => setKeyword(e.target.value)}
- onPressEnter={handleSearch}
- style={{ width: 320 }}
- allowClear
- />
- <Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
- {t("common.search")}
- </Button>
- <Button icon={<ReloadOutlined />} onClick={handleReset}>
- {t("config.reset")}
- </Button>
- </div>
- <Text type="secondary" style={{ display: "block", marginTop: 12, fontSize: 12 }}>
- {t("config.searchTip")}
- </Text>
- </Card>
- <Card>
- <Table<ConfigItem>
- rowKey="configKey"
- columns={columns}
- dataSource={data}
- loading={loading}
- scroll={{ x: 1200 }}
- size="middle"
- pagination={{
- current: query.page,
- pageSize: query.size,
- total,
- showSizeChanger: true,
- showQuickJumper: true,
- showTotal: (count) => t("common.paginationTotal", { total: count }),
- onChange: (page, size) => setQuery((prev) => ({ ...prev, page, size })),
- }}
- />
- </Card>
- <Modal
- open={modalOpen}
- title={editingKey ? t("config.editTitle") : t("config.addTitle")}
- onCancel={() => {
- setModalOpen(false);
- setEditingKey(null);
- form.resetFields();
- }}
- onOk={handleSubmit}
- confirmLoading={submitting}
- destroyOnClose
- >
- <Form form={form} layout="vertical" style={{ marginTop: 16 }}>
- <Form.Item
- name="configKey"
- label={t("config.fieldKey")}
- rules={[{ required: true, message: t("config.required") }]}
- >
- <Input
- placeholder={t("config.keyPlaceholder")}
- disabled={!!editingKey}
- />
- </Form.Item>
- <Form.Item name="configValue" label={t("config.fieldValue")}>
- <TextArea
- rows={4}
- placeholder={t("config.valuePlaceholder")}
- />
- </Form.Item>
- <Form.Item name="remark" label={t("config.fieldRemark")}>
- <TextArea
- rows={3}
- placeholder={t("config.remarkPlaceholder")}
- />
- </Form.Item>
- </Form>
- </Modal>
- </div>
- );
- }
|