|
|
@@ -0,0 +1,514 @@
|
|
|
+import { useState, useEffect } from 'react'
|
|
|
+import {
|
|
|
+ Button,
|
|
|
+ message,
|
|
|
+ Modal,
|
|
|
+ Form,
|
|
|
+ Input,
|
|
|
+ Select,
|
|
|
+ Space,
|
|
|
+ Tag,
|
|
|
+ Card,
|
|
|
+ Descriptions,
|
|
|
+ Switch,
|
|
|
+} from 'antd'
|
|
|
+import { EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
|
|
+import { systemConfigApi } from '../../api/admin'
|
|
|
+import type { ColumnsType } from 'antd/es/table'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import utc from 'dayjs/plugin/utc'
|
|
|
+import timezone from 'dayjs/plugin/timezone'
|
|
|
+import ResizableTable from '../../components/ResizableTable'
|
|
|
+
|
|
|
+dayjs.extend(utc)
|
|
|
+dayjs.extend(timezone)
|
|
|
+
|
|
|
+const { TextArea } = Input
|
|
|
+
|
|
|
+interface SystemConfigItem {
|
|
|
+ id: number
|
|
|
+ config_key: string
|
|
|
+ config_value: string
|
|
|
+ config_type: string
|
|
|
+ description: string | null
|
|
|
+ is_system: boolean
|
|
|
+ is_editable: boolean
|
|
|
+ created_at: string | null
|
|
|
+ updated_at: string | null
|
|
|
+}
|
|
|
+
|
|
|
+export default function SystemConfig() {
|
|
|
+ const [loading, setLoading] = useState(false)
|
|
|
+ const [data, setData] = useState<SystemConfigItem[]>([])
|
|
|
+ const [keyword, setKeyword] = useState<string>('')
|
|
|
+ const [configType, setConfigType] = useState<string>('')
|
|
|
+ const [pagination, setPagination] = useState({
|
|
|
+ current: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ total: 0,
|
|
|
+ })
|
|
|
+ const [modalVisible, setModalVisible] = useState(false)
|
|
|
+ const [editingId, setEditingId] = useState<number | null>(null)
|
|
|
+ const [form] = Form.useForm()
|
|
|
+ const [detailVisible, setDetailVisible] = useState(false)
|
|
|
+ const [detailData, setDetailData] = useState<any>(null)
|
|
|
+ const [detailLoading, setDetailLoading] = useState(false)
|
|
|
+
|
|
|
+ const configTypeOptions = [
|
|
|
+ { value: 'int', label: '整数' },
|
|
|
+ { value: 'string', label: '字符串' },
|
|
|
+ { value: 'bool', label: '布尔值' },
|
|
|
+ { value: 'json', label: 'JSON' },
|
|
|
+ ]
|
|
|
+
|
|
|
+ const columns: ColumnsType<SystemConfigItem> = [
|
|
|
+ {
|
|
|
+ title: 'ID',
|
|
|
+ dataIndex: 'id',
|
|
|
+ key: 'id',
|
|
|
+ width: 80,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '配置键',
|
|
|
+ dataIndex: 'config_key',
|
|
|
+ key: 'config_key',
|
|
|
+ width: 200,
|
|
|
+ ellipsis: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '配置值',
|
|
|
+ dataIndex: 'config_value',
|
|
|
+ key: 'config_value',
|
|
|
+ width: 250,
|
|
|
+ ellipsis: true,
|
|
|
+ render: (text: string, record: SystemConfigItem) => {
|
|
|
+ // 对JSON类型的值进行特殊处理
|
|
|
+ if (record.config_type === 'json') {
|
|
|
+ try {
|
|
|
+ const parsed = JSON.parse(text)
|
|
|
+ return JSON.stringify(parsed, null, 2).substring(0, 100) + '...'
|
|
|
+ } catch {
|
|
|
+ return text
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return text
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '类型',
|
|
|
+ dataIndex: 'config_type',
|
|
|
+ key: 'config_type',
|
|
|
+ width: 100,
|
|
|
+ render: (type: string) => {
|
|
|
+ const typeMap: Record<string, { text: string; color: string }> = {
|
|
|
+ 'int': { text: '整数', color: 'blue' },
|
|
|
+ 'string': { text: '字符串', color: 'green' },
|
|
|
+ 'bool': { text: '布尔值', color: 'orange' },
|
|
|
+ 'json': { text: 'JSON', color: 'purple' },
|
|
|
+ }
|
|
|
+ const info = typeMap[type] || { text: type, color: 'default' }
|
|
|
+ return <Tag color={info.color}>{info.text}</Tag>
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '描述',
|
|
|
+ dataIndex: 'description',
|
|
|
+ key: 'description',
|
|
|
+ width: 200,
|
|
|
+ ellipsis: true,
|
|
|
+ render: (text: string) => (text ? text : '-'),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '系统配置',
|
|
|
+ dataIndex: 'is_system',
|
|
|
+ key: 'is_system',
|
|
|
+ width: 100,
|
|
|
+ render: (isSystem: boolean) => (
|
|
|
+ isSystem ? <Tag color="red">系统</Tag> : <Tag color="default">自定义</Tag>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '可编辑',
|
|
|
+ dataIndex: 'is_editable',
|
|
|
+ key: 'is_editable',
|
|
|
+ width: 100,
|
|
|
+ render: (isEditable: boolean, record: SystemConfigItem) => (
|
|
|
+ <Switch
|
|
|
+ checked={isEditable}
|
|
|
+ disabled={record.is_system} // 系统配置不可编辑
|
|
|
+ onChange={() => handleToggleEditable(record.id)}
|
|
|
+ />
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '创建时间',
|
|
|
+ dataIndex: 'created_at',
|
|
|
+ key: 'created_at',
|
|
|
+ width: 180,
|
|
|
+ render: (text: string) => (text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '操作',
|
|
|
+ key: 'action',
|
|
|
+ width: 200,
|
|
|
+ render: (_, record) => (
|
|
|
+ <Space>
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ size="small"
|
|
|
+ onClick={() => handleViewDetail(record.id)}
|
|
|
+ >
|
|
|
+ <EyeOutlined />
|
|
|
+ 详情
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ size="small"
|
|
|
+ onClick={() => handleEdit(record)}
|
|
|
+ disabled={!record.is_editable}
|
|
|
+ >
|
|
|
+ <EditOutlined />
|
|
|
+ 编辑
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ size="small"
|
|
|
+ danger
|
|
|
+ onClick={() => handleDelete(record.id)}
|
|
|
+ disabled={record.is_system} // 系统配置不可删除
|
|
|
+ >
|
|
|
+ <DeleteOutlined />
|
|
|
+ 删除
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ]
|
|
|
+
|
|
|
+ const fetchData = async (
|
|
|
+ page: number = pagination.current,
|
|
|
+ pageSize: number = pagination.pageSize,
|
|
|
+ searchKeyword: string = keyword,
|
|
|
+ typeFilter: string = configType
|
|
|
+ ) => {
|
|
|
+ setLoading(true)
|
|
|
+ try {
|
|
|
+ const response = await systemConfigApi.getList({
|
|
|
+ page,
|
|
|
+ page_size: pageSize,
|
|
|
+ keyword: searchKeyword || undefined,
|
|
|
+ config_type: typeFilter || undefined,
|
|
|
+ })
|
|
|
+ if (response.code === 200) {
|
|
|
+ setData(response.data || [])
|
|
|
+ setPagination({
|
|
|
+ current: response.page || page,
|
|
|
+ pageSize: response.page_size || pageSize,
|
|
|
+ total: response.total || 0,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ message.error('获取系统配置列表失败')
|
|
|
+ } finally {
|
|
|
+ setLoading(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ fetchData()
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ const handleTableChange = (pag: any) => {
|
|
|
+ const { current, pageSize } = pag
|
|
|
+ fetchData(current, pageSize)
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleSearch = () => {
|
|
|
+ // 搜索时从第一页开始
|
|
|
+ fetchData(1, pagination.pageSize, keyword, configType)
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleCreate = async (values: any) => {
|
|
|
+ try {
|
|
|
+ if (editingId) {
|
|
|
+ await systemConfigApi.update(editingId, values)
|
|
|
+ message.success('更新成功')
|
|
|
+ } else {
|
|
|
+ await systemConfigApi.create(values)
|
|
|
+ message.success('创建成功')
|
|
|
+ }
|
|
|
+ setModalVisible(false)
|
|
|
+ setEditingId(null)
|
|
|
+ form.resetFields()
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ message.error(editingId ? '更新失败' : '创建失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleEdit = (record: SystemConfigItem) => {
|
|
|
+ setEditingId(record.id)
|
|
|
+ form.setFieldsValue({
|
|
|
+ ...record,
|
|
|
+ })
|
|
|
+ setModalVisible(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleDelete = async (id: number) => {
|
|
|
+ Modal.confirm({
|
|
|
+ title: '确认删除',
|
|
|
+ content: '确定要删除这条系统配置吗?删除后不可恢复。',
|
|
|
+ onOk: async () => {
|
|
|
+ try {
|
|
|
+ await systemConfigApi.delete(id)
|
|
|
+ message.success('删除成功')
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ message.error('删除失败')
|
|
|
+ }
|
|
|
+ },
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleToggleEditable = async (id: number) => {
|
|
|
+ try {
|
|
|
+ const response = await systemConfigApi.toggleEditable(id)
|
|
|
+ if (response.code === 200) {
|
|
|
+ message.success('编辑状态更新成功')
|
|
|
+ fetchData()
|
|
|
+ } else {
|
|
|
+ message.error(response.msg || '编辑状态更新失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ message.error('编辑状态更新失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleViewDetail = async (id: number) => {
|
|
|
+ setDetailLoading(true)
|
|
|
+ setDetailVisible(true)
|
|
|
+ try {
|
|
|
+ const response = await systemConfigApi.getDetail(id)
|
|
|
+ if (response.code === 200) {
|
|
|
+ setDetailData(response.data)
|
|
|
+ } else {
|
|
|
+ message.error('获取系统配置详情失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ message.error('获取系统配置详情失败')
|
|
|
+ } finally {
|
|
|
+ setDetailLoading(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ {/* 第一区域:模块标题和检索栏 */}
|
|
|
+ <Card style={{ marginBottom: 24 }}>
|
|
|
+ <div style={{ marginBottom: 16 }}>
|
|
|
+ <div className="page-title">系统配置管理(管理系统配置参数)</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 搜索栏 */}
|
|
|
+ <div style={{ padding: 16 }}>
|
|
|
+ <Space>
|
|
|
+ <Input.Search
|
|
|
+ allowClear
|
|
|
+ placeholder="按配置键或描述搜索"
|
|
|
+ value={keyword}
|
|
|
+ onChange={(e) => setKeyword(e.target.value)}
|
|
|
+ onSearch={handleSearch}
|
|
|
+ style={{ width: 260 }}
|
|
|
+ />
|
|
|
+ <Select
|
|
|
+ allowClear
|
|
|
+ placeholder="选择配置类型"
|
|
|
+ value={configType}
|
|
|
+ onChange={(value) => setConfigType(value)}
|
|
|
+ style={{ width: 120 }}
|
|
|
+ >
|
|
|
+ {configTypeOptions.map(option => (
|
|
|
+ <Select.Option key={option.value} value={option.value}>
|
|
|
+ {option.label}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ <Button type="primary" onClick={handleSearch}>
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* 第二区域:列表和列表的按钮栏 */}
|
|
|
+ <Card>
|
|
|
+ {/* 按钮栏 */}
|
|
|
+ <div style={{ marginBottom: 16 }}>
|
|
|
+ <Space>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ onClick={() => {
|
|
|
+ setEditingId(null)
|
|
|
+ form.resetFields()
|
|
|
+ form.setFieldsValue({
|
|
|
+ config_type: 'string',
|
|
|
+ is_editable: true,
|
|
|
+ })
|
|
|
+ setModalVisible(true)
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 创建配置
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <ResizableTable
|
|
|
+ columns={columns}
|
|
|
+ dataSource={data}
|
|
|
+ loading={loading}
|
|
|
+ rowKey="id"
|
|
|
+ actionColumnKey="action"
|
|
|
+ pagination={{
|
|
|
+ current: pagination.current,
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
+ total: pagination.total,
|
|
|
+ showSizeChanger: true,
|
|
|
+ pageSizeOptions: [10, 20, 50],
|
|
|
+ showTotal: (total) => `共 ${total} 条`,
|
|
|
+ }}
|
|
|
+ onChange={handleTableChange}
|
|
|
+ />
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Modal
|
|
|
+ title={editingId ? '编辑系统配置' : '创建系统配置'}
|
|
|
+ open={modalVisible}
|
|
|
+ onCancel={() => {
|
|
|
+ setModalVisible(false)
|
|
|
+ setEditingId(null)
|
|
|
+ form.resetFields()
|
|
|
+ }}
|
|
|
+ onOk={() => form.submit()}
|
|
|
+ width={800}
|
|
|
+ >
|
|
|
+ <Form form={form} onFinish={handleCreate} layout="vertical">
|
|
|
+ <Form.Item
|
|
|
+ name="config_key"
|
|
|
+ label="配置键"
|
|
|
+ rules={[
|
|
|
+ { required: true, message: '请输入配置键' },
|
|
|
+ { pattern: /^[a-zA-Z0-9_-]+$/, message: '配置键只能包含字母、数字、下划线和连字符' }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <Input
|
|
|
+ placeholder="请输入配置键"
|
|
|
+ disabled={!!editingId} // 编辑时不能修改配置键
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ name="config_value"
|
|
|
+ label="配置值"
|
|
|
+ rules={[{ required: true, message: '请输入配置值' }]}
|
|
|
+ >
|
|
|
+ <TextArea
|
|
|
+ rows={4}
|
|
|
+ placeholder="请输入配置值"
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ name="config_type"
|
|
|
+ label="配置类型"
|
|
|
+ rules={[{ required: true, message: '请选择配置类型' }]}
|
|
|
+ >
|
|
|
+ <Select placeholder="选择配置类型">
|
|
|
+ {configTypeOptions.map(option => (
|
|
|
+ <Select.Option key={option.value} value={option.value}>
|
|
|
+ {option.label}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item name="description" label="描述">
|
|
|
+ <TextArea rows={2} placeholder="请输入配置描述" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item name="is_editable" label="可编辑" valuePropName="checked">
|
|
|
+ <Switch checkedChildren="可编辑" unCheckedChildren="不可编辑" />
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
+
|
|
|
+ {/* 系统配置详情弹窗 */}
|
|
|
+ <Modal
|
|
|
+ title="系统配置详情"
|
|
|
+ open={detailVisible}
|
|
|
+ onCancel={() => setDetailVisible(false)}
|
|
|
+ footer={[
|
|
|
+ <Button key="close" onClick={() => setDetailVisible(false)}>
|
|
|
+ 关闭
|
|
|
+ </Button>,
|
|
|
+ ]}
|
|
|
+ width={700}
|
|
|
+ >
|
|
|
+ {detailLoading ? (
|
|
|
+ <div style={{ textAlign: 'center', padding: 40 }}>加载中...</div>
|
|
|
+ ) : detailData ? (
|
|
|
+ <div>
|
|
|
+ <Descriptions title="基本信息" bordered column={1}>
|
|
|
+ <Descriptions.Item label="ID">{detailData.id}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="配置键">{detailData.config_key}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="配置类型">
|
|
|
+ {detailData.config_type === 'int' && <Tag color="blue">整数</Tag>}
|
|
|
+ {detailData.config_type === 'string' && <Tag color="green">字符串</Tag>}
|
|
|
+ {detailData.config_type === 'bool' && <Tag color="orange">布尔值</Tag>}
|
|
|
+ {detailData.config_type === 'json' && <Tag color="purple">JSON</Tag>}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="配置类型">{detailData.config_type}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="描述">{detailData.description || '-'}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="系统配置">
|
|
|
+ {detailData.is_system ? (
|
|
|
+ <Tag color="red">系统配置</Tag>
|
|
|
+ ) : (
|
|
|
+ <Tag color="default">自定义配置</Tag>
|
|
|
+ )}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="可编辑">
|
|
|
+ {detailData.is_editable ? (
|
|
|
+ <Tag color="green">可编辑</Tag>
|
|
|
+ ) : (
|
|
|
+ <Tag color="default">不可编辑</Tag>
|
|
|
+ )}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="创建时间">
|
|
|
+ {detailData.created_at
|
|
|
+ ? dayjs(detailData.created_at).format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ : '-'}
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="更新时间">
|
|
|
+ {detailData.updated_at
|
|
|
+ ? dayjs(detailData.updated_at).format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ : '-'}
|
|
|
+ </Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+ <div style={{ marginTop: 24 }}>
|
|
|
+ <h3>配置值</h3>
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ background: '#f5f5f5',
|
|
|
+ padding: 16,
|
|
|
+ borderRadius: 4,
|
|
|
+ whiteSpace: 'pre-wrap',
|
|
|
+ wordBreak: 'break-word',
|
|
|
+ fontFamily: 'monospace',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {detailData.config_type === 'json'
|
|
|
+ ? JSON.stringify(JSON.parse(detailData.config_value), null, 2)
|
|
|
+ : detailData.config_value}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </Modal>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|