mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-24 11:57:15 +00:00
feat: replace notification checkboxes with card-based layout (#5421)
Replace EventBusCheckboxes with card-based notification settings: - Each event group gets its own card with responsive grid layout - Master checkbox per group with indeterminate state - Inline parameter inputs (CPU threshold) appear when enabled - Theme-adaptive via Ant Design Card component Components: - NotificationLayout, NotificationCard, NotificationHeader, NotificationEvent - TelegramNotifications, EmailNotifications with explicit event configs
This commit is contained in:
@@ -1,147 +0,0 @@
|
|||||||
import { Checkbox, Collapse, InputNumber, Space } from 'antd';
|
|
||||||
import { DownOutlined, RightOutlined } from '@ant-design/icons';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
interface EventGroup {
|
|
||||||
key: string;
|
|
||||||
labelKey: string;
|
|
||||||
events: { value: string; labelKey: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const EVENT_GROUPS: EventGroup[] = [
|
|
||||||
{
|
|
||||||
key: 'outbound',
|
|
||||||
labelKey: 'pages.settings.eventGroupOutbound',
|
|
||||||
events: [
|
|
||||||
{ value: 'outbound.down', labelKey: 'pages.settings.eventOutboundDown' },
|
|
||||||
{ value: 'outbound.up', labelKey: 'pages.settings.eventOutboundUp' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'xray',
|
|
||||||
labelKey: 'pages.settings.eventGroupXray',
|
|
||||||
events: [
|
|
||||||
{ value: 'xray.crash', labelKey: 'pages.settings.eventXrayCrash' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'node',
|
|
||||||
labelKey: 'pages.settings.eventGroupNode',
|
|
||||||
events: [
|
|
||||||
{ value: 'node.down', labelKey: 'pages.settings.eventNodeDown' },
|
|
||||||
{ value: 'node.up', labelKey: 'pages.settings.eventNodeUp' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'system',
|
|
||||||
labelKey: 'pages.settings.eventGroupSystem',
|
|
||||||
events: [
|
|
||||||
{ value: 'cpu.high', labelKey: 'pages.settings.eventCPUHigh' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'security',
|
|
||||||
labelKey: 'pages.settings.eventGroupSecurity',
|
|
||||||
events: [
|
|
||||||
{ value: 'login.attempt', labelKey: 'pages.settings.eventLoginAttempt' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface EventBusCheckboxesProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (v: string) => void;
|
|
||||||
/** Maps event value → { key: setting field name, value: current value } for inline inputs */
|
|
||||||
extra?: Record<string, { key: string; value: number }>;
|
|
||||||
/** Callback when extra input changes: (settingKey, newValue) => void */
|
|
||||||
onExtraChange?: (key: string, v: number | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EventBusCheckboxes({ value, onChange, extra, onExtraChange }: EventBusCheckboxesProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const selected = value ? value.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
||||||
|
|
||||||
function toggle(eventType: string) {
|
|
||||||
const next = selected.includes(eventType)
|
|
||||||
? selected.filter((e) => e !== eventType)
|
|
||||||
: [...selected, eventType];
|
|
||||||
onChange(next.join(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleGroup(group: EventGroup) {
|
|
||||||
const groupValues = group.events.map((e) => e.value);
|
|
||||||
const allSelected = groupValues.every((v) => selected.includes(v));
|
|
||||||
let next: string[];
|
|
||||||
if (allSelected) {
|
|
||||||
next = selected.filter((v) => !groupValues.includes(v));
|
|
||||||
} else {
|
|
||||||
next = [...new Set([...selected, ...groupValues])];
|
|
||||||
}
|
|
||||||
onChange(next.join(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = EVENT_GROUPS.map((group) => {
|
|
||||||
const count = group.events.filter((e) => selected.includes(e.value)).length;
|
|
||||||
const total = group.events.length;
|
|
||||||
const allSelected = count === total;
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: group.key,
|
|
||||||
label: (
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<span style={{ fontWeight: 500 }}>{t(group.labelKey)}</span>
|
|
||||||
<span style={{ color: '#999', fontSize: 12 }}>
|
|
||||||
{count}/{total}
|
|
||||||
</span>
|
|
||||||
<Checkbox
|
|
||||||
checked={allSelected}
|
|
||||||
indeterminate={count > 0 && count < total}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onChange={() => toggleGroup(group)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
children: (
|
|
||||||
<Checkbox.Group value={selected} style={{ width: '100%' }}>
|
|
||||||
<Space wrap size={[16, 4]}>
|
|
||||||
{group.events.map((et) => {
|
|
||||||
const checked = selected.includes(et.value);
|
|
||||||
const extraConf = extra?.[et.value];
|
|
||||||
return (
|
|
||||||
<span key={et.value} style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
|
||||||
<Checkbox value={et.value} onChange={() => toggle(et.value)}>
|
|
||||||
{t(et.labelKey)}
|
|
||||||
</Checkbox>
|
|
||||||
{extraConf && onExtraChange && (
|
|
||||||
<InputNumber
|
|
||||||
size="small"
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
value={extraConf.value}
|
|
||||||
disabled={!checked}
|
|
||||||
onChange={(v) => onExtraChange(extraConf.key, v)}
|
|
||||||
style={{ width: 60 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Space>
|
|
||||||
</Checkbox.Group>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultActiveKeys = EVENT_GROUPS
|
|
||||||
.filter((g) => g.events.some((e) => selected.includes(e.value)))
|
|
||||||
.map((g) => g.key);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapse
|
|
||||||
items={items}
|
|
||||||
defaultActiveKey={defaultActiveKeys.length > 0 ? defaultActiveKeys : ['outbound']}
|
|
||||||
expandIcon={({ isActive }) => isActive ? <DownOutlined /> : <RightOutlined />}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export { default as InputAddon } from './InputAddon';
|
export { default as InputAddon } from './InputAddon';
|
||||||
export { default as InfinityIcon } from './InfinityIcon';
|
export { default as InfinityIcon } from './InfinityIcon';
|
||||||
export { default as SettingListItem } from './SettingListItem';
|
export { default as SettingListItem } from './SettingListItem';
|
||||||
export { EventBusCheckboxes } from './EventBusCheckboxes';
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { InputNumber } from 'antd';
|
||||||
|
import { CloudServerOutlined, ThunderboltOutlined, DesktopOutlined, DashboardOutlined, SafetyOutlined } from '@ant-design/icons';
|
||||||
|
import type { AllSetting } from '@/models/setting';
|
||||||
|
import { NotificationLayout } from './NotificationLayout';
|
||||||
|
import { NotificationGroup } from './NotificationGroup';
|
||||||
|
import type { NotificationGroupConfig } from './types';
|
||||||
|
|
||||||
|
const GROUPS: NotificationGroupConfig[] = [
|
||||||
|
{
|
||||||
|
icon: <CloudServerOutlined />,
|
||||||
|
title: 'eventGroupOutbound',
|
||||||
|
events: [
|
||||||
|
{ key: 'outbound.down', label: 'eventOutboundDown', settingKey: '' },
|
||||||
|
{ key: 'outbound.up', label: 'eventOutboundUp', settingKey: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ThunderboltOutlined />,
|
||||||
|
title: 'eventGroupXray',
|
||||||
|
events: [
|
||||||
|
{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <DesktopOutlined />,
|
||||||
|
title: 'eventGroupNode',
|
||||||
|
events: [
|
||||||
|
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||||
|
{ key: 'node.up', label: 'eventNodeUp', settingKey: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <DashboardOutlined />,
|
||||||
|
title: 'eventGroupSystem',
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
key: 'cpu.high',
|
||||||
|
label: 'eventCPUHigh',
|
||||||
|
settingKey: 'smtpCpu',
|
||||||
|
extra: ({ value, onChange }) => (
|
||||||
|
<InputNumber size="small" min={0} max={100} value={value} onChange={onChange} style={{ width: 80 }} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <SafetyOutlined />,
|
||||||
|
title: 'eventGroupSecurity',
|
||||||
|
events: [
|
||||||
|
{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
allSetting: AllSetting;
|
||||||
|
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmailNotifications({ allSetting, updateSetting }: Props) {
|
||||||
|
const events = allSetting.smtpEnabledEvents || '';
|
||||||
|
const selected = events ? events.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
||||||
|
|
||||||
|
function toggle(key: string) {
|
||||||
|
const next = selected.includes(key)
|
||||||
|
? selected.filter((e) => e !== key)
|
||||||
|
: [...selected, key];
|
||||||
|
updateSetting({ smtpEnabledEvents: next.join(',') });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAll(keys: string[]) {
|
||||||
|
const allSelected = keys.every((v) => selected.includes(v));
|
||||||
|
const next = allSelected
|
||||||
|
? selected.filter((v) => !keys.includes(v))
|
||||||
|
: [...new Set([...selected, ...keys])];
|
||||||
|
updateSetting({ smtpEnabledEvents: next.join(',') });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationLayout>
|
||||||
|
{GROUPS.map((group, i) => (
|
||||||
|
<NotificationGroup
|
||||||
|
key={i}
|
||||||
|
config={group}
|
||||||
|
selected={selected}
|
||||||
|
onToggle={toggle}
|
||||||
|
onToggleAll={toggleAll}
|
||||||
|
allSetting={allSetting}
|
||||||
|
updateSetting={updateSetting}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</NotificationLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { Card } from 'antd';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: ReactNode;
|
||||||
|
title: ReactNode;
|
||||||
|
extra: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationCard({ icon, title, extra, children }: Props) {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
title={<span>{icon} {title}</span>}
|
||||||
|
extra={extra}
|
||||||
|
style={{ borderWidth: 1 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { Checkbox } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
checked: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationEvent({ label, checked, onToggle, children }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Checkbox checked={checked} onChange={onToggle}>
|
||||||
|
{t(label)}
|
||||||
|
</Checkbox>
|
||||||
|
{checked && children && (
|
||||||
|
<div style={{ paddingLeft: 24, marginTop: 4 }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Space } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { AllSetting } from '@/models/setting';
|
||||||
|
import type { NotificationGroupConfig } from './types';
|
||||||
|
import { NotificationCard } from './NotificationCard';
|
||||||
|
import { NotificationHeader } from './NotificationHeader';
|
||||||
|
import { NotificationEvent } from './NotificationEvent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
config: NotificationGroupConfig;
|
||||||
|
selected: string[];
|
||||||
|
onToggle: (key: string) => void;
|
||||||
|
onToggleAll: (keys: string[]) => void;
|
||||||
|
allSetting: AllSetting;
|
||||||
|
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationGroup({ config, selected, onToggle, onToggleAll, allSetting, updateSetting }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const count = config.events.filter((e) => selected.includes(e.key)).length;
|
||||||
|
const total = config.events.length;
|
||||||
|
|
||||||
|
function toggleAll() {
|
||||||
|
const values = config.events.map((e) => e.key);
|
||||||
|
onToggleAll(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationCard
|
||||||
|
icon={config.icon}
|
||||||
|
title={t(`pages.settings.${config.title}`)}
|
||||||
|
extra={
|
||||||
|
<NotificationHeader
|
||||||
|
count={count}
|
||||||
|
total={total}
|
||||||
|
allSelected={count === total}
|
||||||
|
indeterminate={count > 0 && count < total}
|
||||||
|
onToggleAll={toggleAll}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||||
|
{config.events.map((event) => (
|
||||||
|
<NotificationEvent
|
||||||
|
key={event.key}
|
||||||
|
label={t(`pages.settings.${event.label}`)}
|
||||||
|
checked={selected.includes(event.key)}
|
||||||
|
onToggle={() => onToggle(event.key)}
|
||||||
|
>
|
||||||
|
{event.extra?.({
|
||||||
|
value: Number((allSetting as unknown as Record<string, unknown>)[event.settingKey]) || 0,
|
||||||
|
onChange: (v) => updateSetting({ [event.settingKey]: v }),
|
||||||
|
})}
|
||||||
|
</NotificationEvent>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</NotificationCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useRef, useEffect } from 'react';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
count: number;
|
||||||
|
total: number;
|
||||||
|
allSelected: boolean;
|
||||||
|
indeterminate: boolean;
|
||||||
|
onToggleAll: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MasterCheckbox({ checked, indeterminate, onChange }: { checked: boolean; indeterminate: boolean; onChange: () => void }) {
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) ref.current.indeterminate = indeterminate;
|
||||||
|
}, [indeterminate]);
|
||||||
|
return <input ref={ref} type="checkbox" checked={checked} onChange={onChange} style={{ cursor: 'pointer' }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationHeader({ count, total, allSelected, indeterminate, onToggleAll }: Props) {
|
||||||
|
return (
|
||||||
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<Tag>{count}/{total}</Tag>
|
||||||
|
<MasterCheckbox checked={allSelected} indeterminate={indeterminate} onChange={onToggleAll} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationLayout({ children }: Props) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 12 }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { InputNumber } from 'antd';
|
||||||
|
import { CloudServerOutlined, ThunderboltOutlined, DesktopOutlined, DashboardOutlined, SafetyOutlined } from '@ant-design/icons';
|
||||||
|
import type { AllSetting } from '@/models/setting';
|
||||||
|
import { NotificationLayout } from './NotificationLayout';
|
||||||
|
import { NotificationGroup } from './NotificationGroup';
|
||||||
|
import type { NotificationGroupConfig } from './types';
|
||||||
|
|
||||||
|
const GROUPS: NotificationGroupConfig[] = [
|
||||||
|
{
|
||||||
|
icon: <CloudServerOutlined />,
|
||||||
|
title: 'eventGroupOutbound',
|
||||||
|
events: [
|
||||||
|
{ key: 'outbound.down', label: 'eventOutboundDown', settingKey: '' },
|
||||||
|
{ key: 'outbound.up', label: 'eventOutboundUp', settingKey: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ThunderboltOutlined />,
|
||||||
|
title: 'eventGroupXray',
|
||||||
|
events: [
|
||||||
|
{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <DesktopOutlined />,
|
||||||
|
title: 'eventGroupNode',
|
||||||
|
events: [
|
||||||
|
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||||
|
{ key: 'node.up', label: 'eventNodeUp', settingKey: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <DashboardOutlined />,
|
||||||
|
title: 'eventGroupSystem',
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
key: 'cpu.high',
|
||||||
|
label: 'eventCPUHigh',
|
||||||
|
settingKey: 'tgCpu',
|
||||||
|
extra: ({ value, onChange }) => (
|
||||||
|
<InputNumber size="small" min={0} max={100} value={value} onChange={onChange} style={{ width: 80 }} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <SafetyOutlined />,
|
||||||
|
title: 'eventGroupSecurity',
|
||||||
|
events: [
|
||||||
|
{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
allSetting: AllSetting;
|
||||||
|
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TelegramNotifications({ allSetting, updateSetting }: Props) {
|
||||||
|
const events = allSetting.tgEnabledEvents || '';
|
||||||
|
const selected = events ? events.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
||||||
|
|
||||||
|
function toggle(key: string) {
|
||||||
|
const next = selected.includes(key)
|
||||||
|
? selected.filter((e) => e !== key)
|
||||||
|
: [...selected, key];
|
||||||
|
updateSetting({ tgEnabledEvents: next.join(',') });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAll(keys: string[]) {
|
||||||
|
const allSelected = keys.every((v) => selected.includes(v));
|
||||||
|
const next = allSelected
|
||||||
|
? selected.filter((v) => !keys.includes(v))
|
||||||
|
: [...new Set([...selected, ...keys])];
|
||||||
|
updateSetting({ tgEnabledEvents: next.join(',') });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationLayout>
|
||||||
|
{GROUPS.map((group, i) => (
|
||||||
|
<NotificationGroup
|
||||||
|
key={i}
|
||||||
|
config={group}
|
||||||
|
selected={selected}
|
||||||
|
onToggle={toggle}
|
||||||
|
onToggleAll={toggleAll}
|
||||||
|
allSetting={allSetting}
|
||||||
|
updateSetting={updateSetting}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</NotificationLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export type { NotificationEventConfig, NotificationGroupConfig } from './types';
|
||||||
|
export { NotificationLayout } from './NotificationLayout';
|
||||||
|
export { NotificationCard } from './NotificationCard';
|
||||||
|
export { NotificationHeader } from './NotificationHeader';
|
||||||
|
export { NotificationEvent } from './NotificationEvent';
|
||||||
|
export { NotificationGroup } from './NotificationGroup';
|
||||||
|
export { TelegramNotifications } from './TelegramNotifications';
|
||||||
|
export { EmailNotifications } from './EmailNotifications';
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export interface NotificationEventConfig {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
settingKey: string;
|
||||||
|
extra?: (props: { value: number; onChange: (v: number | null) => void }) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationGroupConfig {
|
||||||
|
icon: ReactNode;
|
||||||
|
title: string;
|
||||||
|
events: NotificationEventConfig[];
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { Alert, Button, Input, InputNumber, Select, Space, Switch, Tabs } from '
|
|||||||
import { MailOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons';
|
import { MailOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil } from '@/utils';
|
||||||
import type { AllSetting } from '@/models/setting';
|
import type { AllSetting } from '@/models/setting';
|
||||||
import { SettingListItem, EventBusCheckboxes } from '@/components/ui';
|
import { SettingListItem } from '@/components/ui';
|
||||||
|
import { EmailNotifications } from '@/components/ui/notifications/EmailNotifications';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||||
import { catTabLabel } from './catTabLabel';
|
import { catTabLabel } from './catTabLabel';
|
||||||
|
|
||||||
@@ -122,12 +123,7 @@ export default function EmailTab({ allSetting, updateSetting }: EmailTabProps) {
|
|||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem paddings="small" title={t('pages.settings.smtpEventBusNotify')} description={t('pages.settings.smtpEventBusNotifyDesc')}>
|
<SettingListItem paddings="small" title={t('pages.settings.smtpEventBusNotify')} description={t('pages.settings.smtpEventBusNotifyDesc')}>
|
||||||
<EventBusCheckboxes
|
<EmailNotifications allSetting={allSetting} updateSetting={updateSetting} />
|
||||||
value={allSetting.smtpEnabledEvents}
|
|
||||||
onChange={(v) => updateSetting({ smtpEnabledEvents: v })}
|
|
||||||
extra={{ 'cpu.high': { key: 'smtpCpu', value: allSetting.smtpCpu } }}
|
|
||||||
onExtraChange={(key, v) => updateSetting({ [key]: Number(v) || 0 })}
|
|
||||||
/>
|
|
||||||
</SettingListItem>
|
</SettingListItem>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { BellOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons';
|
|||||||
import { LanguageManager } from '@/utils';
|
import { LanguageManager } from '@/utils';
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil } from '@/utils';
|
||||||
import type { AllSetting } from '@/models/setting';
|
import type { AllSetting } from '@/models/setting';
|
||||||
import { SettingListItem, EventBusCheckboxes } from '@/components/ui';
|
import { SettingListItem } from '@/components/ui';
|
||||||
|
import { TelegramNotifications } from '@/components/ui/notifications/TelegramNotifications';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||||
import { catTabLabel } from './catTabLabel';
|
import { catTabLabel } from './catTabLabel';
|
||||||
|
|
||||||
@@ -245,12 +246,7 @@ export default function TelegramTab({ allSetting, updateSetting }: TelegramTabPr
|
|||||||
</SettingListItem>
|
</SettingListItem>
|
||||||
|
|
||||||
<SettingListItem paddings="small" title={t('pages.settings.tgEventBusNotify')} description={t('pages.settings.tgEventBusNotifyDesc')}>
|
<SettingListItem paddings="small" title={t('pages.settings.tgEventBusNotify')} description={t('pages.settings.tgEventBusNotifyDesc')}>
|
||||||
<EventBusCheckboxes
|
<TelegramNotifications allSetting={allSetting} updateSetting={updateSetting} />
|
||||||
value={allSetting.tgEnabledEvents}
|
|
||||||
onChange={(v) => updateSetting({ tgEnabledEvents: v })}
|
|
||||||
extra={{ 'cpu.high': { key: 'tgCpu', value: allSetting.tgCpu } }}
|
|
||||||
onExtraChange={(key, v) => updateSetting({ [key]: Number(v) || 0 })}
|
|
||||||
/>
|
|
||||||
</SettingListItem>
|
</SettingListItem>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user