Files
hfb_jysh/frontend/src/features/admin/AdminPushPage.tsx
T

336 lines
11 KiB
TypeScript

import { useState } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Alert, App, Button, Form, Input, InputNumber, Modal, Switch, Tag, Typography } from 'antd'
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'
import {
createPushChannel,
fetchPushChannels,
fetchPushRules,
testPushChannel,
updatePushChannel,
updatePushRule,
} from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { DataTable } from '@/features/admin/components/DataTable'
import type { AdminPushChannel, AdminPushRule } from '@/features/admin/types'
import { useAdminSessionStore } from '@/stores/adminSession'
import { formatDateTime, formatText } from '@/shared/utils/format'
function errorText(error: unknown) {
const response = (error as { response?: { data?: { message?: string } } })?.response
return response?.data?.message || (error instanceof Error ? error.message : '请求失败')
}
export function AdminPushPage() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [form] = Form.useForm<{ name: string; type: string; config: string }>()
const [ruleForm] = Form.useForm<{ threshold: number; message_template: string }>()
const [open, setOpen] = useState(false)
const [ruleTarget, setRuleTarget] = useState<AdminPushRule>()
const [submitting, setSubmitting] = useState(false)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
const canView = Boolean(permissions?.includes('*') || permissions?.includes('system_config:view'))
const canManage = Boolean(permissions?.includes('*') || permissions?.includes('system_config:update'))
const channelsQuery = useQuery({
queryKey: ['admin-push-channels'],
queryFn: fetchPushChannels,
enabled: canView,
})
const rulesQuery = useQuery({
queryKey: ['admin-push-rules'],
queryFn: fetchPushRules,
enabled: canView,
})
const invalidate = () =>
Promise.all([
queryClient.invalidateQueries({ queryKey: ['admin-push-channels'] }),
queryClient.invalidateQueries({ queryKey: ['admin-push-rules'] }),
])
const channels = Array.isArray(channelsQuery.data) ? channelsQuery.data : []
const rules = Array.isArray(rulesQuery.data) ? rulesQuery.data : []
const create = async () => {
const values = await form.validateFields()
let config: Record<string, unknown>
try {
config = JSON.parse(values.config || '{}')
} catch {
message.error('配置必须是合法 JSON')
return
}
setSubmitting(true)
try {
await createPushChannel({ name: values.name.trim(), type: values.type.trim(), config })
message.success('推送渠道已创建')
setOpen(false)
await invalidate()
} catch (error) {
message.error(errorText(error))
} finally {
setSubmitting(false)
}
}
if (!canView) {
return (
<div className="admin-page">
<AdminPageHeader eyebrow="JIEYOU OPS · 运营" title="推送通知" />
<Alert type="warning" showIcon message="当前账号没有系统配置权限" />
</div>
)
}
return (
<div className="admin-page">
<AdminPageHeader
eyebrow="JIEYOU OPS · 运营"
title="推送通知"
description="维护推送渠道(Webhook/企业微信等)与通知规则。"
meta={
<Tag color="geekblue">
渠道 {channels.length} · 规则 {rules.length}
</Tag>
}
extra={
<Button
icon={<ReloadOutlined />}
loading={channelsQuery.isFetching || rulesQuery.isFetching}
onClick={() => {
void channelsQuery.refetch()
void rulesQuery.refetch()
}}
>
刷新
</Button>
}
/>
<div className="admin-stack" style={{ marginBottom: 12 }}>
<Typography.Title level={4} style={{ margin: 0 }}>
推送渠道
</Typography.Title>
</div>
<DataTable<AdminPushChannel>
columns={[
{
title: '名称 / 类型',
key: 'name',
width: 240,
render: (_, row) => (
<div className="admin-stack">
<strong>{row.name}</strong>
<span className="admin-mono">{row.type}</span>
</div>
),
},
{
title: '配置',
key: 'config',
render: (_, row) => (
<Typography.Paragraph ellipsis={{ rows: 2 }} style={{ marginBottom: 0 }}>
{JSON.stringify(row.config ?? {})}
</Typography.Paragraph>
),
},
{
title: '状态',
key: 'enabled',
width: 110,
render: (_, row) => (
<Switch
checked={row.enabled}
disabled={!canManage}
onChange={async checked => {
try {
await updatePushChannel(row.id, { enabled: checked })
message.success(checked ? '已启用' : '已停用')
await invalidate()
} catch (error) {
message.error(errorText(error))
}
}}
/>
),
},
{
title: '更新时间',
dataIndex: 'updated_at',
width: 170,
render: value => formatDateTime(value),
},
{
title: '操作',
key: 'actions',
width: 200,
render: (_, row) => (
<Button
type="link"
size="small"
disabled={!canManage}
onClick={async () => {
try {
const result = await testPushChannel(row.id)
message.success(result?.message || '测试消息已发送')
} catch (error) {
message.error(errorText(error))
}
}}
>
发送测试
</Button>
),
},
]}
dataSource={channels}
rowKey="id"
loading={channelsQuery.isFetching}
extra={
<Button
type="primary"
size="small"
icon={<PlusOutlined />}
disabled={!canManage}
onClick={() => {
form.setFieldsValue({ name: '', type: 'webhook', config: '{}' })
setOpen(true)
}}
>
新建渠道
</Button>
}
emptyDescription="还没有推送渠道"
/>
<div className="admin-stack" style={{ margin: '20px 0 12px' }}>
<Typography.Title level={4} style={{ margin: 0 }}>
通知规则
</Typography.Title>
</div>
<DataTable<AdminPushRule>
columns={[
{
title: '事件',
dataIndex: 'event',
width: 240,
render: value => <span className="admin-mono">{formatText(value)}</span>,
},
{
title: '消息模板',
dataIndex: 'message_template',
render: value => formatText(value),
},
{
title: '阈值',
dataIndex: 'threshold',
width: 100,
align: 'right',
},
{
title: '状态',
key: 'enabled',
width: 110,
render: (_, row) => (
<Switch
checked={row.enabled}
disabled={!canManage}
onChange={async checked => {
try {
await updatePushRule(row.id, { enabled: checked })
await invalidate()
} catch (error) {
message.error(errorText(error))
}
}}
/>
),
},
{
title: '操作',
key: 'actions',
width: 100,
render: (_, row) => (
<Button
type="link"
size="small"
disabled={!canManage}
onClick={() => {
setRuleTarget(row)
ruleForm.setFieldsValue({
threshold: row.threshold,
message_template: row.message_template,
})
}}
>
编辑
</Button>
),
},
]}
dataSource={rules}
rowKey="id"
loading={rulesQuery.isFetching}
emptyDescription="还没有通知规则"
/>
<Modal
open={open}
title="新建推送渠道"
okText="创建"
confirmLoading={submitting}
onOk={() => void create()}
onCancel={() => setOpen(false)}
>
<Form form={form} layout="vertical" requiredMark={false}>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input maxLength={64} />
</Form.Item>
<Form.Item name="type" label="类型" rules={[{ required: true, message: '请输入类型' }]}>
<Input placeholder="webhook / wecom / feishu" maxLength={32} />
</Form.Item>
<Form.Item name="config" label="配置 JSON" rules={[{ required: true }]}>
<Input.TextArea rows={5} placeholder='{"url":"https://..."}' />
</Form.Item>
</Form>
</Modal>
<Modal
open={Boolean(ruleTarget)}
title={ruleTarget ? `编辑规则 · ${ruleTarget.event}` : '编辑规则'}
okText="保存"
onCancel={() => setRuleTarget(undefined)}
onOk={async () => {
if (!ruleTarget) return
const values = await ruleForm.validateFields()
try {
await updatePushRule(ruleTarget.id, {
threshold: values.threshold,
message_template: values.message_template,
})
message.success('规则已更新')
setRuleTarget(undefined)
await invalidate()
} catch (error) {
message.error(errorText(error))
}
}}
>
<Form form={ruleForm} layout="vertical" requiredMark={false}>
<Form.Item name="threshold" label="阈值">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="message_template" label="消息模板">
<Input.TextArea rows={4} maxLength={255} showCount />
</Form.Item>
</Form>
</Modal>
</div>
)
}