feat: Add user notification settings with quota warning and multiple notification methods
- Implement user notification settings with email and webhook options - Add new user settings for quota warning threshold and notification preferences - Create backend API and database support for user notification configuration - Enhance frontend personal settings with notification configuration UI - Support custom notification email and webhook URL - Add service layer for sending user notifications
This commit is contained in:
@@ -26,6 +26,10 @@ import {
|
||||
Tag,
|
||||
Typography,
|
||||
Collapsible,
|
||||
Select,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
AutoComplete,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
getQuotaPerUnit,
|
||||
@@ -67,14 +71,15 @@ const PersonalSetting = () => {
|
||||
const [transferAmount, setTransferAmount] = useState(0);
|
||||
const [isModelsExpanded, setIsModelsExpanded] = useState(false);
|
||||
const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量
|
||||
const [notificationSettings, setNotificationSettings] = useState({
|
||||
warningType: 'email',
|
||||
warningThreshold: 100000,
|
||||
webhookUrl: '',
|
||||
webhookSecret: '',
|
||||
notificationEmail: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// let user = localStorage.getItem('user');
|
||||
// if (user) {
|
||||
// userDispatch({ type: 'login', payload: user });
|
||||
// }
|
||||
// console.log(localStorage.getItem('user'))
|
||||
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
status = JSON.parse(status);
|
||||
@@ -105,6 +110,19 @@ const PersonalSetting = () => {
|
||||
return () => clearInterval(countdownInterval); // Clean up on unmount
|
||||
}, [disableButton, countdown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userState?.user?.setting) {
|
||||
const settings = JSON.parse(userState.user.setting);
|
||||
setNotificationSettings({
|
||||
warningType: settings.notify_type || 'email',
|
||||
warningThreshold: settings.quota_warning_threshold || 500000,
|
||||
webhookUrl: settings.webhook_url || '',
|
||||
webhookSecret: settings.webhook_secret || '',
|
||||
notificationEmail: settings.notification_email || ''
|
||||
});
|
||||
}
|
||||
}, [userState?.user?.setting]);
|
||||
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({...inputs, [name]: value}));
|
||||
};
|
||||
@@ -300,7 +318,36 @@ const PersonalSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotificationSettingChange = (type, value) => {
|
||||
setNotificationSettings(prev => ({
|
||||
...prev,
|
||||
[type]: value.target ? value.target.value : value // 处理 Radio 事件对象
|
||||
}));
|
||||
};
|
||||
|
||||
const saveNotificationSettings = async () => {
|
||||
try {
|
||||
const res = await API.put('/api/user/setting', {
|
||||
notify_type: notificationSettings.warningType,
|
||||
quota_warning_threshold: notificationSettings.warningThreshold,
|
||||
webhook_url: notificationSettings.webhookUrl,
|
||||
webhook_secret: notificationSettings.webhookSecret,
|
||||
notification_email: notificationSettings.notificationEmail
|
||||
});
|
||||
|
||||
if (res.data.success) {
|
||||
showSuccess(t('通知设置已更新'));
|
||||
await getUserData();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('更新通知设置失败'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div>
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
@@ -526,9 +573,7 @@ const PersonalSetting = () => {
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>{t('微信')}</Typography.Text>
|
||||
<div
|
||||
style={{display: 'flex', justifyContent: 'space-between'}}
|
||||
>
|
||||
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||
<div>
|
||||
<Input
|
||||
value={
|
||||
@@ -541,12 +586,16 @@ const PersonalSetting = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
disabled={
|
||||
(userState.user && userState.user.wechat_id !== '') ||
|
||||
!status.wechat_login
|
||||
}
|
||||
disabled={!status.wechat_login}
|
||||
onClick={() => {
|
||||
setShowWeChatBindModal(true);
|
||||
}}
|
||||
>
|
||||
{status.wechat_login ? t('绑定') : t('未启用')}
|
||||
{userState.user && userState.user.wechat_id !== ''
|
||||
? t('修改绑定')
|
||||
: status.wechat_login
|
||||
? t('绑定')
|
||||
: t('未启用')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -672,18 +721,8 @@ const PersonalSetting = () => {
|
||||
style={{marginTop: '10px'}}
|
||||
/>
|
||||
)}
|
||||
{status.wechat_login && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowWeChatBindModal(true);
|
||||
}}
|
||||
>
|
||||
{t('绑定微信账号')}
|
||||
</Button>
|
||||
)}
|
||||
<Modal
|
||||
onCancel={() => setShowWeChatBindModal(false)}
|
||||
// onOpen={() => setShowWeChatBindModal(true)}
|
||||
visible={showWeChatBindModal}
|
||||
size={'small'}
|
||||
>
|
||||
@@ -707,9 +746,96 @@ const PersonalSetting = () => {
|
||||
</Modal>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{marginTop: 10}}>
|
||||
<Typography.Title heading={6}>{t('通知设置')}</Typography.Title>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text strong>{t('通知方式')}</Typography.Text>
|
||||
<div style={{marginTop: 10}}>
|
||||
<RadioGroup
|
||||
value={notificationSettings.warningType}
|
||||
onChange={value => handleNotificationSettingChange('warningType', value)}
|
||||
>
|
||||
<Radio value="email">{t('邮件通知')}</Radio>
|
||||
<Radio value="webhook">{t('Webhook通知')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
{notificationSettings.warningType === 'webhook' && (
|
||||
<>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text strong>{t('Webhook地址')}</Typography.Text>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Input
|
||||
value={notificationSettings.webhookUrl}
|
||||
onChange={val => handleNotificationSettingChange('webhookUrl', val)}
|
||||
placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
|
||||
{t('系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text strong>{t('接口凭证(可选)')}</Typography.Text>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Input
|
||||
value={notificationSettings.webhookSecret}
|
||||
onChange={val => handleNotificationSettingChange('webhookSecret', val)}
|
||||
placeholder={t('请输入密钥')}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
|
||||
{t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{marginTop: 4, display: 'block'}}>
|
||||
{t('Authorization: Bearer your-secret-key')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{notificationSettings.warningType === 'email' && (
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text strong>{t('通知邮箱')}</Typography.Text>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Input
|
||||
value={notificationSettings.notificationEmail}
|
||||
onChange={val => handleNotificationSettingChange('notificationEmail', val)}
|
||||
placeholder={t('留空则使用账号绑定的邮箱')}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
|
||||
{t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text>
|
||||
<div style={{marginTop: 10}}>
|
||||
<AutoComplete
|
||||
value={notificationSettings.warningThreshold}
|
||||
onChange={val => handleNotificationSettingChange('warningThreshold', val)}
|
||||
style={{width: 200}}
|
||||
placeholder={t('请输入预警额度')}
|
||||
data={[
|
||||
{ value: 100000, label: '0.2$' },
|
||||
{ value: 500000, label: '1$' },
|
||||
{ value: 1000000, label: '5$' },
|
||||
{ value: 5000000, label: '10$' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Typography.Text type="secondary" style={{marginTop: 10, display: 'block'}}>
|
||||
{t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Button type="primary" onClick={saveNotificationSettings}>
|
||||
{t('保存设置')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Modal
|
||||
onCancel={() => setShowEmailBindModal(false)}
|
||||
// onOpen={() => setShowEmailBindModal(true)}
|
||||
onOk={bindEmail}
|
||||
visible={showEmailBindModal}
|
||||
size={'small'}
|
||||
|
||||
@@ -386,7 +386,7 @@ export function renderQuotaWithPrompt(quota, digits) {
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
if (displayInCurrency) {
|
||||
return '|' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
|
||||
return ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user