Przeglądaj źródła

refactor: 运营设置-通用设置

QuentinHsu 1 rok temu
rodzic
commit
83dd62982e

+ 338 - 390
web/src/components/OperationSetting.js

@@ -1,5 +1,7 @@
 import React, { useEffect, useState } from 'react';
 import { Divider, Form, Grid, Header } from 'semantic-ui-react';
+import { Card } from '@douyinfe/semi-ui';
+import GeneralSettings from '../pages/Setting/Operation/GeneralSettings.js';
 import {
   API,
   showError,
@@ -30,8 +32,8 @@ const OperationSetting = () => {
     AutomaticEnableChannelEnabled: '',
     ChannelDisableThreshold: 0,
     LogConsumeEnabled: '',
-    DisplayInCurrencyEnabled: '',
-    DisplayTokenStatEnabled: '',
+    DisplayInCurrencyEnabled: false,
+    DisplayTokenStatEnabled: false,
     CheckSensitiveEnabled: '',
     CheckSensitiveOnPromptEnabled: '',
     CheckSensitiveOnCompletionEnabled: '',
@@ -45,7 +47,7 @@ const OperationSetting = () => {
     DataExportEnabled: '',
     DataExportDefaultTime: 'hour',
     DataExportInterval: 5,
-    DefaultCollapseSidebar: '', // 默认折叠侧边栏
+    DefaultCollapseSidebar: false, // 默认折叠侧边栏
     RetryTimes: 0,
   });
   const [originInputs, setOriginInputs] = useState({});
@@ -72,8 +74,16 @@ const OperationSetting = () => {
         ) {
           item.value = JSON.stringify(JSON.parse(item.value), null, 2);
         }
-        newInputs[item.key] = item.value;
+        if (
+          item.key.endsWith('Enabled') ||
+          ['DefaultCollapseSidebar'].includes(item.key)
+        ) {
+          newInputs[item.key] = item.value === 'true' ? true : false;
+        } else {
+          newInputs[item.key] = item.value;
+        }
       });
+
       setInputs(newInputs);
       setOriginInputs(newInputs);
     } else {
@@ -224,396 +234,334 @@ const OperationSetting = () => {
     showError('日志清理失败:' + message);
   };
   return (
-    <Grid columns={1}>
-      <Grid.Column>
-        <Form loading={loading} inverted={isDark}>
-          <Header as='h3' inverted={isDark}>
-            通用设置
-          </Header>
-          <Form.Group widths={4}>
-            <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.Input
-              label='聊天页面2链接'
-              name='ChatLink2'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.ChatLink2}
-              type='link'
-              placeholder='例如 ChatGPT Web & Midjourney 的部署地址'
-            />
-            <Form.Input
-              label='单位美元额度'
-              name='QuotaPerUnit'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaPerUnit}
-              type='number'
-              step='0.01'
-              placeholder='一单位货币能兑换的额度'
-            />
-            <Form.Input
-              label='失败重试次数'
-              name='RetryTimes'
-              type={'number'}
-              step='1'
-              min='0'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.RetryTimes}
-              placeholder='失败重试次数'
-            />
-          </Form.Group>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.DisplayInCurrencyEnabled === 'true'}
-              label='以货币形式显示额度'
-              name='DisplayInCurrencyEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.DisplayTokenStatEnabled === 'true'}
-              label='Billing 相关 API 显示令牌额度而非用户额度'
-              name='DisplayTokenStatEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.DefaultCollapseSidebar === 'true'}
-              label='默认折叠侧边栏'
-              name='DefaultCollapseSidebar'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              submitConfig('general').then();
-            }}
-          >
-            保存通用设置
-          </Form.Button>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            绘图设置
-          </Header>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.DrawingEnabled === 'true'}
-              label='启用绘图功能'
-              name='DrawingEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.MjNotifyEnabled === 'true'}
-              label='允许回调(会泄露服务器ip地址)'
-              name='MjNotifyEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.MjAccountFilterEnabled === 'true'}
-              label='允许AccountFilter参数'
-              name='MjAccountFilterEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.MjForwardUrlEnabled === 'true'}
-              label='开启之后将上游地址替换为服务器地址'
-              name='MjForwardUrlEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.MjModeClearEnabled === 'true'}
-              label='开启之后会清除用户提示词中的--fast、--relax以及--turbo参数'
-              name='MjModeClearEnabled'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            屏蔽词过滤设置
-          </Header>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.CheckSensitiveEnabled === 'true'}
-              label='启用屏蔽词过滤功能'
-              name='CheckSensitiveEnabled'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.CheckSensitiveOnPromptEnabled === 'true'}
-              label='启用prompt检查'
-              name='CheckSensitiveOnPromptEnabled'
-              onChange={handleInputChange}
-            />
-            {/*<Form.Checkbox*/}
-            {/*  checked={inputs.CheckSensitiveOnCompletionEnabled === 'true'}*/}
-            {/*  label='启用生成内容检查'*/}
-            {/*  name='CheckSensitiveOnCompletionEnabled'*/}
-            {/*  onChange={handleInputChange}*/}
-            {/*/>*/}
-          </Form.Group>
-          {/*<Form.Group inline>*/}
-          {/*  <Form.Checkbox*/}
-          {/*    checked={inputs.StopOnSensitiveEnabled === 'true'}*/}
-          {/*    label='在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词'*/}
-          {/*    name='StopOnSensitiveEnabled'*/}
-          {/*    onChange={handleInputChange}*/}
-          {/*  />*/}
-          {/*</Form.Group>*/}
-          {/*<Form.Group>*/}
-          {/*  <Form.Input*/}
-          {/*    label="流模式下缓存队列,默认不缓存,设置越大检测越准确,但是回复会有卡顿感"*/}
-          {/*    name="StreamCacheTextLength"*/}
-          {/*    onChange={handleInputChange}*/}
-          {/*    value={inputs.StreamCacheQueueLength}*/}
-          {/*    type="number"*/}
-          {/*    min="0"*/}
-          {/*    placeholder="例如:10"*/}
-          {/*  />*/}
-          {/*</Form.Group>*/}
-          <Form.Group widths='equal'>
-            <Form.TextArea
-              label='屏蔽词列表,一行一个屏蔽词,不需要符号分割'
-              name='SensitiveWords'
-              onChange={handleInputChange}
-              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
-              value={inputs.SensitiveWords}
-              placeholder='一行一个屏蔽词'
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              submitConfig('words').then();
-            }}
-          >
-            保存屏蔽词设置
-          </Form.Button>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            日志设置
-          </Header>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.LogConsumeEnabled === 'true'}
-              label='启用额度消费日志记录'
-              name='LogConsumeEnabled'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Form.Group widths={4}>
-            <Form.Input
-              label='目标时间'
-              value={historyTimestamp}
-              type='datetime-local'
-              name='history_timestamp'
-              onChange={(e, { name, value }) => {
-                setHistoryTimestamp(value);
+    <>
+      <Card>
+        {/* 通用设置 */}
+        <GeneralSettings options={inputs} />
+      </Card>
+      <Grid columns={1}>
+        <Grid.Column>
+          <Form loading={loading} inverted={isDark}>
+            <Divider />
+            <Header as='h3' inverted={isDark}>
+              绘图设置
+            </Header>
+            <Form.Group inline>
+              <Form.Checkbox
+                checked={inputs.DrawingEnabled === 'true'}
+                label='启用绘图功能'
+                name='DrawingEnabled'
+                onChange={handleInputChange}
+              />
+              <Form.Checkbox
+                checked={inputs.MjNotifyEnabled === 'true'}
+                label='允许回调(会泄露服务器ip地址)'
+                name='MjNotifyEnabled'
+                onChange={handleInputChange}
+              />
+              <Form.Checkbox
+                checked={inputs.MjAccountFilterEnabled === 'true'}
+                label='允许AccountFilter参数'
+                name='MjAccountFilterEnabled'
+                onChange={handleInputChange}
+              />
+              <Form.Checkbox
+                checked={inputs.MjForwardUrlEnabled === 'true'}
+                label='开启之后将上游地址替换为服务器地址'
+                name='MjForwardUrlEnabled'
+                onChange={handleInputChange}
+              />
+              <Form.Checkbox
+                checked={inputs.MjModeClearEnabled === 'true'}
+                label='开启之后会清除用户提示词中的--fast、--relax以及--turbo参数'
+                name='MjModeClearEnabled'
+                onChange={handleInputChange}
+              />
+            </Form.Group>
+            <Divider />
+            <Header as='h3' inverted={isDark}>
+              屏蔽词过滤设置
+            </Header>
+            <Form.Group inline>
+              <Form.Checkbox
+                checked={inputs.CheckSensitiveEnabled === 'true'}
+                label='启用屏蔽词过滤功能'
+                name='CheckSensitiveEnabled'
+                onChange={handleInputChange}
+              />
+            </Form.Group>
+            <Form.Group inline>
+              <Form.Checkbox
+                checked={inputs.CheckSensitiveOnPromptEnabled === 'true'}
+                label='启用prompt检查'
+                name='CheckSensitiveOnPromptEnabled'
+                onChange={handleInputChange}
+              />
+              {/*<Form.Checkbox*/}
+              {/*  checked={inputs.CheckSensitiveOnCompletionEnabled === 'true'}*/}
+              {/*  label='启用生成内容检查'*/}
+              {/*  name='CheckSensitiveOnCompletionEnabled'*/}
+              {/*  onChange={handleInputChange}*/}
+              {/*/>*/}
+            </Form.Group>
+            {/*<Form.Group inline>*/}
+            {/*  <Form.Checkbox*/}
+            {/*    checked={inputs.StopOnSensitiveEnabled === 'true'}*/}
+            {/*    label='在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词'*/}
+            {/*    name='StopOnSensitiveEnabled'*/}
+            {/*    onChange={handleInputChange}*/}
+            {/*  />*/}
+            {/*</Form.Group>*/}
+            {/*<Form.Group>*/}
+            {/*  <Form.Input*/}
+            {/*    label="流模式下缓存队列,默认不缓存,设置越大检测越准确,但是回复会有卡顿感"*/}
+            {/*    name="StreamCacheTextLength"*/}
+            {/*    onChange={handleInputChange}*/}
+            {/*    value={inputs.StreamCacheQueueLength}*/}
+            {/*    type="number"*/}
+            {/*    min="0"*/}
+            {/*    placeholder="例如:10"*/}
+            {/*  />*/}
+            {/*</Form.Group>*/}
+            <Form.Group widths='equal'>
+              <Form.TextArea
+                label='屏蔽词列表,一行一个屏蔽词,不需要符号分割'
+                name='SensitiveWords'
+                onChange={handleInputChange}
+                style={{
+                  minHeight: 250,
+                  fontFamily: 'JetBrains Mono, Consolas',
+                }}
+                value={inputs.SensitiveWords}
+                placeholder='一行一个屏蔽词'
+              />
+            </Form.Group>
+            <Form.Button
+              onClick={() => {
+                submitConfig('words').then();
               }}
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              deleteHistoryLogs().then();
-            }}
-          >
-            清理历史日志
-          </Form.Button>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            数据看板
-          </Header>
-          <Form.Checkbox
-            checked={inputs.DataExportEnabled === 'true'}
-            label='启用数据看板(实验性)'
-            name='DataExportEnabled'
-            onChange={handleInputChange}
-          />
-          <Form.Group>
-            <Form.Input
-              label='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
-              name='DataExportInterval'
-              type={'number'}
-              step='1'
-              min='1'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.DataExportInterval}
-              placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
-            />
-            <Form.Select
-              label='数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)'
-              options={timeOptions}
-              name='DataExportDefaultTime'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.DataExportDefaultTime}
-              placeholder='数据看板默认时间粒度'
-            />
-          </Form.Group>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            监控设置
-          </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.Button>
+            <Divider />
+            <Header as='h3' inverted={isDark}>
+              日志设置
+            </Header>
+            <Form.Group inline>
+              <Form.Checkbox
+                checked={inputs.LogConsumeEnabled === 'true'}
+                label='启用额度消费日志记录'
+                name='LogConsumeEnabled'
+                onChange={handleInputChange}
+              />
+            </Form.Group>
+            <Form.Group widths={4}>
+              <Form.Input
+                label='目标时间'
+                value={historyTimestamp}
+                type='datetime-local'
+                name='history_timestamp'
+                onChange={(e, { name, value }) => {
+                  setHistoryTimestamp(value);
+                }}
+              />
+            </Form.Group>
+            <Form.Button
+              onClick={() => {
+                deleteHistoryLogs().then();
+              }}
+            >
+              清理历史日志
+            </Form.Button>
+            <Divider />
+            <Header as='h3' inverted={isDark}>
+              数据看板
+            </Header>
             <Form.Checkbox
-              checked={inputs.AutomaticEnableChannelEnabled === 'true'}
-              label='成功时自动启用通道'
-              name='AutomaticEnableChannelEnabled'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              submitConfig('monitor').then();
-            }}
-          >
-            保存监控设置
-          </Form.Button>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            额度设置
-          </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'
+              checked={inputs.DataExportEnabled === 'true'}
+              label='启用数据看板(实验性)'
+              name='DataExportEnabled'
               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' inverted={isDark}>
-            倍率设置
-          </Header>
-          <Form.Group widths='equal'>
-            <Form.TextArea
-              label='模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)'
-              name='ModelPrice'
-              onChange={handleInputChange}
-              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
-              autoComplete='new-password'
-              value={inputs.ModelPrice}
-              placeholder='为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀'
-            />
-          </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.Button
-            onClick={() => {
-              submitConfig('ratio').then();
-            }}
-          >
-            保存倍率设置
-          </Form.Button>
-        </Form>
-      </Grid.Column>
-    </Grid>
+            <Form.Group>
+              <Form.Input
+                label='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
+                name='DataExportInterval'
+                type={'number'}
+                step='1'
+                min='1'
+                onChange={handleInputChange}
+                autoComplete='new-password'
+                value={inputs.DataExportInterval}
+                placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
+              />
+              <Form.Select
+                label='数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)'
+                options={timeOptions}
+                name='DataExportDefaultTime'
+                onChange={handleInputChange}
+                autoComplete='new-password'
+                value={inputs.DataExportDefaultTime}
+                placeholder='数据看板默认时间粒度'
+              />
+            </Form.Group>
+            <Divider />
+            <Header as='h3' inverted={isDark}>
+              监控设置
+            </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.Checkbox
+                checked={inputs.AutomaticEnableChannelEnabled === 'true'}
+                label='成功时自动启用通道'
+                name='AutomaticEnableChannelEnabled'
+                onChange={handleInputChange}
+              />
+            </Form.Group>
+            <Form.Button
+              onClick={() => {
+                submitConfig('monitor').then();
+              }}
+            >
+              保存监控设置
+            </Form.Button>
+            <Divider />
+            <Header as='h3' inverted={isDark}>
+              额度设置
+            </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' inverted={isDark}>
+              倍率设置
+            </Header>
+            <Form.Group widths='equal'>
+              <Form.TextArea
+                label='模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)'
+                name='ModelPrice'
+                onChange={handleInputChange}
+                style={{
+                  minHeight: 250,
+                  fontFamily: 'JetBrains Mono, Consolas',
+                }}
+                autoComplete='new-password'
+                value={inputs.ModelPrice}
+                placeholder='为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀'
+              />
+            </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.Button
+              onClick={() => {
+                submitConfig('ratio').then();
+              }}
+            >
+              保存倍率设置
+            </Form.Button>
+          </Form>
+        </Grid.Column>
+      </Grid>
+    </>
   );
 };
 

+ 25 - 0
web/src/helpers/utils.js

@@ -220,3 +220,28 @@ export function shouldShowPrompt(id) {
 export function setPromptShown(id) {
   localStorage.setItem(`prompt-${id}`, 'true');
 }
+
+/**
+ * 比较两个对象的属性,找出有变化的属性,并返回包含变化属性信息的数组
+ * @param {Object} oldObject - 旧对象
+ * @param {Object} newObject - 新对象
+ * @return {Array} 包含变化属性信息的数组,每个元素是一个对象,包含 key, oldValue 和 newValue
+ */
+export function compareObjects(oldObject, newObject) {
+  const changedProperties = [];
+
+  // 比较两个对象的属性
+  for (const key in oldObject) {
+    if (oldObject.hasOwnProperty(key) && newObject.hasOwnProperty(key)) {
+      if (oldObject[key] !== newObject[key]) {
+        changedProperties.push({
+          key: key,
+          oldValue: oldObject[key],
+          newValue: newObject[key],
+        });
+      }
+    }
+  }
+
+  return changedProperties;
+}

+ 198 - 0
web/src/pages/Setting/Operation/GeneralSettings.js

@@ -0,0 +1,198 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+} from '../../../helpers';
+
+export default function GeneralSettings(props) {
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    TopUpLink: '',
+    ChatLink: '',
+    ChatLink2: '',
+    QuotaPerUnit: '',
+    RetryTimes: '',
+    DisplayInCurrencyEnabled: false,
+    DisplayTokenStatEnabled: false,
+    DefaultCollapseSidebar: false,
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+  function onChange(value, e) {
+    const name = e.target.id;
+    setInputs((inputs) => ({ ...inputs, [name]: value }));
+  }
+  function onSubmit() {
+    const updateArray = compareObjects(inputs, inputsRow);
+    if (!updateArray.length) return showWarning('你似乎并没有修改什么');
+    const requestQueue = updateArray.map((item) => {
+      let value = '';
+      if (typeof inputs[item.key] === 'boolean') {
+        value = String(inputs[item.key]);
+      } else {
+        value = inputs[item.key];
+      }
+      return API.put('/api/option/', {
+        key: item.key,
+        value,
+      });
+    });
+    setLoading(true);
+    Promise.all(requestQueue)
+      .then((res) => {
+        if (requestQueue.length === 1) {
+          if (res.includes(undefined)) return;
+        } else if (requestQueue.length > 1) {
+          if (res.includes(undefined)) return showError('部分更新失败');
+        }
+        showSuccess('更新成功');
+      })
+      .catch(() => {
+        showError('更新失败');
+      })
+      .finally(() => {
+        setLoading(false);
+        setInputsRow(structuredClone(inputs));
+      });
+  }
+
+  useEffect(() => {
+    const currentInputs = {};
+    for (let key in props.options) {
+      if (Object.keys(inputs).includes(key)) {
+        currentInputs[key] = props.options[key];
+      }
+    }
+    setInputs(currentInputs);
+    setInputsRow(structuredClone(currentInputs));
+    refForm.current.setValues(currentInputs);
+  }, [props.options]);
+  return (
+    <>
+      <Spin spinning={loading}>
+        <Form
+          values={inputs}
+          getFormApi={(formAPI) => (refForm.current = formAPI)}
+          style={{ marginBottom: 15 }}
+        >
+          <Form.Section text={'通用设置'}>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Input
+                  field={'TopUpLink'}
+                  label={'充值链接'}
+                  initValue={''}
+                  placeholder={'例如发卡网站的购买链接'}
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Input
+                  field={'ChatLink'}
+                  label={'默认聊天页面链接'}
+                  initValue={''}
+                  placeholder='例如 ChatGPT Next Web 的部署地址'
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Input
+                  field={'ChatLink2'}
+                  label={'聊天页面 2 链接'}
+                  initValue={''}
+                  placeholder='例如 ChatGPT Next Web 的部署地址'
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Input
+                  field={'QuotaPerUnit'}
+                  label={'单位美元额度'}
+                  initValue={''}
+                  placeholder='一单位货币能兑换的额度'
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Input
+                  field={'RetryTimes'}
+                  label={'失败重试次数'}
+                  initValue={''}
+                  placeholder='失败重试次数'
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+            </Row>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Switch
+                  field={'DisplayInCurrencyEnabled'}
+                  label={'以货币形式显示额度'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  defaultChecked={false}
+                  checked={false}
+                  onChange={(value) => {
+                    setInputs({
+                      ...inputs,
+                      DisplayInCurrencyEnabled: value,
+                    });
+                  }}
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'DisplayTokenStatEnabled'}
+                  label={'Billing 相关 API 显示令牌额度而非用户额度'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  defaultChecked={false}
+                  checked={false}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      DisplayTokenStatEnabled: value,
+                    })
+                  }
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'DefaultCollapseSidebar'}
+                  label={'默认折叠侧边栏'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  defaultChecked={false}
+                  checked={false}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      DefaultCollapseSidebar: value,
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+            <Row>
+              <Button size='large' onClick={onSubmit}>
+                保存通用设置
+              </Button>
+            </Row>
+          </Form.Section>
+        </Form>
+      </Spin>
+    </>
+  );
+}