优化代码

This commit is contained in:
dushixiang committed 2025-11-11 22:03:46 +08:00
1 parent 4d904fd185
commit cbce63db8a
3 files changed
+666

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
import { get, post, put } from './request';
import type { NotificationChannel } from '../types';
const PROPERTY_ID_NOTIFICATION_CHANNELS = 'notification_channels';
// 获取通知渠道列表
export const getNotificationChannels = async (): Promise<NotificationChannel[]> => {
const response = await get<{ id: string; name: string; value: NotificationChannel[] }>(
`/admin/properties/${PROPERTY_ID_NOTIFICATION_CHANNELS}`
);
return response.data.value || [];
};
// 保存通知渠道列表
export const saveNotificationChannels = async (channels: NotificationChannel[]): Promise<void> => {
await put(`/admin/properties/${PROPERTY_ID_NOTIFICATION_CHANNELS}`, {
name: '通知渠道配置',
value: channels,
});
};
// 测试通知渠道(从数据库读取配置)
export const testNotificationChannel = async (type: string): Promise<{ message: string }> => {
const response = await post<{ message: string }>(`/admin/notification-channels/${type}/test`);
return response.data;
};
@@ -0,0 +1,344 @@
import { useState } from 'react';
import { Button, Table, Space, App, Modal, Form, Input, Switch, Tag, Select } from 'antd';
import { Plus, Pencil, Trash2, TestTube } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { NotificationChannel } from '../../types';
import {
getNotificationChannels,
saveNotificationChannels,
testNotificationChannel,
} from '../../api/notification-channel';
import { getErrorMessage } from '../../lib/utils';
// 支持的渠道类型
const CHANNEL_TYPES = [
{ label: '钉钉', value: 'dingtalk' },
{ label: '企业微信', value: 'wecom' },
{ label: '飞书', value: 'feishu' },
{ label: '自定义Webhook', value: 'webhook' },
];
const NotificationChannels = () => {
const [form] = Form.useForm();
const { message: messageApi, modal: modalApi } = App.useApp();
const queryClient = useQueryClient();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
const [editingType, setEditingType] = useState<string | null>(null);
const [selectedType, setSelectedType] = useState<string>('');
// 获取通知渠道列表
const { data: channels = [], isLoading } = useQuery({
queryKey: ['notificationChannels'],
queryFn: getNotificationChannels,
});
// 保存 mutation
const saveMutation = useMutation({
mutationFn: saveNotificationChannels,
onSuccess: () => {
messageApi.success('保存成功');
queryClient.invalidateQueries({ queryKey: ['notificationChannels'] });
setIsModalOpen(false);
setEditingType(null);
form.resetFields();
},
onError: (error: unknown) => {
messageApi.error(getErrorMessage(error, '保存失败'));
},
});
// 测试 mutation
const testMutation = useMutation({
mutationFn: testNotificationChannel,
onSuccess: () => {
messageApi.success('测试通知已发送');
},
onError: (error: unknown) => {
messageApi.error(getErrorMessage(error, '测试失败'));
},
});
const handleAdd = () => {
setIsSelectModalOpen(true);
};
const handleSelectType = () => {
if (!selectedType) {
messageApi.warning('请选择渠道类型');
return;
}
// 检查是否已存在
const exists = channels.find((ch) => ch.type === selectedType);
if (exists) {
messageApi.warning('该渠道类型已存在,请直接编辑');
setIsSelectModalOpen(false);
return;
}
setEditingType(selectedType);
form.setFieldsValue({
type: selectedType,
enabled: true,
});
setIsSelectModalOpen(false);
setIsModalOpen(true);
setSelectedType('');
};
const handleEdit = (record: NotificationChannel) => {
setEditingType(record.type);
// 将配置展开到表单字段
form.setFieldsValue({
type: record.type,
enabled: record.enabled,
...record.config, // 展开配置对象
});
setIsModalOpen(true);
};
const handleDelete = (type: string) => {
modalApi.confirm({
title: '确认删除',
content: '确定要删除这个通知渠道配置吗?',
onOk: () => {
// 从列表中删除该类型的渠道
const newChannels = channels.filter((ch) => ch.type !== type);
saveMutation.mutate(newChannels);
},
});
};
const handleTest = (type: string) => {
testMutation.mutate(type);
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
const { type, enabled, ...configFields } = values;
// 构建配置对象
const config: Record<string, any> = {};
Object.keys(configFields).forEach((key) => {
if (configFields[key] !== undefined && configFields[key] !== '') {
config[key] = configFields[key];
}
});
const newChannel: NotificationChannel = {
type,
enabled,
config,
};
// 更新或添加渠道
const existingIndex = channels.findIndex((ch) => ch.type === type);
let newChannels: NotificationChannel[];
if (existingIndex >= 0) {
// 更新现有渠道
newChannels = [...channels];
newChannels[existingIndex] = newChannel;
} else {
// 添加新渠道
newChannels = [...channels, newChannel];
}
saveMutation.mutate(newChannels);
} catch (error) {
// 表单验证失败
}
};
const getTypeLabel = (type: string) => {
const types = {
dingtalk: '钉钉',
wecom: '企业微信',
feishu: '飞书',
email: '邮件',
webhook: '自定义Webhook',
};
return types[type as keyof typeof types] || type;
};
const columns = [
{
title: '渠道类型',
dataIndex: 'type',
key: 'type',
render: (type: string) => getTypeLabel(type),
},
{
title: '状态',
dataIndex: 'enabled',
key: 'enabled',
render: (enabled: boolean) =>
enabled ? <Tag color="success">启用</Tag> : <Tag>禁用</Tag>,
},
{
title: '操作',
key: 'action',
render: (_: unknown, record: NotificationChannel) => (
<Space>
<Button
type="link"
size="small"
icon={<TestTube size={14} />}
onClick={() => handleTest(record.type)}
loading={testMutation.isPending}
disabled={!record.enabled}
>
测试
</Button>
<Button type="link" size="small" icon={<Pencil size={14} />} onClick={() => handleEdit(record)}>
配置
</Button>
<Button
type="link"
size="small"
danger
icon={<Trash2 size={14} />}
onClick={() => handleDelete(record.type)}
>
删除
</Button>
</Space>
),
},
];
// 获取未配置的渠道类型
const availableTypes = CHANNEL_TYPES.filter((type) => !channels.find((ch) => ch.type === type.value));
return (
<div>
<div className="mb-4 flex justify-between items-start">
<div>
<h2 className="text-xl font-bold">通知渠道管理</h2>
<p className="text-gray-500 mt-2">配置钉钉、企业微信、飞书和自定义Webhook通知渠道</p>
</div>
<Button type="primary" icon={<Plus size={16} />} onClick={handleAdd} disabled={availableTypes.length === 0}>
添加渠道
</Button>
</div>
<Table columns={columns} dataSource={channels} rowKey="type" loading={isLoading} />
{/* 选择渠道类型对话框 */}
<Modal
title="选择通知渠道类型"
open={isSelectModalOpen}
onOk={handleSelectType}
onCancel={() => {
setIsSelectModalOpen(false);
setSelectedType('');
}}
width={400}
>
<Form layout="vertical">
<Form.Item label="渠道类型" required>
<Select
placeholder="请选择要添加的渠道类型"
value={selectedType || undefined}
onChange={setSelectedType}
options={availableTypes}
/>
</Form.Item>
</Form>
</Modal>
<Modal
title={`配置${getTypeLabel(editingType || '')}`}
open={isModalOpen}
onOk={handleSubmit}
onCancel={() => {
setIsModalOpen(false);
setEditingType(null);
form.resetFields();
}}
confirmLoading={saveMutation.isPending}
width={600}
>
<Form form={form} layout="vertical">
<Form.Item name="type" hidden>
<Input />
</Form.Item>
<Form.Item label="启用" name="enabled" valuePropName="checked" initialValue={true}>
<Switch />
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.type !== curr.type}>
{({ getFieldValue }) => {
const type = getFieldValue('type');
if (type === 'dingtalk') {
return (
<>
<Form.Item
label="访问令牌 (Access Token)"
name="secretKey"
rules={[{ required: true, message: '请输入访问令牌' }]}
tooltip="在钉钉机器人配置中获取的 access_token"
>
<Input placeholder="输入访问令牌" />
</Form.Item>
<Form.Item label="加签密钥(可选)" name="signSecret" tooltip="如果启用了加签,请填写 SEC 开头的密钥">
<Input.Password placeholder="SEC 开头的加签密钥" />
</Form.Item>
</>
);
}
if (type === 'wecom') {
return (
<Form.Item
label="Webhook Key"
name="secretKey"
rules={[{ required: true, message: '请输入 Webhook Key' }]}
tooltip="企业微信群机器人的 Webhook Key"
>
<Input placeholder="输入 Webhook Key" />
</Form.Item>
);
}
if (type === 'feishu') {
return (
<>
<Form.Item
label="Webhook Token"
name="secretKey"
rules={[{ required: true, message: '请输入 Webhook Token' }]}
tooltip="飞书群机器人的 Webhook Token"
>
<Input placeholder="输入 Webhook Token" />
</Form.Item>
<Form.Item label="签名密钥(可选)" name="signSecret" tooltip="如果启用了签名验证,请填写密钥">
<Input.Password placeholder="输入签名密钥" />
</Form.Item>
</>
);
}
if (type === 'webhook') {
return (
<Form.Item
label="Webhook URL"
name="url"
rules={[
{ required: true, message: '请输入 Webhook URL' },
{ type: 'url', message: '请输入有效的 URL' },
]}
>
<Input placeholder="https://your-server.com/webhook" />
</Form.Item>
);
}
return null;
}}
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default NotificationChannels;
@@ -0,0 +1,296 @@
import { useState } from 'react';
import { Button, Table, Space, App, Modal, Form, Input, Switch, Select, Tag } from 'antd';
import { Plus, Pencil, Trash2, TestTube } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { NotificationChannel } from '../../types';
import {
getNotificationChannels,
createNotificationChannel,
updateNotificationChannel,
deleteNotificationChannel,
testNotificationChannel,
} from '../../api/notification-channel';
import { getErrorMessage } from '../../lib/utils';
const NotificationChannels = () => {
const [form] = Form.useForm();
const { message: messageApi, modal: modalApi } = App.useApp();
const queryClient = useQueryClient();
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingChannel, setEditingChannel] = useState<NotificationChannel | null>(null);
// 获取通知渠道列表
const { data: channels = [], isLoading } = useQuery({
queryKey: ['notificationChannels'],
queryFn: getNotificationChannels,
});
// 创建 mutation
const createMutation = useMutation({
mutationFn: createNotificationChannel,
onSuccess: () => {
messageApi.success('创建成功');
queryClient.invalidateQueries({ queryKey: ['notificationChannels'] });
setIsModalOpen(false);
form.resetFields();
},
onError: (error: unknown) => {
messageApi.error(getErrorMessage(error, '创建失败'));
},
});
// 更新 mutation
const updateMutation = useMutation({
mutationFn: ({ id, channel }: { id: string; channel: NotificationChannel }) =>
updateNotificationChannel(id, channel),
onSuccess: () => {
messageApi.success('更新成功');
queryClient.invalidateQueries({ queryKey: ['notificationChannels'] });
setIsModalOpen(false);
setEditingChannel(null);
form.resetFields();
},
onError: (error: unknown) => {
messageApi.error(getErrorMessage(error, '更新失败'));
},
});
// 删除 mutation
const deleteMutation = useMutation({
mutationFn: deleteNotificationChannel,
onSuccess: () => {
messageApi.success('删除成功');
queryClient.invalidateQueries({ queryKey: ['notificationChannels'] });
},
onError: (error: unknown) => {
messageApi.error(getErrorMessage(error, '删除失败'));
},
});
// 测试 mutation
const testMutation = useMutation({
mutationFn: testNotificationChannel,
onSuccess: () => {
messageApi.success('测试通知已发送');
},
onError: (error: unknown) => {
messageApi.error(getErrorMessage(error, '测试失败'));
},
});
const handleCreate = () => {
setEditingChannel(null);
form.resetFields();
setIsModalOpen(true);
};
const handleEdit = (record: NotificationChannel) => {
setEditingChannel(record);
form.setFieldsValue(record);
setIsModalOpen(true);
};
const handleDelete = (id: string) => {
modalApi.confirm({
title: '确认删除',
content: '确定要删除这个通知渠道吗?',
onOk: () => deleteMutation.mutate(id),
});
};
const handleTest = (id: string) => {
testMutation.mutate(id);
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (editingChannel?.id) {
updateMutation.mutate({ id: editingChannel.id, channel: values });
} else {
createMutation.mutate(values);
}
} catch (error) {
// 表单验证失败
}
};
const getTypeLabel = (type: string) => {
const types = {
dingtalk: '钉钉',
wecom: '企业微信',
feishu: '飞书',
email: '邮件',
webhook: '自定义Webhook',
};
return types[type as keyof typeof types] || type;
};
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
render: (type: string) => getTypeLabel(type),
},
{
title: '状态',
dataIndex: 'enabled',
key: 'enabled',
render: (enabled: boolean) =>
enabled ? <Tag color="success">启用</Tag> : <Tag>禁用</Tag>,
},
{
title: '操作',
key: 'action',
render: (_: unknown, record: NotificationChannel) => (
<Space>
<Button
type="link"
size="small"
icon={<TestTube size={14} />}
onClick={() => handleTest(record.id!)}
loading={testMutation.isPending}
>
测试
</Button>
<Button type="link" size="small" icon={<Pencil size={14} />} onClick={() => handleEdit(record)}>
编辑
</Button>
<Button
type="link"
size="small"
danger
icon={<Trash2 size={14} />}
onClick={() => handleDelete(record.id!)}
>
删除
</Button>
</Space>
),
},
];
return (
<div>
<div className="mb-4 flex justify-between items-center">
<h2 className="text-xl font-bold">通知渠道管理</h2>
<Button type="primary" icon={<Plus size={16} />} onClick={handleCreate}>
创建通知渠道
</Button>
</div>
<Table columns={columns} dataSource={channels} rowKey="id" loading={isLoading} />
<Modal
title={editingChannel ? '编辑通知渠道' : '创建通知渠道'}
open={isModalOpen}
onOk={handleSubmit}
onCancel={() => {
setIsModalOpen(false);
setEditingChannel(null);
form.resetFields();
}}
confirmLoading={createMutation.isPending || updateMutation.isPending}
width={600}
>
<Form form={form} layout="vertical">
<Form.Item label="渠道名称" name="name" rules={[{ required: true, message: '请输入渠道名称' }]}>
<Input placeholder="例如:运维群通知" />
</Form.Item>
<Form.Item label="渠道类型" name="type" rules={[{ required: true, message: '请选择渠道类型' }]}>
<Select
placeholder="选择通知渠道类型"
options={[
{ label: '钉钉', value: 'dingtalk' },
{ label: '企业微信', value: 'wecom' },
{ label: '飞书', value: 'feishu' },
{ label: '自定义Webhook', value: 'webhook' },
]}
/>
</Form.Item>
<Form.Item label="启用" name="enabled" valuePropName="checked" initialValue={true}>
<Switch />
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.type !== curr.type}>
{({ getFieldValue }) => {
const type = getFieldValue('type');
if (type === 'dingtalk') {
return (
<>
<Form.Item
label="Webhook URL"
name="dingTalkWebhook"
rules={[
{ required: true, message: '请输入钉钉 Webhook URL' },
{ type: 'url', message: '请输入有效的 URL' },
]}
>
<Input placeholder="https://oapi.dingtalk.com/robot/send?access_token=..." />
</Form.Item>
<Form.Item label="加签密钥(可选)" name="dingTalkSecret">
<Input.Password placeholder="SEC 开头的加签密钥" />
</Form.Item>
</>
);
}
if (type === 'wecom') {
return (
<Form.Item
label="Webhook URL"
name="weComWebhook"
rules={[
{ required: true, message: '请输入企业微信 Webhook URL' },
{ type: 'url', message: '请输入有效的 URL' },
]}
>
<Input placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." />
</Form.Item>
);
}
if (type === 'feishu') {
return (
<Form.Item
label="Webhook URL"
name="feishuWebhook"
rules={[
{ required: true, message: '请输入飞书 Webhook URL' },
{ type: 'url', message: '请输入有效的 URL' },
]}
>
<Input placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/..." />
</Form.Item>
);
}
if (type === 'webhook') {
return (
<Form.Item
label="Webhook URL"
name="customWebhookUrl"
rules={[
{ required: true, message: '请输入自定义 Webhook URL' },
{ type: 'url', message: '请输入有效的 URL' },
]}
>
<Input placeholder="https://your-server.com/webhook" />
</Form.Item>
);
}
return null;
}}
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default NotificationChannels;