Bläddra i källkod

refactor: make operation settings separated from system settings

JustSong 2 år sedan
förälder
incheckning
75545a1f47
3 ändrade filer med 292 tillägg och 191 borttagningar
  1. 282 0
      web/src/components/OperationSetting.js
  2. 1 191
      web/src/components/SystemSetting.js
  3. 9 0
      web/src/pages/Setting/index.js

+ 282 - 0
web/src/components/OperationSetting.js

@@ -0,0 +1,282 @@
+import React, { useEffect, useState } from 'react';
+import { Divider, Form, Grid, Header } from 'semantic-ui-react';
+import { API, showError, verifyJSON } from '../helpers';
+
+const OperationSetting = () => {
+  let [inputs, setInputs] = useState({
+    QuotaForNewUser: 0,
+    QuotaForInviter: 0,
+    QuotaForInvitee: 0,
+    QuotaRemindThreshold: 0,
+    PreConsumedQuota: 0,
+    ModelRatio: '',
+    GroupRatio: '',
+    TopUpLink: '',
+    ChatLink: '',
+    AutomaticDisableChannelEnabled: '',
+    ChannelDisableThreshold: 0,
+    LogConsumeEnabled: ''
+  });
+  const [originInputs, setOriginInputs] = useState({});
+  let [loading, setLoading] = useState(false);
+
+  const getOptions = async () => {
+    const res = await API.get('/api/option/');
+    const { success, message, data } = res.data;
+    if (success) {
+      let newInputs = {};
+      data.forEach((item) => {
+        if (item.key === 'ModelRatio' || item.key === 'GroupRatio') {
+          item.value = JSON.stringify(JSON.parse(item.value), null, 2);
+        }
+        newInputs[item.key] = item.value;
+      });
+      setInputs(newInputs);
+      setOriginInputs(newInputs);
+    } else {
+      showError(message);
+    }
+  };
+
+  useEffect(() => {
+    getOptions().then();
+  }, []);
+
+  const updateOption = async (key, value) => {
+    setLoading(true);
+    if (key.endsWith('Enabled')) {
+      value = inputs[key] === 'true' ? 'false' : 'true';
+    }
+    const res = await API.put('/api/option/', {
+      key,
+      value
+    });
+    const { success, message } = res.data;
+    if (success) {
+      setInputs((inputs) => ({ ...inputs, [key]: value }));
+    } else {
+      showError(message);
+    }
+    setLoading(false);
+  };
+
+  const handleInputChange = async (e, { name, value }) => {
+    if (name.endsWith('Enabled')) {
+      await updateOption(name, value);
+    } else {
+      setInputs((inputs) => ({ ...inputs, [name]: value }));
+    }
+  };
+
+  const submitConfig = async (group) => {
+    switch (group) {
+      case 'monitor':
+        if (originInputs['AutomaticDisableChannelEnabled'] !== inputs.AutomaticDisableChannelEnabled) {
+          await updateOption('AutomaticDisableChannelEnabled', inputs.AutomaticDisableChannelEnabled);
+        }
+        if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
+          await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
+        }
+        if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
+          await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
+        }
+        break;
+      case 'ratio':
+        if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
+          if (!verifyJSON(inputs.ModelRatio)) {
+            showError('模型倍率不是合法的 JSON 字符串');
+            return;
+          }
+          await updateOption('ModelRatio', inputs.ModelRatio);
+        }
+        if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
+          if (!verifyJSON(inputs.GroupRatio)) {
+            showError('分组倍率不是合法的 JSON 字符串');
+            return;
+          }
+          await updateOption('GroupRatio', inputs.GroupRatio);
+        }
+        break;
+      case 'quota':
+        if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
+          await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
+        }
+        if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
+          await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
+        }
+        if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
+          await updateOption('QuotaForInviter', inputs.QuotaForInviter);
+        }
+        if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
+          await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
+        }
+        break;
+      case 'general':
+        if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
+          await updateOption('TopUpLink', inputs.TopUpLink);
+        }
+        if (originInputs['ChatLink'] !== inputs.ChatLink) {
+          await updateOption('ChatLink', inputs.ChatLink);
+        }
+        break;
+    }
+  };
+
+  return (
+    <Grid columns={1}>
+      <Grid.Column>
+        <Form loading={loading}>
+          <Header as='h3'>
+            通用设置
+          </Header>
+          <Form.Group widths={2}>
+            <Form.Input
+              label='充值链接'
+              name='TopUpLink'
+              onChange={handleInputChange}
+              autoComplete='new-password'
+              value={inputs.TopUpLink}
+              type='link'
+              placeholder='例如发卡网站的购买链接'
+            />
+            <Form.Input
+              label='聊天页面链接'
+              name='ChatLink'
+              onChange={handleInputChange}
+              autoComplete='new-password'
+              value={inputs.ChatLink}
+              type='link'
+              placeholder='例如 ChatGPT Next Web 的部署地址'
+            />
+          </Form.Group>
+          <Form.Button onClick={() => {
+            submitConfig('general').then();
+          }}>保存通用设置</Form.Button>
+          <Divider />
+          <Header as='h3'>
+            监控设置
+          </Header>
+          <Form.Group widths={3}>
+            <Form.Input
+              label='最长响应时间'
+              name='ChannelDisableThreshold'
+              onChange={handleInputChange}
+              autoComplete='new-password'
+              value={inputs.ChannelDisableThreshold}
+              type='number'
+              min='0'
+              placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
+            />
+            <Form.Input
+              label='额度提醒阈值'
+              name='QuotaRemindThreshold'
+              onChange={handleInputChange}
+              autoComplete='new-password'
+              value={inputs.QuotaRemindThreshold}
+              type='number'
+              min='0'
+              placeholder='低于此额度时将发送邮件提醒用户'
+            />
+          </Form.Group>
+          <Form.Group inline>
+            <Form.Checkbox
+              checked={inputs.AutomaticDisableChannelEnabled === 'true'}
+              label='失败时自动禁用通道'
+              name='AutomaticDisableChannelEnabled'
+              onChange={handleInputChange}
+            />
+          </Form.Group>
+          <Form.Button onClick={() => {
+            submitConfig('monitor').then();
+          }}>保存监控设置</Form.Button>
+          <Divider />
+          <Header as='h3'>
+            额度设置
+          </Header>
+          <Form.Group widths={4}>
+            <Form.Input
+              label='新用户初始额度'
+              name='QuotaForNewUser'
+              onChange={handleInputChange}
+              autoComplete='new-password'
+              value={inputs.QuotaForNewUser}
+              type='number'
+              min='0'
+              placeholder='例如:100'
+            />
+            <Form.Input
+              label='请求预扣费额度'
+              name='PreConsumedQuota'
+              onChange={handleInputChange}
+              autoComplete='new-password'
+              value={inputs.PreConsumedQuota}
+              type='number'
+              min='0'
+              placeholder='请求结束后多退少补'
+            />
+            <Form.Input
+              label='邀请新用户奖励额度'
+              name='QuotaForInviter'
+              onChange={handleInputChange}
+              autoComplete='new-password'
+              value={inputs.QuotaForInviter}
+              type='number'
+              min='0'
+              placeholder='例如:2000'
+            />
+            <Form.Input
+              label='新用户使用邀请码奖励额度'
+              name='QuotaForInvitee'
+              onChange={handleInputChange}
+              autoComplete='new-password'
+              value={inputs.QuotaForInvitee}
+              type='number'
+              min='0'
+              placeholder='例如:1000'
+            />
+          </Form.Group>
+          <Form.Button onClick={() => {
+            submitConfig('quota').then();
+          }}>保存额度设置</Form.Button>
+          <Divider />
+          <Header as='h3'>
+            倍率设置
+          </Header>
+          <Form.Group widths='equal'>
+            <Form.TextArea
+              label='模型倍率'
+              name='ModelRatio'
+              onChange={handleInputChange}
+              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
+              autoComplete='new-password'
+              value={inputs.ModelRatio}
+              placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
+            />
+          </Form.Group>
+          <Form.Group widths='equal'>
+            <Form.TextArea
+              label='分组倍率'
+              name='GroupRatio'
+              onChange={handleInputChange}
+              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
+              autoComplete='new-password'
+              value={inputs.GroupRatio}
+              placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
+            />
+          </Form.Group>
+          <Form.Checkbox
+            checked={inputs.LogConsumeEnabled === 'true'}
+            label='启用额度消费日志记录'
+            name='LogConsumeEnabled'
+            onChange={handleInputChange}
+          />
+          <Form.Button onClick={() => {
+            submitConfig('ratio').then();
+          }}>保存倍率设置</Form.Button>
+        </Form>
+      </Grid.Column>
+    </Grid>
+  );
+};
+
+export default OperationSetting;

+ 1 - 191
web/src/components/SystemSetting.js

@@ -26,18 +26,6 @@ const SystemSetting = () => {
     TurnstileSiteKey: '',
     TurnstileSecretKey: '',
     RegisterEnabled: '',
-    QuotaForNewUser: 0,
-    QuotaForInviter: 0,
-    QuotaForInvitee: 0,
-    QuotaRemindThreshold: 0,
-    PreConsumedQuota: 0,
-    ModelRatio: '',
-    GroupRatio: '',
-    TopUpLink: '',
-    ChatLink: '',
-    AutomaticDisableChannelEnabled: '',
-    ChannelDisableThreshold: 0,
-    LogConsumeEnabled: ''
   });
   const [originInputs, setOriginInputs] = useState({});
   let [loading, setLoading] = useState(false);
@@ -71,8 +59,6 @@ const SystemSetting = () => {
       case 'WeChatAuthEnabled':
       case 'TurnstileCheckEnabled':
       case 'RegisterEnabled':
-      case 'AutomaticDisableChannelEnabled':
-      case 'LogConsumeEnabled':
         value = inputs[key] === 'true' ? 'false' : 'true';
         break;
       default:
@@ -102,16 +88,7 @@ const SystemSetting = () => {
       name === 'WeChatServerToken' ||
       name === 'WeChatAccountQRCodeImageURL' ||
       name === 'TurnstileSiteKey' ||
-      name === 'TurnstileSecretKey' ||
-      name === 'QuotaForNewUser' ||
-      name === 'QuotaForInviter' ||
-      name === 'QuotaForInvitee' ||
-      name === 'QuotaRemindThreshold' ||
-      name === 'PreConsumedQuota' ||
-      name === 'ModelRatio' ||
-      name === 'GroupRatio' ||
-      name === 'TopUpLink' ||
-      name === 'ChatLink'
+      name === 'TurnstileSecretKey'
     ) {
       setInputs((inputs) => ({ ...inputs, [name]: value }));
     } else {
@@ -124,44 +101,6 @@ const SystemSetting = () => {
     await updateOption('ServerAddress', ServerAddress);
   };
 
-  const submitOperationConfig = async () => {
-    if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
-      await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
-    }
-    if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
-      await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
-    }
-    if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
-      await updateOption('QuotaForInviter', inputs.QuotaForInviter);
-    }
-    if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
-      await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
-    }
-    if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
-      await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
-    }
-    if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
-      if (!verifyJSON(inputs.ModelRatio)) {
-        showError('模型倍率不是合法的 JSON 字符串');
-        return;
-      }
-      await updateOption('ModelRatio', inputs.ModelRatio);
-    }
-    if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
-      if (!verifyJSON(inputs.GroupRatio)) {
-        showError('分组倍率不是合法的 JSON 字符串');
-        return;
-      }
-      await updateOption('GroupRatio', inputs.GroupRatio);
-    }
-    if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
-      await updateOption('TopUpLink', inputs.TopUpLink);
-    }
-    if (originInputs['ChatLink'] !== inputs.ChatLink) {
-      await updateOption('ChatLink', inputs.ChatLink);
-    }
-  };
-
   const submitSMTP = async () => {
     if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
       await updateOption('SMTPServer', inputs.SMTPServer);
@@ -300,135 +239,6 @@ const SystemSetting = () => {
             />
           </Form.Group>
           <Divider />
-          <Header as='h3'>
-            运营设置
-          </Header>
-          <Form.Group widths={4}>
-            <Form.Input
-              label='新用户初始配额'
-              name='QuotaForNewUser'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaForNewUser}
-              type='number'
-              min='0'
-              placeholder='例如:100'
-            />
-            <Form.Input
-              label='充值链接'
-              name='TopUpLink'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.TopUpLink}
-              type='link'
-              placeholder='例如发卡网站的购买链接'
-            />
-            <Form.Input
-              label='额度提醒阈值'
-              name='QuotaRemindThreshold'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaRemindThreshold}
-              type='number'
-              min='0'
-              placeholder='低于此额度时将发送邮件提醒用户'
-            />
-            <Form.Input
-              label='请求预扣费额度'
-              name='PreConsumedQuota'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.PreConsumedQuota}
-              type='number'
-              min='0'
-              placeholder='请求结束后多退少补'
-            />
-          </Form.Group>
-          <Form.Group widths={4}>
-            <Form.Input
-              label='邀请新用户奖励配额'
-              name='QuotaForInviter'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaForInviter}
-              type='number'
-              min='0'
-              placeholder='例如:100'
-            />
-            <Form.Input
-              label='新用户使用邀请码奖励配额'
-              name='QuotaForInvitee'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaForInvitee}
-              type='number'
-              min='0'
-              placeholder='例如:100'
-            />
-            <Form.Input
-              label='聊天页面链接'
-              name='ChatLink'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.ChatLink}
-              type='link'
-              placeholder='例如 ChatGPT Next Web 的部署地址'
-            />
-          </Form.Group>
-          <Form.Group widths='equal'>
-            <Form.TextArea
-              label='模型倍率'
-              name='ModelRatio'
-              onChange={handleInputChange}
-              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
-              autoComplete='new-password'
-              value={inputs.ModelRatio}
-              placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
-            />
-          </Form.Group>
-          <Form.Group widths='equal'>
-            <Form.TextArea
-              label='分组倍率'
-              name='GroupRatio'
-              onChange={handleInputChange}
-              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
-              autoComplete='new-password'
-              value={inputs.GroupRatio}
-              placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
-            />
-          </Form.Group>
-          <Form.Checkbox
-            checked={inputs.LogConsumeEnabled === 'true'}
-            label='启用额度消费日志记录'
-            name='LogConsumeEnabled'
-            onChange={handleInputChange}
-          />
-          <Form.Button onClick={submitOperationConfig}>保存运营设置</Form.Button>
-          <Divider />
-          <Header as='h3'>
-            监控设置
-          </Header>
-          <Form.Group widths={3}>
-            <Form.Input
-              label='最长响应时间'
-              name='ChannelDisableThreshold'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.ChannelDisableThreshold}
-              type='number'
-              min='0'
-              placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
-            />
-          </Form.Group>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.AutomaticDisableChannelEnabled === 'true'}
-              label='失败时自动禁用通道'
-              name='AutomaticDisableChannelEnabled'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Divider />
           <Header as='h3'>
             配置 SMTP
             <Header.Subheader>用以支持系统的邮件发送</Header.Subheader>

+ 9 - 0
web/src/pages/Setting/index.js

@@ -4,6 +4,7 @@ import SystemSetting from '../../components/SystemSetting';
 import { isRoot } from '../../helpers';
 import OtherSetting from '../../components/OtherSetting';
 import PersonalSetting from '../../components/PersonalSetting';
+import OperationSetting from '../../components/OperationSetting';
 
 const Setting = () => {
   let panes = [
@@ -18,6 +19,14 @@ const Setting = () => {
   ];
 
   if (isRoot()) {
+    panes.push({
+      menuItem: '运营设置',
+      render: () => (
+        <Tab.Pane attached={false}>
+          <OperationSetting />
+        </Tab.Pane>
+      )
+    });
     panes.push({
       menuItem: '系统设置',
       render: () => (