Просмотр исходного кода

📝 refactor: reorganize payment settings into dedicated tab

Restructure payment settings into a separate tab for better organization and user experience. The changes include:

1. Create dedicated Payment components in the Setting directory structure
2. Move payment-related settings from SystemSetting to PaymentSetting
3. Add proper i18n support with useTranslation hook
4. Split payment settings into GeneralPayment and PaymentGateway components
5. Fix internationalization issues in placeholder text
6. Update navigation with CreditCard icon for payment tab

This refactoring improves code maintainability by following the established project pattern of having specialized setting components in their own directories.
t0ng7u 6 месяцев назад
Родитель
Сommit
01ef1fe4e4

+ 88 - 0
web/src/components/settings/PaymentSetting.js

@@ -0,0 +1,88 @@
+import React, { useEffect, useState } from 'react';
+import { Card, Spin } from '@douyinfe/semi-ui';
+import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment.js';
+import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway.js';
+import { API, showError } from '../../helpers';
+import { useTranslation } from 'react-i18next';
+
+const PaymentSetting = () => {
+  const { t } = useTranslation();
+  let [inputs, setInputs] = useState({
+    ServerAddress: '',
+    PayAddress: '',
+    EpayId: '',
+    EpayKey: '',
+    Price: 7.3,
+    MinTopUp: 1,
+    TopupGroupRatio: '',
+    CustomCallbackAddress: '',
+    PayMethods: '',
+  });
+
+  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) => {
+        switch (item.key) {
+          case 'TopupGroupRatio':
+            try {
+              newInputs[item.key] = JSON.stringify(JSON.parse(item.value), null, 2);
+            } catch (error) {
+              console.error('解析TopupGroupRatio出错:', error);
+              newInputs[item.key] = item.value;
+            }
+            break;
+          case 'Price':
+          case 'MinTopUp':
+            newInputs[item.key] = parseFloat(item.value);
+            break;
+          default:
+            if (item.key.endsWith('Enabled')) {
+              newInputs[item.key] = item.value === 'true' ? true : false;
+            } else {
+              newInputs[item.key] = item.value;
+            }
+            break;
+        }
+      });
+
+      setInputs(newInputs);
+    } else {
+      showError(t(message));
+    }
+  };
+
+  async function onRefresh() {
+    try {
+      setLoading(true);
+      await getOptions();
+    } catch (error) {
+      showError(t('刷新失败'));
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  useEffect(() => {
+    onRefresh();
+  }, []);
+
+  return (
+    <>
+      <Spin spinning={loading} size='large'>
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsGeneralPayment options={inputs} refresh={onRefresh} />
+        </Card>
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsPaymentGateway options={inputs} refresh={onRefresh} />
+        </Card>
+      </Spin>
+    </>
+  );
+};
+
+export default PaymentSetting; 

+ 0 - 150
web/src/components/settings/SystemSetting.js

@@ -17,7 +17,6 @@ import {
   removeTrailingSlash,
   showError,
   showSuccess,
-  verifyJSON,
 } from '../../helpers';
 import axios from 'axios';
 
@@ -42,17 +41,9 @@ const SystemSetting = () => {
     SMTPAccount: '',
     SMTPFrom: '',
     SMTPToken: '',
-    ServerAddress: '',
     WorkerUrl: '',
     WorkerValidKey: '',
     WorkerAllowHttpImageRequestEnabled: '',
-    EpayId: '',
-    EpayKey: '',
-    Price: 7.3,
-    MinTopUp: 1,
-    TopupGroupRatio: '',
-    PayAddress: '',
-    CustomCallbackAddress: '',
     Footer: '',
     WeChatAuthEnabled: '',
     WeChatServerAddress: '',
@@ -73,7 +64,6 @@ const SystemSetting = () => {
     LinuxDOOAuthEnabled: '',
     LinuxDOClientId: '',
     LinuxDOClientSecret: '',
-    PayMethods: '',
   });
 
   const [originInputs, setOriginInputs] = useState({});
@@ -200,11 +190,6 @@ const SystemSetting = () => {
     setInputs(values);
   };
 
-  const submitServerAddress = async () => {
-    let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
-    await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]);
-  };
-
   const submitWorker = async () => {
     let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
     const options = [
@@ -220,56 +205,6 @@ const SystemSetting = () => {
     await updateOptions(options);
   };
 
-  const submitPayAddress = async () => {
-    if (inputs.ServerAddress === '') {
-      showError('请先填写服务器地址');
-      return;
-    }
-    if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
-      if (!verifyJSON(inputs.TopupGroupRatio)) {
-        showError('充值分组倍率不是合法的 JSON 字符串');
-        return;
-      }
-    }
-    if (originInputs['PayMethods'] !== inputs.PayMethods) {
-      if (!verifyJSON(inputs.PayMethods)) {
-        showError('充值方式设置不是合法的 JSON 字符串');
-        return;
-      }
-    }
-
-    const options = [
-      { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) },
-    ];
-
-    if (inputs.EpayId !== '') {
-      options.push({ key: 'EpayId', value: inputs.EpayId });
-    }
-    if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
-      options.push({ key: 'EpayKey', value: inputs.EpayKey });
-    }
-    if (inputs.Price !== '') {
-      options.push({ key: 'Price', value: inputs.Price.toString() });
-    }
-    if (inputs.MinTopUp !== '') {
-      options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
-    }
-    if (inputs.CustomCallbackAddress !== '') {
-      options.push({
-        key: 'CustomCallbackAddress',
-        value: inputs.CustomCallbackAddress,
-      });
-    }
-    if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
-      options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
-    }
-    if (originInputs['PayMethods'] !== inputs.PayMethods) {
-      options.push({ key: 'PayMethods', value: inputs.PayMethods });
-    }
-
-    await updateOptions(options);
-  };
-
   const submitSMTP = async () => {
     const options = [];
 
@@ -551,17 +486,6 @@ const SystemSetting = () => {
                 marginTop: '10px',
               }}
             >
-              <Card>
-                <Form.Section text='通用设置'>
-                  <Form.Input
-                    field='ServerAddress'
-                    label='服务器地址'
-                    placeholder='例如:https://yourdomain.com'
-                    style={{ width: '100%' }}
-                  />
-                  <Button onClick={submitServerAddress}>更新服务器地址</Button>
-                </Form.Section>
-              </Card>
               <Card>
                 <Form.Section text='代理设置'>
                   <Text>
@@ -604,80 +528,6 @@ const SystemSetting = () => {
                 </Form.Section>
               </Card>
 
-              <Card>
-                <Form.Section text='支付设置'>
-                  <Text>
-                    (当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
-                  </Text>
-                  <Row
-                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
-                  >
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input
-                        field='PayAddress'
-                        label='支付地址'
-                        placeholder='例如:https://yourdomain.com'
-                      />
-                    </Col>
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input
-                        field='EpayId'
-                        label='易支付商户ID'
-                        placeholder='例如:0001'
-                      />
-                    </Col>
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input
-                        field='EpayKey'
-                        label='易支付商户密钥'
-                        placeholder='敏感信息不会发送到前端显示'
-                        type='password'
-                      />
-                    </Col>
-                  </Row>
-                  <Row
-                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
-                    style={{ marginTop: 16 }}
-                  >
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input
-                        field='CustomCallbackAddress'
-                        label='回调地址'
-                        placeholder='例如:https://yourdomain.com'
-                      />
-                    </Col>
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.InputNumber
-                        field='Price'
-                        precision={2}
-                        label='充值价格(x元/美金)'
-                        placeholder='例如:7,就是7元/美金'
-                      />
-                    </Col>
-                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.InputNumber
-                        field='MinTopUp'
-                        label='最低充值美元数量'
-                        placeholder='例如:2,就是最低充值2$'
-                      />
-                    </Col>
-                  </Row>
-                  <Form.TextArea
-                    field='TopupGroupRatio'
-                    label='充值分组倍率'
-                    placeholder='为一个 JSON 文本,键为组名称,值为倍率'
-                    autosize
-                  />
-                  <Form.TextArea
-                    field='PayMethods'
-                    label='充值方式设置'
-                    placeholder='为一个 JSON 文本'
-                    autosize
-                  />
-                  <Button onClick={submitPayAddress}>更新支付设置</Button>
-                </Form.Section>
-              </Card>
-
               <Card>
                 <Form.Section text='配置登录注册'>
                   <Row

+ 12 - 1
web/src/i18n/locales/en.json

@@ -1689,5 +1689,16 @@
   "请先选择同步渠道": "Please select the synchronization channel first",
   "与本地相同": "Same as local",
   "未找到匹配的模型": "No matching model found",
-  "暴露倍率接口": "Expose ratio API"
+  "暴露倍率接口": "Expose ratio API",
+  "支付设置": "Payment Settings",
+  "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Currently only supports Epay interface, the default callback address is the server address above!)",
+  "支付地址": "Payment address",
+  "易支付商户ID": "Epay merchant ID",
+  "易支付商户密钥": "Epay merchant key",
+  "回调地址": "Callback address",
+  "充值价格(x元/美金)": "Recharge price (x yuan/dollar)",
+  "最低充值美元数量": "Minimum recharge dollar amount",
+  "充值分组倍率": "Recharge group ratio",
+  "充值方式设置": "Recharge method settings",
+  "更新支付设置": "Update payment settings"
 }

+ 74 - 0
web/src/pages/Setting/Payment/SettingsGeneralPayment.js

@@ -0,0 +1,74 @@
+import React, { useEffect, useState, useRef } from 'react';
+import {
+  Button,
+  Form,
+  Spin,
+} from '@douyinfe/semi-ui';
+import {
+  API,
+  removeTrailingSlash,
+  showError,
+  showSuccess,
+} from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+export default function SettingsGeneralPayment(props) {
+  const { t } = useTranslation();
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    ServerAddress: '',
+  });
+  const formApiRef = useRef(null);
+
+  useEffect(() => {
+    if (props.options && formApiRef.current) {
+      const currentInputs = { ServerAddress: props.options.ServerAddress || '' };
+      setInputs(currentInputs);
+      formApiRef.current.setValues(currentInputs);
+    }
+  }, [props.options]);
+
+  const handleFormChange = (values) => {
+    setInputs(values);
+  };
+
+  const submitServerAddress = async () => {
+    setLoading(true);
+    try {
+      let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
+      const res = await API.put('/api/option/', {
+        key: 'ServerAddress',
+        value: ServerAddress,
+      });
+      if (res.data.success) {
+        showSuccess(t('更新成功'));
+        props.refresh && props.refresh();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (error) {
+      showError(t('更新失败'));
+    }
+    setLoading(false);
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <Form
+        initValues={inputs}
+        onValueChange={handleFormChange}
+        getFormApi={(api) => (formApiRef.current = api)}
+      >
+        <Form.Section text={t('通用设置')}>
+          <Form.Input
+            field='ServerAddress'
+            label={t('服务器地址')}
+            placeholder={'https://yourdomain.com'}
+            style={{ width: '100%' }}
+          />
+          <Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
+        </Form.Section>
+      </Form>
+    </Spin>
+  );
+} 

+ 218 - 0
web/src/pages/Setting/Payment/SettingsPaymentGateway.js

@@ -0,0 +1,218 @@
+import React, { useEffect, useState, useRef } from 'react';
+import {
+  Button,
+  Form,
+  Row,
+  Col,
+  Typography,
+  Spin,
+} from '@douyinfe/semi-ui';
+const { Text } = Typography;
+import {
+  API,
+  removeTrailingSlash,
+  showError,
+  showSuccess,
+  verifyJSON,
+} from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+export default function SettingsPaymentGateway(props) {
+  const { t } = useTranslation();
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    PayAddress: '',
+    EpayId: '',
+    EpayKey: '',
+    Price: 7.3,
+    MinTopUp: 1,
+    TopupGroupRatio: '',
+    CustomCallbackAddress: '',
+    PayMethods: '',
+  });
+  const [originInputs, setOriginInputs] = useState({});
+  const formApiRef = useRef(null);
+
+  useEffect(() => {
+    if (props.options && formApiRef.current) {
+      const currentInputs = {
+        PayAddress: props.options.PayAddress || '',
+        EpayId: props.options.EpayId || '',
+        EpayKey: props.options.EpayKey || '',
+        Price: props.options.Price !== undefined ? parseFloat(props.options.Price) : 7.3,
+        MinTopUp: props.options.MinTopUp !== undefined ? parseFloat(props.options.MinTopUp) : 1,
+        TopupGroupRatio: props.options.TopupGroupRatio || '',
+        CustomCallbackAddress: props.options.CustomCallbackAddress || '',
+        PayMethods: props.options.PayMethods || '',
+      };
+      setInputs(currentInputs);
+      setOriginInputs({ ...currentInputs });
+      formApiRef.current.setValues(currentInputs);
+    }
+  }, [props.options]);
+
+  const handleFormChange = (values) => {
+    setInputs(values);
+  };
+
+  const submitPayAddress = async () => {
+    if (props.options.ServerAddress === '') {
+      showError(t('请先填写服务器地址'));
+      return;
+    }
+
+    if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
+      if (!verifyJSON(inputs.TopupGroupRatio)) {
+        showError(t('充值分组倍率不是合法的 JSON 字符串'));
+        return;
+      }
+    }
+
+    if (originInputs['PayMethods'] !== inputs.PayMethods) {
+      if (!verifyJSON(inputs.PayMethods)) {
+        showError(t('充值方式设置不是合法的 JSON 字符串'));
+        return;
+      }
+    }
+
+    setLoading(true);
+    try {
+      const options = [
+        { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) },
+      ];
+
+      if (inputs.EpayId !== '') {
+        options.push({ key: 'EpayId', value: inputs.EpayId });
+      }
+      if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
+        options.push({ key: 'EpayKey', value: inputs.EpayKey });
+      }
+      if (inputs.Price !== '') {
+        options.push({ key: 'Price', value: inputs.Price.toString() });
+      }
+      if (inputs.MinTopUp !== '') {
+        options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
+      }
+      if (inputs.CustomCallbackAddress !== '') {
+        options.push({
+          key: 'CustomCallbackAddress',
+          value: inputs.CustomCallbackAddress,
+        });
+      }
+      if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
+        options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
+      }
+      if (originInputs['PayMethods'] !== inputs.PayMethods) {
+        options.push({ key: 'PayMethods', value: inputs.PayMethods });
+      }
+
+      // 发送请求
+      const requestQueue = options.map(opt =>
+        API.put('/api/option/', {
+          key: opt.key,
+          value: opt.value,
+        })
+      );
+
+      const results = await Promise.all(requestQueue);
+
+      // 检查所有请求是否成功
+      const errorResults = results.filter(res => !res.data.success);
+      if (errorResults.length > 0) {
+        errorResults.forEach(res => {
+          showError(res.data.message);
+        });
+      } else {
+        showSuccess(t('更新成功'));
+        // 更新本地存储的原始值
+        setOriginInputs({ ...inputs });
+        props.refresh && props.refresh();
+      }
+    } catch (error) {
+      showError(t('更新失败'));
+    }
+    setLoading(false);
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <Form
+        initValues={inputs}
+        onValueChange={handleFormChange}
+        getFormApi={(api) => (formApiRef.current = api)}
+      >
+        <Form.Section text={t('支付设置')}>
+          <Text>
+            {t('(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)')}
+          </Text>
+          <Row
+            gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+          >
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Input
+                field='PayAddress'
+                label={t('支付地址')}
+                placeholder={t('例如:https://yourdomain.com')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Input
+                field='EpayId'
+                label={t('易支付商户ID')}
+                placeholder={t('例如:0001')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Input
+                field='EpayKey'
+                label={t('易支付商户密钥')}
+                placeholder={t('敏感信息不会发送到前端显示')}
+                type='password'
+              />
+            </Col>
+          </Row>
+          <Row
+            gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+            style={{ marginTop: 16 }}
+          >
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Input
+                field='CustomCallbackAddress'
+                label={t('回调地址')}
+                placeholder={t('例如:https://yourdomain.com')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.InputNumber
+                field='Price'
+                precision={2}
+                label={t('充值价格(x元/美金)')}
+                placeholder={t('例如:7,就是7元/美金')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.InputNumber
+                field='MinTopUp'
+                label={t('最低充值美元数量')}
+                placeholder={t('例如:2,就是最低充值2$')}
+              />
+            </Col>
+          </Row>
+          <Form.TextArea
+            field='TopupGroupRatio'
+            label={t('充值分组倍率')}
+            placeholder={t('为一个 JSON 文本,键为组名称,值为倍率')}
+            autosize
+          />
+          <Form.TextArea
+            field='PayMethods'
+            label={t('充值方式设置')}
+            placeholder={t('为一个 JSON 文本')}
+            autosize
+          />
+          <Button onClick={submitPayAddress}>{t('更新支付设置')}</Button>
+        </Form.Section>
+      </Form>
+    </Spin>
+  );
+} 

+ 13 - 1
web/src/pages/Setting/index.js

@@ -11,7 +11,8 @@ import {
   MoreHorizontal,
   LayoutDashboard,
   MessageSquare,
-  Palette
+  Palette,
+  CreditCard
 } from 'lucide-react';
 
 import SystemSetting from '../../components/settings/SystemSetting.js';
@@ -24,6 +25,7 @@ import DashboardSetting from '../../components/settings/DashboardSetting.js';
 import RatioSetting from '../../components/settings/RatioSetting.js';
 import ChatsSetting from '../../components/settings/ChatsSetting.js';
 import DrawingSetting from '../../components/settings/DrawingSetting.js';
+import PaymentSetting from '../../components/settings/PaymentSetting.js';
 
 const Setting = () => {
   const { t } = useTranslation();
@@ -63,6 +65,16 @@ const Setting = () => {
       content: <DrawingSetting />,
       itemKey: 'drawing',
     });
+    panes.push({
+      tab: (
+        <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
+          <CreditCard size={18} />
+          {t('支付设置')}
+        </span>
+      ),
+      content: <PaymentSetting />,
+      itemKey: 'payment',
+    });
     panes.push({
       tab: (
         <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>