| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- /*
- Copyright (C) 2025 QuantumNous
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
- For commercial licensing, please contact support@quantumnous.com
- */
- import React, { useRef, useEffect } from 'react';
- import {
- Button,
- Typography,
- Card,
- Avatar,
- Form,
- Radio,
- Toast,
- Tabs,
- TabPane
- } from '@douyinfe/semi-ui';
- import {
- IconMail,
- IconKey,
- IconBell,
- IconLink
- } from '@douyinfe/semi-icons';
- import { ShieldCheck, Bell, DollarSign } from 'lucide-react';
- import { renderQuotaWithPrompt } from '../../../../helpers';
- import CodeViewer from '../../../playground/CodeViewer';
- const NotificationSettings = ({
- t,
- notificationSettings,
- handleNotificationSettingChange,
- saveNotificationSettings
- }) => {
- const formApiRef = useRef(null);
- // 初始化表单值
- useEffect(() => {
- if (formApiRef.current && notificationSettings) {
- formApiRef.current.setValues(notificationSettings);
- }
- }, [notificationSettings]);
- // 处理表单字段变化
- const handleFormChange = (field, value) => {
- handleNotificationSettingChange(field, value);
- };
- // 表单提交
- const handleSubmit = () => {
- if (formApiRef.current) {
- formApiRef.current.validate()
- .then(() => {
- saveNotificationSettings();
- })
- .catch((errors) => {
- console.log('表单验证失败:', errors);
- Toast.error(t('请检查表单填写是否正确'));
- });
- } else {
- saveNotificationSettings();
- }
- };
- return (
- <Card
- className="!rounded-2xl shadow-sm border-0"
- footer={
- <div className="flex justify-end">
- <Button
- type='primary'
- onClick={handleSubmit}
- >
- {t('保存设置')}
- </Button>
- </div>
- }
- >
- {/* 卡片头部 */}
- <div className="flex items-center mb-4">
- <Avatar size="small" color="blue" className="mr-3 shadow-md">
- <Bell size={16} />
- </Avatar>
- <div>
- <Typography.Text className="text-lg font-medium">{t('其他设置')}</Typography.Text>
- <div className="text-xs text-gray-600">{t('通知、价格和隐私相关设置')}</div>
- </div>
- </div>
- <Form
- getFormApi={(api) => (formApiRef.current = api)}
- initValues={notificationSettings}
- onSubmit={handleSubmit}
- >
- {() => (
- <Tabs type="card" defaultActiveKey="notification">
- {/* 通知配置 Tab */}
- <TabPane
- tab={
- <div className="flex items-center">
- <Bell size={16} className="mr-2" />
- {t('通知配置')}
- </div>
- }
- itemKey="notification"
- >
- <div className="py-4">
- <Form.RadioGroup
- field='warningType'
- label={t('通知方式')}
- initValue={notificationSettings.warningType}
- onChange={(value) => handleFormChange('warningType', value)}
- rules={[{ required: true, message: t('请选择通知方式') }]}
- >
- <Radio value="email">{t('邮件通知')}</Radio>
- <Radio value="webhook">{t('Webhook通知')}</Radio>
- </Form.RadioGroup>
- <Form.AutoComplete
- field='warningThreshold'
- label={
- <span>
- {t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
- </span>
- }
- placeholder={t('请输入预警额度')}
- data={[
- { value: 100000, label: '0.2$' },
- { value: 500000, label: '1$' },
- { value: 1000000, label: '5$' },
- { value: 5000000, label: '10$' },
- ]}
- onChange={(val) => handleFormChange('warningThreshold', val)}
- prefix={<IconBell />}
- extraText={t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
- style={{ width: '100%', maxWidth: '300px' }}
- rules={[
- { required: true, message: t('请输入预警阈值') },
- {
- validator: (rule, value) => {
- const numValue = Number(value);
- if (isNaN(numValue) || numValue <= 0) {
- return Promise.reject(t('预警阈值必须为正数'));
- }
- return Promise.resolve();
- }
- }
- ]}
- />
- {/* 邮件通知设置 */}
- {notificationSettings.warningType === 'email' && (
- <Form.Input
- field='notificationEmail'
- label={t('通知邮箱')}
- placeholder={t('留空则使用账号绑定的邮箱')}
- onChange={(val) => handleFormChange('notificationEmail', val)}
- prefix={<IconMail />}
- extraText={t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
- showClear
- />
- )}
- {/* Webhook通知设置 */}
- {notificationSettings.warningType === 'webhook' && (
- <>
- <Form.Input
- field='webhookUrl'
- label={t('Webhook地址')}
- placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
- onChange={(val) => handleFormChange('webhookUrl', val)}
- prefix={<IconLink />}
- extraText={t('只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求')}
- showClear
- rules={[
- {
- required: notificationSettings.warningType === 'webhook',
- message: t('请输入Webhook地址')
- },
- {
- pattern: /^https:\/\/.+/,
- message: t('Webhook地址必须以https://开头')
- }
- ]}
- />
- <Form.Input
- field='webhookSecret'
- label={t('接口凭证')}
- placeholder={t('请输入密钥')}
- onChange={(val) => handleFormChange('webhookSecret', val)}
- prefix={<IconKey />}
- extraText={t('密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性')}
- showClear
- />
- <Form.Slot label={t('Webhook请求结构说明')}>
- <div>
- <div style={{ height: '200px', marginBottom: '12px' }}>
- <CodeViewer
- content={{
- "type": "quota_exceed",
- "title": "额度预警通知",
- "content": "您的额度即将用尽,当前剩余额度为 {{value}}",
- "values": ["$0.99"],
- "timestamp": 1739950503
- }}
- title="webhook"
- language="json"
- />
- </div>
- <div className="text-xs text-gray-500 leading-relaxed">
- <div><strong>type:</strong> {t('通知类型 (quota_exceed: 额度预警)')} </div>
- <div><strong>title:</strong> {t('通知标题')}</div>
- <div><strong>content:</strong> {t('通知内容,支持 {{value}} 变量占位符')}</div>
- <div><strong>values:</strong> {t('按顺序替换content中的变量占位符')}</div>
- <div><strong>timestamp:</strong> {t('Unix时间戳')}</div>
- </div>
- </div>
- </Form.Slot>
- </>
- )}
- </div>
- </TabPane>
- {/* 价格设置 Tab */}
- <TabPane
- tab={
- <div className="flex items-center">
- <DollarSign size={16} className="mr-2" />
- {t('价格设置')}
- </div>
- }
- itemKey="pricing"
- >
- <div className="py-4">
- <Form.Switch
- field='acceptUnsetModelRatioModel'
- label={t('接受未设置价格模型')}
- checkedText={t('开')}
- uncheckedText={t('关')}
- onChange={(value) => handleFormChange('acceptUnsetModelRatioModel', value)}
- extraText={t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
- />
- </div>
- </TabPane>
- {/* 隐私设置 Tab */}
- <TabPane
- tab={
- <div className="flex items-center">
- <ShieldCheck size={16} className="mr-2" />
- {t('隐私设置')}
- </div>
- }
- itemKey="privacy"
- >
- <div className="py-4">
- <Form.Switch
- field='recordIpLog'
- label={t('记录请求与错误日志IP')}
- checkedText={t('开')}
- uncheckedText={t('关')}
- onChange={(value) => handleFormChange('recordIpLog', value)}
- extraText={t('开启后,仅"消费"和"错误"日志将记录您的客户端IP地址')}
- />
- </div>
- </TabPane>
- </Tabs>
- )}
- </Form>
- </Card>
- );
- };
- export default NotificationSettings;
|