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

🎁 refactor(ui/redemption): migrate EditRedemption page to Semi Form & enhance UX

SUMMARY
• Re-implemented `web/src/pages/Redemption/EditRedemption.js` with Semi Form components, removing legacy local-state handling.
• Added `formApiRef` for centralized control; external “Submit” button now triggers `formApi.submitForm()`.
• Replaced `Input/AutoComplete/DatePicker` etc. with `<Form.*>` fields, leveraging built-in validation & accessibility.
• Field validations:
  – `name` (create only), `quota`, `count` → required with localized messages.
• Expiration-time flow:
  – Default value `null` (no more 1970-01-01).
  – When loading data, convert 0 → null, timestamp → Date.
  – On submit, Date → unix seconds; empty → 0.
• Responsive grid layout (`Row/Col`) for tidy alignment.
• Added helpful `showClear` & full-width styling for inputs; quota presets retained.
• Cleaned unused imports & handlers; fixed linter issues.

RESULT
The Redemption form now benefits from higher performance, clearer validation, and a cleaner codebase consistent with Semi Design best practices.
t0ng7u 8 месяцев назад
Родитель
Сommit
d07224e658
2 измененных файлов с 145 добавлено и 128 удалено
  1. 3 1
      web/src/i18n/locales/en.json
  2. 142 127
      web/src/pages/Redemption/EditRedemption.js

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

@@ -1745,5 +1745,7 @@
   "已复制 ${count} 个模型": "Copied ${count} models",
   "复制失败,请手动复制": "Copy failed, please copy manually",
   "快捷设置": "Quick settings",
-  "批量创建时会在名称后自动添加随机后缀": "When creating in batches, a random suffix will be automatically added to the name"
+  "批量创建时会在名称后自动添加随机后缀": "When creating in batches, a random suffix will be automatically added to the name",
+  "额度必须大于0": "Quota must be greater than 0",
+  "生成数量必须大于0": "Generation quantity must be greater than 0"
 }

+ 142 - 127
web/src/pages/Redemption/EditRedemption.js

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
 import { useTranslation } from 'react-i18next';
 import {
   API,
@@ -7,12 +7,10 @@ import {
   showError,
   showSuccess,
   renderQuota,
-  renderQuotaWithPrompt
+  renderQuotaWithPrompt,
 } from '../../helpers';
 import {
-  AutoComplete,
   Button,
-  Input,
   Modal,
   SideSheet,
   Space,
@@ -21,8 +19,9 @@ import {
   Card,
   Tag,
   Form,
-  DatePicker,
   Avatar,
+  Row,
+  Col,
 } from '@douyinfe/semi-ui';
 import {
   IconCreditCard,
@@ -37,30 +36,30 @@ const EditRedemption = (props) => {
   const { t } = useTranslation();
   const isEdit = props.editingRedemption.id !== undefined;
   const [loading, setLoading] = useState(isEdit);
+  const formApiRef = useRef(null);
 
-  const originInputs = {
+  const getInitValues = () => ({
     name: '',
     quota: 100000,
     count: 1,
-    expired_time: 0,
-  };
-  const [inputs, setInputs] = useState(originInputs);
-  const { name, quota, count, expired_time } = inputs;
+    expired_time: null,
+  });
 
   const handleCancel = () => {
     props.handleClose();
   };
 
-  const handleInputChange = (name, value) => {
-    setInputs((inputs) => ({ ...inputs, [name]: value }));
-  };
-
   const loadRedemption = async () => {
     setLoading(true);
     let res = await API.get(`/api/redemption/${props.editingRedemption.id}`);
     const { success, message, data } = res.data;
     if (success) {
-      setInputs(data);
+      if (data.expired_time === 0) {
+        data.expired_time = null;
+      } else {
+        data.expired_time = new Date(data.expired_time * 1000);
+      }
+      formApiRef.current?.setValues({ ...getInitValues(), ...data });
     } else {
       showError(message);
     }
@@ -68,28 +67,30 @@ const EditRedemption = (props) => {
   };
 
   useEffect(() => {
-    if (isEdit) {
-      loadRedemption().then(() => {
-        // console.log(inputs);
-      });
-    } else {
-      setInputs(originInputs);
+    if (formApiRef.current) {
+      if (isEdit) {
+        loadRedemption();
+      } else {
+        formApiRef.current.setValues(getInitValues());
+      }
     }
   }, [props.editingRedemption.id]);
 
-  const submit = async () => {
-    let name = inputs.name;
-    if (!isEdit && inputs.name === '') {
-      // set default name
-      name = renderQuota(quota);
+  const submit = async (values) => {
+    let name = values.name;
+    if (!isEdit && values.name === '') {
+
+      name = renderQuota(values.quota);
     }
     setLoading(true);
-    let localInputs = inputs;
-    localInputs.count = parseInt(localInputs.count);
-    localInputs.quota = parseInt(localInputs.quota);
+    let localInputs = { ...values };
+    localInputs.count = parseInt(localInputs.count) || 0;
+    localInputs.quota = parseInt(localInputs.quota) || 0;
     localInputs.name = name;
-    if (localInputs.expired_time === null || localInputs.expired_time === undefined) {
+    if (!localInputs.expired_time) {
       localInputs.expired_time = 0;
+    } else {
+      localInputs.expired_time = Math.floor(localInputs.expired_time.getTime() / 1000);
     }
     let res;
     if (isEdit) {
@@ -110,8 +111,8 @@ const EditRedemption = (props) => {
         props.handleClose();
       } else {
         showSuccess(t('兑换码创建成功!'));
-        setInputs(originInputs);
         props.refresh();
+        formApiRef.current?.setValues(getInitValues());
         props.handleClose();
       }
     } else {
@@ -131,7 +132,7 @@ const EditRedemption = (props) => {
           </div>
         ),
         onOk: () => {
-          downloadTextAsFile(text, `${inputs.name}.txt`);
+          downloadTextAsFile(text, `${localInputs.name}.txt`);
         },
       });
     }
@@ -166,7 +167,7 @@ const EditRedemption = (props) => {
               <Button
                 theme="solid"
                 className="!rounded-full"
-                onClick={submit}
+                onClick={() => formApiRef.current?.submitForm()}
                 icon={<IconSave />}
                 loading={loading}
               >
@@ -188,105 +189,119 @@ const EditRedemption = (props) => {
         onCancel={() => handleCancel()}
       >
         <Spin spinning={loading}>
-          <div className="p-6">
-            <Card className="!rounded-2xl shadow-sm border-0 mb-6">
-              {/* Header: Basic Info */}
-              <div className="flex items-center mb-2">
-                <Avatar size="small" color="blue" className="mr-2 shadow-md">
-                  <IconGift size={16} />
-                </Avatar>
-                <div>
-                  <Text className="text-lg font-medium">{t('基本信息')}</Text>
-                  <div className="text-xs text-gray-600">{t('设置兑换码的基本信息')}</div>
-                </div>
-              </div>
-
-              <div className="space-y-4">
-                <div>
-                  <Text strong className="block mb-2">{t('名称')}</Text>
-                  <Input
-                    placeholder={t('请输入名称')}
-                    onChange={(value) => handleInputChange('name', value)}
-                    value={name}
-                    autoComplete="new-password"
-                    className="!rounded-lg"
-                    showClear
-                    required={!isEdit}
-                  />
-                </div>
-                <div>
-                  <Text strong className="block mb-2">{t('过期时间')}</Text>
-                  <DatePicker
-                    type="dateTime"
-                    placeholder={t('选择过期时间(可选,留空为永久)')}
-                    showClear
-                    value={expired_time ? new Date(expired_time * 1000) : null}
-                    onChange={(value) => {
-                      if (value === null || value === undefined) {
-                        handleInputChange('expired_time', 0);
-                      } else {
-                        const timestamp = Math.floor(value.getTime() / 1000);
-                        handleInputChange('expired_time', timestamp);
-                      }
-                    }}
-                    className="!rounded-lg w-full"
-                  />
-                </div>
-              </div>
-            </Card>
+          <Form
+            initValues={getInitValues()}
+            getFormApi={(api) => formApiRef.current = api}
+            onSubmit={submit}
+          >
+            {({ values }) => (
+              <div className="p-6 space-y-6">
+                <Card className="!rounded-2xl shadow-sm border-0 mb-6">
+                  {/* Header: Basic Info */}
+                  <div className="flex items-center mb-2">
+                    <Avatar size="small" color="blue" className="mr-2 shadow-md">
+                      <IconGift size={16} />
+                    </Avatar>
+                    <div>
+                      <Text className="text-lg font-medium">{t('基本信息')}</Text>
+                      <div className="text-xs text-gray-600">{t('设置兑换码的基本信息')}</div>
+                    </div>
+                  </div>
 
-            <Card className="!rounded-2xl shadow-sm border-0">
-              {/* Header: Quota Settings */}
-              <div className="flex items-center mb-2">
-                <Avatar size="small" color="green" className="mr-2 shadow-md">
-                  <IconCreditCard size={16} />
-                </Avatar>
-                <div>
-                  <Text className="text-lg font-medium">{t('额度设置')}</Text>
-                  <div className="text-xs text-gray-600">{t('设置兑换码的额度和数量')}</div>
-                </div>
-              </div>
+                  <Row gutter={12}>
+                    <Col span={24}>
+                      <Form.Input
+                        field='name'
+                        label={t('名称')}
+                        placeholder={t('请输入名称')}
+                        style={{ width: '100%' }}
+                        rules={isEdit ? [] : [{ required: true, message: t('请输入名称') }]}
+                        showClear
+                      />
+                    </Col>
+                    <Col span={24}>
+                      <Form.DatePicker
+                        field='expired_time'
+                        label={t('过期时间')}
+                        type='dateTime'
+                        placeholder={t('选择过期时间(可选,留空为永久)')}
+                        style={{ width: '100%' }}
+                        showClear
+                      />
+                    </Col>
+                  </Row>
+                </Card>
 
-              <div className="space-y-4">
-                <div>
-                  <div className="flex justify-between mb-2">
-                    <Text strong>{t('额度')}</Text>
-                    <Text type="tertiary">{renderQuotaWithPrompt(quota)}</Text>
+                <Card className="!rounded-2xl shadow-sm border-0">
+                  {/* Header: Quota Settings */}
+                  <div className="flex items-center mb-2">
+                    <Avatar size="small" color="green" className="mr-2 shadow-md">
+                      <IconCreditCard size={16} />
+                    </Avatar>
+                    <div>
+                      <Text className="text-lg font-medium">{t('额度设置')}</Text>
+                      <div className="text-xs text-gray-600">{t('设置兑换码的额度和数量')}</div>
+                    </div>
                   </div>
-                  <AutoComplete
-                    placeholder={t('请输入额度')}
-                    onChange={(value) => handleInputChange('quota', value)}
-                    value={quota}
-                    autoComplete="new-password"
-                    type="number"
-                    className="w-full !rounded-lg"
-                    data={[
-                      { value: 500000, label: '1$' },
-                      { value: 5000000, label: '10$' },
-                      { value: 25000000, label: '50$' },
-                      { value: 50000000, label: '100$' },
-                      { value: 250000000, label: '500$' },
-                      { value: 500000000, label: '1000$' },
-                    ]}
-                  />
-                </div>
 
-                {!isEdit && (
-                  <div>
-                    <Text strong className="block mb-2">{t('生成数量')}</Text>
-                    <Input
-                      placeholder={t('请输入生成数量')}
-                      onChange={(value) => handleInputChange('count', value)}
-                      value={count}
-                      autoComplete="new-password"
-                      type="number"
-                      className="!rounded-lg"
-                    />
-                  </div>
-                )}
+                  <Row gutter={12}>
+                    <Col span={12}>
+                      <Form.AutoComplete
+                        field='quota'
+                        label={t('额度')}
+                        placeholder={t('请输入额度')}
+                        style={{ width: '100%' }}
+                        type='number'
+                        rules={[
+                          { required: true, message: t('请输入额度') },
+                          {
+                            validator: (rule, v) => {
+                              const num = parseInt(v, 10);
+                              return num > 0
+                                ? Promise.resolve()
+                                : Promise.reject(t('额度必须大于0'));
+                            },
+                          },
+                        ]}
+                        extraText={renderQuotaWithPrompt(Number(values.quota) || 0)}
+                        data={[
+                          { value: 500000, label: '1$' },
+                          { value: 5000000, label: '10$' },
+                          { value: 25000000, label: '50$' },
+                          { value: 50000000, label: '100$' },
+                          { value: 250000000, label: '500$' },
+                          { value: 500000000, label: '1000$' },
+                        ]}
+                        showClear
+                      />
+                    </Col>
+                    {!isEdit && (
+                      <Col span={12}>
+                        <Form.InputNumber
+                          field='count'
+                          label={t('生成数量')}
+                          min={1}
+                          rules={[
+                            { required: true, message: t('请输入生成数量') },
+                            {
+                              validator: (rule, v) => {
+                                const num = parseInt(v, 10);
+                                return num > 0
+                                  ? Promise.resolve()
+                                  : Promise.reject(t('生成数量必须大于0'));
+                              },
+                            },
+                          ]}
+                          style={{ width: '100%' }}
+                          showClear
+                        />
+                      </Col>
+                    )}
+                  </Row>
+                </Card>
               </div>
-            </Card>
-          </div>
+            )}
+          </Form>
         </Spin>
       </SideSheet>
     </>