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

♻️ refactor(components): migrate all table components to use Form API

- Refactor LogsTable, MjLogsTable, TokensTable, UsersTable, and ChannelsTable to use Semi-UI Form components
- Replace individual input state management with centralized Form API
- Add form validation and consistent form handling across all tables
- Implement auto-search functionality with proper state update timing
- Add reset functionality to clear all search filters
- Improve responsive layout design for better mobile experience
- Remove duplicate form initial values and consolidate form logic
- Remove column visibility feature from ChannelsTable to simplify UI
- Standardize search form structure and styling across all table components
- Fix state update timing issues in search functionality
- Add proper form submission handling with loading states

BREAKING CHANGE: Form state management has been completely rewritten.
All table components now use Form API instead of individual useState hooks.
Column visibility settings for ChannelsTable have been removed.
Apple\Apple 9 месяцев назад
Родитель
Сommit
86354e305e

+ 101 - 262
web/src/components/table/ChannelsTable.js

@@ -25,9 +25,8 @@ import {
   Tag,
   Tooltip,
   Typography,
-  Checkbox,
   Card,
-  Select
+  Form
 } from '@douyinfe/semi-ui';
 import EditChannel from '../../pages/Channel/EditChannel.js';
 import {
@@ -149,108 +148,17 @@ const ChannelsTable = () => {
     }
   };
 
-  // Define column keys for selection
-  const COLUMN_KEYS = {
-    ID: 'id',
-    NAME: 'name',
-    GROUP: 'group',
-    TYPE: 'type',
-    STATUS: 'status',
-    RESPONSE_TIME: 'response_time',
-    BALANCE: 'balance',
-    PRIORITY: 'priority',
-    WEIGHT: 'weight',
-    OPERATE: 'operate',
-  };
-
-  // State for column visibility
-  const [visibleColumns, setVisibleColumns] = useState({});
-  const [showColumnSelector, setShowColumnSelector] = useState(false);
-
-  // Load saved column preferences from localStorage
-  useEffect(() => {
-    const savedColumns = localStorage.getItem('channels-table-columns');
-    if (savedColumns) {
-      try {
-        const parsed = JSON.parse(savedColumns);
-        // Make sure all columns are accounted for
-        const defaults = getDefaultColumnVisibility();
-        const merged = { ...defaults, ...parsed };
-        setVisibleColumns(merged);
-      } catch (e) {
-        console.error('Failed to parse saved column preferences', e);
-        initDefaultColumns();
-      }
-    } else {
-      initDefaultColumns();
-    }
-  }, []);
-
-  // Update table when column visibility changes
-  useEffect(() => {
-    if (Object.keys(visibleColumns).length > 0) {
-      // Save to localStorage
-      localStorage.setItem(
-        'channels-table-columns',
-        JSON.stringify(visibleColumns),
-      );
-    }
-  }, [visibleColumns]);
-
-  // Get default column visibility
-  const getDefaultColumnVisibility = () => {
-    return {
-      [COLUMN_KEYS.ID]: true,
-      [COLUMN_KEYS.NAME]: true,
-      [COLUMN_KEYS.GROUP]: true,
-      [COLUMN_KEYS.TYPE]: true,
-      [COLUMN_KEYS.STATUS]: true,
-      [COLUMN_KEYS.RESPONSE_TIME]: true,
-      [COLUMN_KEYS.BALANCE]: true,
-      [COLUMN_KEYS.PRIORITY]: true,
-      [COLUMN_KEYS.WEIGHT]: true,
-      [COLUMN_KEYS.OPERATE]: true,
-    };
-  };
-
-  // Initialize default column visibility
-  const initDefaultColumns = () => {
-    const defaults = getDefaultColumnVisibility();
-    setVisibleColumns(defaults);
-  };
-
-  // Handle column visibility change
-  const handleColumnVisibilityChange = (columnKey, checked) => {
-    const updatedColumns = { ...visibleColumns, [columnKey]: checked };
-    setVisibleColumns(updatedColumns);
-  };
-
-  // Handle "Select All" checkbox
-  const handleSelectAll = (checked) => {
-    const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
-    const updatedColumns = {};
-
-    allKeys.forEach((key) => {
-      updatedColumns[key] = checked;
-    });
-
-    setVisibleColumns(updatedColumns);
-  };
-
-  // Define all columns with keys
-  const allColumns = [
+  // Define all columns
+  const columns = [
     {
-      key: COLUMN_KEYS.ID,
       title: t('ID'),
       dataIndex: 'id',
     },
     {
-      key: COLUMN_KEYS.NAME,
       title: t('名称'),
       dataIndex: 'name',
     },
     {
-      key: COLUMN_KEYS.GROUP,
       title: t('分组'),
       dataIndex: 'group',
       render: (text, record, index) => (
@@ -269,7 +177,6 @@ const ChannelsTable = () => {
       ),
     },
     {
-      key: COLUMN_KEYS.TYPE,
       title: t('类型'),
       dataIndex: 'type',
       render: (text, record, index) => {
@@ -281,7 +188,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.STATUS,
       title: t('状态'),
       dataIndex: 'status',
       render: (text, record, index) => {
@@ -307,7 +213,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.RESPONSE_TIME,
       title: t('响应时间'),
       dataIndex: 'response_time',
       render: (text, record, index) => (
@@ -315,7 +220,6 @@ const ChannelsTable = () => {
       ),
     },
     {
-      key: COLUMN_KEYS.BALANCE,
       title: t('已用/剩余'),
       dataIndex: 'expired_time',
       render: (text, record, index) => {
@@ -354,7 +258,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.PRIORITY,
       title: t('优先级'),
       dataIndex: 'priority',
       render: (text, record, index) => {
@@ -406,7 +309,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.WEIGHT,
       title: t('权重'),
       dataIndex: 'weight',
       render: (text, record, index) => {
@@ -458,7 +360,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.OPERATE,
       title: '',
       dataIndex: 'operate',
       fixed: 'right',
@@ -631,96 +532,10 @@ const ChannelsTable = () => {
     },
   ];
 
-  // Filter columns based on visibility settings
-  const getVisibleColumns = () => {
-    return allColumns.filter((column) => visibleColumns[column.key]);
-  };
-
-  // Column selector modal
-  const renderColumnSelector = () => {
-    return (
-      <Modal
-        title={t('列设置')}
-        visible={showColumnSelector}
-        onCancel={() => setShowColumnSelector(false)}
-        footer={
-          <div className="flex justify-end">
-            <Button
-              theme="light"
-              onClick={() => initDefaultColumns()}
-              className="!rounded-full"
-            >
-              {t('重置')}
-            </Button>
-            <Button
-              theme="light"
-              onClick={() => setShowColumnSelector(false)}
-              className="!rounded-full"
-            >
-              {t('取消')}
-            </Button>
-            <Button
-              type='primary'
-              onClick={() => setShowColumnSelector(false)}
-              className="!rounded-full"
-            >
-              {t('确定')}
-            </Button>
-          </div>
-        }
-        size="middle"
-        centered={true}
-      >
-        <div style={{ marginBottom: 20 }}>
-          <Checkbox
-            checked={Object.values(visibleColumns).every((v) => v === true)}
-            indeterminate={
-              Object.values(visibleColumns).some((v) => v === true) &&
-              !Object.values(visibleColumns).every((v) => v === true)
-            }
-            onChange={(e) => handleSelectAll(e.target.checked)}
-          >
-            {t('全选')}
-          </Checkbox>
-        </div>
-        <div
-          className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4"
-          style={{ border: '1px solid var(--semi-color-border)' }}
-        >
-          {allColumns.map((column) => {
-            // Skip columns without title
-            if (!column.title) {
-              return null;
-            }
-
-            return (
-              <div
-                key={column.key}
-                className="w-1/2 mb-4 pr-2"
-              >
-                <Checkbox
-                  checked={!!visibleColumns[column.key]}
-                  onChange={(e) =>
-                    handleColumnVisibilityChange(column.key, e.target.checked)
-                  }
-                >
-                  {column.title}
-                </Checkbox>
-              </div>
-            );
-          })}
-        </div>
-      </Modal>
-    );
-  };
-
   const [channels, setChannels] = useState([]);
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
   const [idSort, setIdSort] = useState(false);
-  const [searchKeyword, setSearchKeyword] = useState('');
-  const [searchGroup, setSearchGroup] = useState('');
-  const [searchModel, setSearchModel] = useState('');
   const [searching, setSearching] = useState(false);
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
   const [channelCount, setChannelCount] = useState(pageSize);
@@ -745,6 +560,16 @@ const ChannelsTable = () => {
   const [testQueue, setTestQueue] = useState([]);
   const [isProcessingQueue, setIsProcessingQueue] = useState(false);
 
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
+
+  // Form 初始值
+  const formInitValues = {
+    searchKeyword: '',
+    searchGroup: '',
+    searchModel: '',
+  };
+
   const removeRecord = (record) => {
     let newDataSource = [...channels];
     if (record.id != null) {
@@ -896,15 +721,11 @@ const ChannelsTable = () => {
   };
 
   const refresh = async () => {
+    const { searchKeyword, searchGroup, searchModel } = getFormValues();
     if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
       await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
     } else {
-      await searchChannels(
-        searchKeyword,
-        searchGroup,
-        searchModel,
-        enableTagMode,
-      );
+      await searchChannels(enableTagMode);
     }
   };
 
@@ -1010,12 +831,19 @@ const ChannelsTable = () => {
     }
   };
 
-  const searchChannels = async (
-    searchKeyword,
-    searchGroup,
-    searchModel,
-    enableTagMode,
-  ) => {
+  // 获取表单值的辅助函数,确保所有值都是字符串
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
+    return {
+      searchKeyword: formValues.searchKeyword || '',
+      searchGroup: formValues.searchGroup || '',
+      searchModel: formValues.searchModel || '',
+    };
+  };
+
+  const searchChannels = async (enableTagMode) => {
+    const { searchKeyword, searchGroup, searchModel } = getFormValues();
+
     if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
       await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
       // setActivePage(1);
@@ -1540,71 +1368,83 @@ const ChannelsTable = () => {
           >
             {t('刷新')}
           </Button>
-
-          <Button
-            theme='light'
-            type='tertiary'
-            icon={<IconSetting />}
-            onClick={() => setShowColumnSelector(true)}
-            className="!rounded-full w-full md:w-auto"
-          >
-            {t('列设置')}
-          </Button>
         </div>
 
         <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
-          <div className="relative w-full md:w-64">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')}
-              value={searchKeyword}
-              loading={searching}
-              onChange={(v) => {
-                setSearchKeyword(v.trim());
-              }}
-              className="!rounded-full"
-              showClear
-            />
-          </div>
-          <div className="w-full md:w-48">
-            <Input
-              prefix={<IconFilter />}
-              placeholder={t('模型关键字')}
-              value={searchModel}
+          <Form
+            initValues={formInitValues}
+            getFormApi={(api) => setFormApi(api)}
+            onSubmit={() => searchChannels(enableTagMode)}
+            allowEmpty={true}
+            autoComplete="off"
+            layout="horizontal"
+            trigger="change"
+            stopValidateWithError={false}
+            className="flex flex-col md:flex-row items-center gap-4 w-full"
+          >
+            <div className="relative w-full md:w-64">
+              <Form.Input
+                field="searchKeyword"
+                prefix={<IconSearch />}
+                placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="w-full md:w-48">
+              <Form.Input
+                field="searchModel"
+                prefix={<IconFilter />}
+                placeholder={t('模型关键字')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="w-full md:w-48">
+              <Form.Select
+                field="searchGroup"
+                placeholder={t('选择分组')}
+                optionList={[
+                  { label: t('选择分组'), value: null },
+                  ...groupOptions,
+                ]}
+                className="!rounded-full w-full"
+                showClear
+                pure
+                onChange={() => {
+                  // 延迟执行搜索,让表单值先更新
+                  setTimeout(() => {
+                    searchChannels(enableTagMode);
+                  }, 0);
+                }}
+              />
+            </div>
+            <Button
+              type="primary"
+              htmlType="submit"
               loading={searching}
-              onChange={(v) => {
-                setSearchModel(v.trim());
-              }}
-              className="!rounded-full"
-              showClear
-            />
-          </div>
-          <div className="w-full md:w-48">
-            <Select
-              placeholder={t('选择分组')}
-              optionList={[
-                { label: t('选择分组'), value: null },
-                ...groupOptions,
-              ]}
-              value={searchGroup}
-              onChange={(v) => {
-                setSearchGroup(v);
-                searchChannels(searchKeyword, v, searchModel, enableTagMode);
+              className="!rounded-full w-full md:w-auto"
+            >
+              {t('查询')}
+            </Button>
+            <Button
+              theme='light'
+              onClick={() => {
+                if (formApi) {
+                  formApi.reset();
+                  // 重置后立即查询,使用setTimeout确保表单重置完成
+                  setTimeout(() => {
+                    refresh();
+                  }, 100);
+                }
               }}
-              className="!rounded-full w-full"
-              showClear
-            />
-          </div>
-          <Button
-            type="primary"
-            onClick={() => {
-              searchChannels(searchKeyword, searchGroup, searchModel, enableTagMode);
-            }}
-            loading={searching}
-            className="!rounded-full w-full md:w-auto"
-          >
-            {t('查询')}
-          </Button>
+              className="!rounded-full w-full md:w-auto"
+            >
+              {t('重置')}
+            </Button>
+          </Form>
         </div>
       </div>
     </div>
@@ -1612,7 +1452,6 @@ const ChannelsTable = () => {
 
   return (
     <>
-      {renderColumnSelector()}
       <EditTagModal
         visible={showEditTag}
         tag={editingTag}
@@ -1633,7 +1472,7 @@ const ChannelsTable = () => {
         bordered={false}
       >
         <Table
-          columns={getVisibleColumns()}
+          columns={columns}
           dataSource={pageData}
           scroll={{ x: 'max-content' }}
           pagination={{

+ 7 - 12
web/src/components/table/LogsTable.js

@@ -1204,18 +1204,6 @@ const LogsTable = () => {
               allowEmpty={true}
               autoComplete="off"
               layout="vertical"
-              onValueChange={(values, changedValue) => {
-                // 实时监听日志类型变化
-                if (changedValue.logType !== undefined) {
-                  setLogType(parseInt(changedValue.logType));
-                  // 日志类型变化时自动搜索,不传入logType参数让其从表单获取最新值
-                  setTimeout(() => {
-                    setActivePage(1);
-                    handleEyeClick();
-                    loadLogs(1, pageSize); // 不传入logType参数
-                  }, 100);
-                }
-              }}
               trigger="change"
               stopValidateWithError={false}
             >
@@ -1228,6 +1216,7 @@ const LogsTable = () => {
                       className='w-full'
                       type='dateTimeRange'
                       placeholder={[t('开始时间'), t('结束时间')]}
+                      showClear
                       pure
                     />
                   </div>
@@ -1239,6 +1228,12 @@ const LogsTable = () => {
                     className='!rounded-full'
                     showClear
                     pure
+                    onChange={() => {
+                      // 延迟执行搜索,让表单值先更新
+                      setTimeout(() => {
+                        refresh();
+                      }, 0);
+                    }}
                   >
                     <Form.Select.Option value='0'>{t('全部')}</Form.Select.Option>
                     <Form.Select.Option value='1'>{t('充值')}</Form.Select.Option>

+ 113 - 69
web/src/components/table/MjLogsTable.js

@@ -13,10 +13,9 @@ import {
   Button,
   Card,
   Checkbox,
-  DatePicker,
   Divider,
+  Form,
   ImagePreview,
-  Input,
   Layout,
   Modal,
   Progress,
@@ -571,7 +570,6 @@ const LogsTable = () => {
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
   const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
-  const [logType, setLogType] = useState(0);
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
   const [isModalOpenurl, setIsModalOpenurl] = useState(false);
   const [showBanner, setShowBanner] = useState(false);
@@ -579,22 +577,44 @@ const LogsTable = () => {
   // 定义模态框图片URL的状态和更新函数
   const [modalImageUrl, setModalImageUrl] = useState('');
   let now = new Date();
-  // 初始化start_timestamp为前一天
-  const [inputs, setInputs] = useState({
+
+  // Form 初始值
+  const formInitValues = {
     channel_id: '',
     mj_id: '',
-    start_timestamp: timestamp2string(now.getTime() / 1000 - 2592000),
-    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
-  });
-  const { channel_id, mj_id, start_timestamp, end_timestamp } = inputs;
+    dateRange: [
+      timestamp2string(now.getTime() / 1000 - 2592000),
+      timestamp2string(now.getTime() / 1000 + 3600)
+    ],
+  };
+
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
 
   const [stat, setStat] = useState({
     quota: 0,
     token: 0,
   });
 
-  const handleInputChange = (value, name) => {
-    setInputs((inputs) => ({ ...inputs, [name]: value }));
+  // 获取表单值的辅助函数
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
+
+    // 处理时间范围
+    let start_timestamp = timestamp2string(now.getTime() / 1000 - 2592000);
+    let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
+
+    if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
+      start_timestamp = formValues.dateRange[0];
+      end_timestamp = formValues.dateRange[1];
+    }
+
+    return {
+      channel_id: formValues.channel_id || '',
+      mj_id: formValues.mj_id || '',
+      start_timestamp,
+      end_timestamp,
+    };
   };
 
   const setLogsFormat = (logs) => {
@@ -612,6 +632,7 @@ const LogsTable = () => {
     setLoading(true);
 
     let url = '';
+    const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
     let localStartTimestamp = Date.parse(start_timestamp);
     let localEndTimestamp = Date.parse(end_timestamp);
     if (isAdminUser) {
@@ -674,7 +695,7 @@ const LogsTable = () => {
     const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
     setPageSize(localPageSize);
     loadLogs(0, localPageSize).then();
-  }, [logType]);
+  }, []);
 
   useEffect(() => {
     const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
@@ -789,70 +810,93 @@ const LogsTable = () => {
               <Divider margin="12px" />
 
               {/* 搜索表单区域 */}
-              <div className="flex flex-col gap-4">
-                <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-                  {/* 时间选择器 */}
-                  <div className="col-span-1 lg:col-span-2">
-                    <DatePicker
-                      className="w-full"
-                      value={[start_timestamp, end_timestamp]}
-                      type='dateTimeRange'
-                      onChange={(value) => {
-                        if (Array.isArray(value) && value.length === 2) {
-                          handleInputChange(value[0], 'start_timestamp');
-                          handleInputChange(value[1], 'end_timestamp');
-                        }
-                      }}
-                    />
-                  </div>
-
-                  {/* 任务 ID */}
-                  <Input
-                    prefix={<IconSearch />}
-                    placeholder={t('任务 ID')}
-                    value={mj_id}
-                    onChange={(value) => handleInputChange(value, 'mj_id')}
-                    className="!rounded-full"
-                    showClear
-                  />
-
-                  {/* 渠道 ID - 仅管理员可见 */}
-                  {isAdminUser && (
-                    <Input
+              <Form
+                initValues={formInitValues}
+                getFormApi={(api) => setFormApi(api)}
+                onSubmit={refresh}
+                allowEmpty={true}
+                autoComplete="off"
+                layout="vertical"
+                trigger="change"
+                stopValidateWithError={false}
+              >
+                <div className="flex flex-col gap-4">
+                  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
+                    {/* 时间选择器 */}
+                    <div className="col-span-1 lg:col-span-2">
+                      <Form.DatePicker
+                        field='dateRange'
+                        className="w-full"
+                        type='dateTimeRange'
+                        placeholder={[t('开始时间'), t('结束时间')]}
+                        showClear
+                        pure
+                      />
+                    </div>
+
+                    {/* 任务 ID */}
+                    <Form.Input
+                      field='mj_id'
                       prefix={<IconSearch />}
-                      placeholder={t('渠道 ID')}
-                      value={channel_id}
-                      onChange={(value) => handleInputChange(value, 'channel_id')}
+                      placeholder={t('任务 ID')}
                       className="!rounded-full"
                       showClear
+                      pure
                     />
-                  )}
-                </div>
 
-                {/* 操作按钮区域 */}
-                <div className="flex justify-between items-center pt-2">
-                  <div></div>
-                  <div className="flex gap-2">
-                    <Button
-                      type='primary'
-                      onClick={refresh}
-                      loading={loading}
-                      className="!rounded-full"
-                    >
-                      {t('查询')}
-                    </Button>
-                    <Button
-                      theme='light'
-                      type='tertiary'
-                      icon={<IconSetting />}
-                      onClick={() => setShowColumnSelector(true)}
-                      className="!rounded-full"
-                    >
-                      {t('列设置')}
-                    </Button>
+                    {/* 渠道 ID - 仅管理员可见 */}
+                    {isAdminUser && (
+                      <Form.Input
+                        field='channel_id'
+                        prefix={<IconSearch />}
+                        placeholder={t('渠道 ID')}
+                        className="!rounded-full"
+                        showClear
+                        pure
+                      />
+                    )}
+                  </div>
+
+                  {/* 操作按钮区域 */}
+                  <div className="flex justify-between items-center pt-2">
+                    <div></div>
+                    <div className="flex gap-2">
+                      <Button
+                        type='primary'
+                        htmlType='submit'
+                        loading={loading}
+                        className="!rounded-full"
+                      >
+                        {t('查询')}
+                      </Button>
+                      <Button
+                        theme='light'
+                        onClick={() => {
+                          if (formApi) {
+                            formApi.reset();
+                            // 重置后立即查询,使用setTimeout确保表单重置完成
+                            setTimeout(() => {
+                              refresh();
+                            }, 100);
+                          }
+                        }}
+                        className="!rounded-full"
+                      >
+                        {t('重置')}
+                      </Button>
+                      <Button
+                        theme='light'
+                        type='tertiary'
+                        icon={<IconSetting />}
+                        onClick={() => setShowColumnSelector(true)}
+                        className="!rounded-full"
+                      >
+                        {t('列设置')}
+                      </Button>
+                    </div>
                   </div>
                 </div>
-              </div>
+              </Form>
             </div>
           }
           shadows='always'

+ 79 - 29
web/src/components/table/RedemptionsTable.js

@@ -14,7 +14,7 @@ import {
   Card,
   Divider,
   Dropdown,
-  Input,
+  Form,
   Modal,
   Popover,
   Space,
@@ -223,7 +223,6 @@ const RedemptionsTable = () => {
   const [redemptions, setRedemptions] = useState([]);
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
-  const [searchKeyword, setSearchKeyword] = useState('');
   const [searching, setSearching] = useState(false);
   const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
   const [selectedKeys, setSelectedKeys] = useState([]);
@@ -233,6 +232,22 @@ const RedemptionsTable = () => {
   });
   const [showEdit, setShowEdit] = useState(false);
 
+  // Form 初始值
+  const formInitValues = {
+    searchKeyword: '',
+  };
+
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
+
+  // 获取表单值的辅助函数
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
+    return {
+      searchKeyword: formValues.searchKeyword || '',
+    };
+  };
+
   const closeEdit = () => {
     setShowEdit(false);
     setTimeout(() => {
@@ -340,8 +355,14 @@ const RedemptionsTable = () => {
     setLoading(false);
   };
 
-  const searchRedemptions = async (keyword, page, pageSize) => {
-    if (searchKeyword === '') {
+  const searchRedemptions = async (keyword = null, page, pageSize) => {
+    // 如果没有传递keyword参数,从表单获取值
+    if (keyword === null) {
+      const formValues = getFormValues();
+      keyword = formValues.searchKeyword;
+    }
+
+    if (keyword === '') {
       await loadRedemptions(page, pageSize);
       return;
     }
@@ -361,10 +382,6 @@ const RedemptionsTable = () => {
     setSearching(false);
   };
 
-  const handleKeywordChange = async (value) => {
-    setSearchKeyword(value.trim());
-  };
-
   const sortRedemption = (key) => {
     if (redemptions.length === 0) return;
     setLoading(true);
@@ -381,6 +398,7 @@ const RedemptionsTable = () => {
 
   const handlePageChange = (page) => {
     setActivePage(page);
+    const { searchKeyword } = getFormValues();
     if (searchKeyword === '') {
       loadRedemptions(page, pageSize).then();
     } else {
@@ -457,28 +475,59 @@ const RedemptionsTable = () => {
           </Button>
         </div>
 
-        <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
-          <div className="relative w-full md:w-64">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('关键字(id或者名称)')}
-              value={searchKeyword}
-              onChange={handleKeywordChange}
-              className="!rounded-full"
-              showClear
-            />
+        <Form
+          initValues={formInitValues}
+          getFormApi={(api) => setFormApi(api)}
+          onSubmit={() => {
+            setActivePage(1);
+            searchRedemptions(null, 1, pageSize);
+          }}
+          allowEmpty={true}
+          autoComplete="off"
+          layout="horizontal"
+          trigger="change"
+          stopValidateWithError={false}
+          className="w-full md:w-auto order-1 md:order-2"
+        >
+          <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
+            <div className="relative w-full md:w-64">
+              <Form.Input
+                field="searchKeyword"
+                prefix={<IconSearch />}
+                placeholder={t('关键字(id或者名称)')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="flex gap-2 w-full md:w-auto">
+              <Button
+                type="primary"
+                htmlType="submit"
+                loading={searching}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('查询')}
+              </Button>
+              <Button
+                theme="light"
+                onClick={() => {
+                  if (formApi) {
+                    formApi.reset();
+                    // 重置后立即查询,使用setTimeout确保表单重置完成
+                    setTimeout(() => {
+                      setActivePage(1);
+                      loadRedemptions(1, pageSize);
+                    }, 100);
+                  }
+                }}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('重置')}
+              </Button>
+            </div>
           </div>
-          <Button
-            type="primary"
-            onClick={() => {
-              searchRedemptions(searchKeyword, 1, pageSize).then();
-            }}
-            loading={searching}
-            className="!rounded-full w-full md:w-auto"
-          >
-            {t('查询')}
-          </Button>
-        </div>
+        </Form>
       </div>
     </div>
   );
@@ -517,6 +566,7 @@ const RedemptionsTable = () => {
             onPageSizeChange: (size) => {
               setPageSize(size);
               setActivePage(1);
+              const { searchKeyword } = getFormValues();
               if (searchKeyword === '') {
                 loadRedemptions(1, size).then();
               } else {

+ 113 - 68
web/src/components/table/TaskLogsTable.js

@@ -13,9 +13,8 @@ import {
   Button,
   Card,
   Checkbox,
-  DatePicker,
   Divider,
-  Input,
+  Form,
   Layout,
   Modal,
   Progress,
@@ -437,21 +436,43 @@ const LogsTable = () => {
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
   const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
-  const [logType] = useState(0);
 
   let now = new Date();
   // 初始化start_timestamp为前一天
   let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
-  const [inputs, setInputs] = useState({
+
+  // Form 初始值
+  const formInitValues = {
     channel_id: '',
     task_id: '',
-    start_timestamp: timestamp2string(zeroNow.getTime() / 1000),
-    end_timestamp: '',
-  });
-  const { channel_id, task_id, start_timestamp, end_timestamp } = inputs;
+    dateRange: [
+      timestamp2string(zeroNow.getTime() / 1000),
+      timestamp2string(now.getTime() / 1000 + 3600)
+    ],
+  };
+
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
 
-  const handleInputChange = (value, name) => {
-    setInputs((inputs) => ({ ...inputs, [name]: value }));
+  // 获取表单值的辅助函数
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
+
+    // 处理时间范围
+    let start_timestamp = timestamp2string(zeroNow.getTime() / 1000);
+    let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
+
+    if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
+      start_timestamp = formValues.dateRange[0];
+      end_timestamp = formValues.dateRange[1];
+    }
+
+    return {
+      channel_id: formValues.channel_id || '',
+      task_id: formValues.task_id || '',
+      start_timestamp,
+      end_timestamp,
+    };
   };
 
   const setLogsFormat = (logs) => {
@@ -469,6 +490,7 @@ const LogsTable = () => {
     setLoading(true);
 
     let url = '';
+    const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues();
     let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
     let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
     if (isAdminUser) {
@@ -528,7 +550,7 @@ const LogsTable = () => {
     const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
     setPageSize(localPageSize);
     loadLogs(0, localPageSize).then();
-  }, [logType]);
+  }, []);
 
   // 列选择器模态框
   const renderColumnSelector = () => {
@@ -628,70 +650,93 @@ const LogsTable = () => {
               <Divider margin="12px" />
 
               {/* 搜索表单区域 */}
-              <div className="flex flex-col gap-4">
-                <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-                  {/* 时间选择器 */}
-                  <div className="col-span-1 lg:col-span-2">
-                    <DatePicker
-                      className="w-full"
-                      value={[start_timestamp, end_timestamp]}
-                      type='dateTimeRange'
-                      onChange={(value) => {
-                        if (Array.isArray(value) && value.length === 2) {
-                          handleInputChange(value[0], 'start_timestamp');
-                          handleInputChange(value[1], 'end_timestamp');
-                        }
-                      }}
-                    />
-                  </div>
-
-                  {/* 任务 ID */}
-                  <Input
-                    prefix={<IconSearch />}
-                    placeholder={t('任务 ID')}
-                    value={task_id}
-                    onChange={(value) => handleInputChange(value, 'task_id')}
-                    className="!rounded-full"
-                    showClear
-                  />
-
-                  {/* 渠道 ID - 仅管理员可见 */}
-                  {isAdminUser && (
-                    <Input
+              <Form
+                initValues={formInitValues}
+                getFormApi={(api) => setFormApi(api)}
+                onSubmit={refresh}
+                allowEmpty={true}
+                autoComplete="off"
+                layout="vertical"
+                trigger="change"
+                stopValidateWithError={false}
+              >
+                <div className="flex flex-col gap-4">
+                  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
+                    {/* 时间选择器 */}
+                    <div className="col-span-1 lg:col-span-2">
+                      <Form.DatePicker
+                        field='dateRange'
+                        className="w-full"
+                        type='dateTimeRange'
+                        placeholder={[t('开始时间'), t('结束时间')]}
+                        showClear
+                        pure
+                      />
+                    </div>
+
+                    {/* 任务 ID */}
+                    <Form.Input
+                      field='task_id'
                       prefix={<IconSearch />}
-                      placeholder={t('渠道 ID')}
-                      value={channel_id}
-                      onChange={(value) => handleInputChange(value, 'channel_id')}
+                      placeholder={t('任务 ID')}
                       className="!rounded-full"
                       showClear
+                      pure
                     />
-                  )}
-                </div>
 
-                {/* 操作按钮区域 */}
-                <div className="flex justify-between items-center pt-2">
-                  <div></div>
-                  <div className="flex gap-2">
-                    <Button
-                      type='primary'
-                      onClick={refresh}
-                      loading={loading}
-                      className="!rounded-full"
-                    >
-                      {t('查询')}
-                    </Button>
-                    <Button
-                      theme='light'
-                      type='tertiary'
-                      icon={<IconSetting />}
-                      onClick={() => setShowColumnSelector(true)}
-                      className="!rounded-full"
-                    >
-                      {t('列设置')}
-                    </Button>
+                    {/* 渠道 ID - 仅管理员可见 */}
+                    {isAdminUser && (
+                      <Form.Input
+                        field='channel_id'
+                        prefix={<IconSearch />}
+                        placeholder={t('渠道 ID')}
+                        className="!rounded-full"
+                        showClear
+                        pure
+                      />
+                    )}
+                  </div>
+
+                  {/* 操作按钮区域 */}
+                  <div className="flex justify-between items-center pt-2">
+                    <div></div>
+                    <div className="flex gap-2">
+                      <Button
+                        type='primary'
+                        htmlType='submit'
+                        loading={loading}
+                        className="!rounded-full"
+                      >
+                        {t('查询')}
+                      </Button>
+                      <Button
+                        theme='light'
+                        onClick={() => {
+                          if (formApi) {
+                            formApi.reset();
+                            // 重置后立即查询,使用setTimeout确保表单重置完成
+                            setTimeout(() => {
+                              refresh();
+                            }, 100);
+                          }
+                        }}
+                        className="!rounded-full"
+                      >
+                        {t('重置')}
+                      </Button>
+                      <Button
+                        theme='light'
+                        type='tertiary'
+                        icon={<IconSetting />}
+                        onClick={() => setShowColumnSelector(true)}
+                        className="!rounded-full"
+                      >
+                        {t('列设置')}
+                      </Button>
+                    </div>
                   </div>
                 </div>
-              </div>
+              </Form>
             </div>
           }
           shadows='always'

+ 78 - 43
web/src/components/table/TokensTable.js

@@ -14,12 +14,12 @@ import {
   Button,
   Card,
   Dropdown,
+  Form,
   Modal,
   Space,
   SplitButtonGroup,
   Table,
   Tag,
-  Input,
 } from '@douyinfe/semi-ui';
 
 import {
@@ -335,14 +335,29 @@ const TokensTable = () => {
   const [tokenCount, setTokenCount] = useState(pageSize);
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
-  const [searchKeyword, setSearchKeyword] = useState('');
-  const [searchToken, setSearchToken] = useState('');
   const [searching, setSearching] = useState(false);
-  const [chats, setChats] = useState([]);
   const [editingToken, setEditingToken] = useState({
     id: undefined,
   });
 
+  // Form 初始值
+  const formInitValues = {
+    searchKeyword: '',
+    searchToken: '',
+  };
+
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
+
+  // 获取表单值的辅助函数
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
+    return {
+      searchKeyword: formValues.searchKeyword || '',
+      searchToken: formValues.searchToken || '',
+    };
+  };
+
   const closeEdit = () => {
     setShowEdit(false);
     setTimeout(() => {
@@ -416,8 +431,6 @@ const TokensTable = () => {
     window.open(url, '_blank');
   };
 
-
-
   useEffect(() => {
     loadTokens(0)
       .then()
@@ -472,6 +485,7 @@ const TokensTable = () => {
   };
 
   const searchTokens = async () => {
+    const { searchKeyword, searchToken } = getFormValues();
     if (searchKeyword === '' && searchToken === '') {
       await loadTokens(0);
       setActivePage(1);
@@ -491,14 +505,6 @@ const TokensTable = () => {
     setSearching(false);
   };
 
-  const handleKeywordChange = async (value) => {
-    setSearchKeyword(value.trim());
-  };
-
-  const handleSearchTokenChange = async (value) => {
-    setSearchToken(value.trim());
-  };
-
   const sortToken = (key) => {
     if (tokens.length === 0) return;
     setLoading(true);
@@ -580,36 +586,65 @@ const TokensTable = () => {
           </Button>
         </div>
 
-        <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
-          <div className="relative w-full md:w-56">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('搜索关键字')}
-              value={searchKeyword}
-              onChange={handleKeywordChange}
-              className="!rounded-full"
-              showClear
-            />
-          </div>
-          <div className="relative w-full md:w-56">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('密钥')}
-              value={searchToken}
-              onChange={handleSearchTokenChange}
-              className="!rounded-full"
-              showClear
-            />
+        <Form
+          initValues={formInitValues}
+          getFormApi={(api) => setFormApi(api)}
+          onSubmit={searchTokens}
+          allowEmpty={true}
+          autoComplete="off"
+          layout="horizontal"
+          trigger="change"
+          stopValidateWithError={false}
+          className="w-full md:w-auto order-1 md:order-2"
+        >
+          <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
+            <div className="relative w-full md:w-56">
+              <Form.Input
+                field="searchKeyword"
+                prefix={<IconSearch />}
+                placeholder={t('搜索关键字')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="relative w-full md:w-56">
+              <Form.Input
+                field="searchToken"
+                prefix={<IconSearch />}
+                placeholder={t('密钥')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="flex gap-2 w-full md:w-auto">
+              <Button
+                type="primary"
+                htmlType="submit"
+                loading={searching}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('查询')}
+              </Button>
+              <Button
+                theme="light"
+                onClick={() => {
+                  if (formApi) {
+                    formApi.reset();
+                    // 重置后立即查询,使用setTimeout确保表单重置完成
+                    setTimeout(() => {
+                      searchTokens();
+                    }, 100);
+                  }
+                }}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('重置')}
+              </Button>
+            </div>
           </div>
-          <Button
-            type="primary"
-            onClick={searchTokens}
-            loading={searching}
-            className="!rounded-full w-full md:w-auto"
-          >
-            {t('查询')}
-          </Button>
-        </div>
+        </Form>
       </div>
     </div>
   );

+ 102 - 47
web/src/components/table/UsersTable.js

@@ -5,9 +5,8 @@ import {
   Card,
   Divider,
   Dropdown,
-  Input,
+  Form,
   Modal,
-  Select,
   Space,
   Table,
   Tag,
@@ -285,9 +284,7 @@ const UsersTable = () => {
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
-  const [searchKeyword, setSearchKeyword] = useState('');
   const [searching, setSearching] = useState(false);
-  const [searchGroup, setSearchGroup] = useState('');
   const [groupOptions, setGroupOptions] = useState([]);
   const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
   const [showAddUser, setShowAddUser] = useState(false);
@@ -296,6 +293,24 @@ const UsersTable = () => {
     id: undefined,
   });
 
+  // Form 初始值
+  const formInitValues = {
+    searchKeyword: '',
+    searchGroup: '',
+  };
+
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
+
+  // 获取表单值的辅助函数
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
+    return {
+      searchKeyword: formValues.searchKeyword || '',
+      searchGroup: formValues.searchGroup || '',
+    };
+  };
+
   const removeRecord = (key) => {
     let newDataSource = [...users];
     if (key != null) {
@@ -363,9 +378,16 @@ const UsersTable = () => {
   const searchUsers = async (
     startIdx,
     pageSize,
-    searchKeyword,
-    searchGroup,
+    searchKeyword = null,
+    searchGroup = null,
   ) => {
+    // 如果没有传递参数,从表单获取值
+    if (searchKeyword === null || searchGroup === null) {
+      const formValues = getFormValues();
+      searchKeyword = formValues.searchKeyword;
+      searchGroup = formValues.searchGroup;
+    }
+
     if (searchKeyword === '' && searchGroup === '') {
       // if keyword is blank, load files instead.
       await loadUsers(startIdx, pageSize);
@@ -387,12 +409,9 @@ const UsersTable = () => {
     setSearching(false);
   };
 
-  const handleKeywordChange = async (value) => {
-    setSearchKeyword(value.trim());
-  };
-
   const handlePageChange = (page) => {
     setActivePage(page);
+    const { searchKeyword, searchGroup } = getFormValues();
     if (searchKeyword === '' && searchGroup === '') {
       loadUsers(page, pageSize).then();
     } else {
@@ -413,10 +432,11 @@ const UsersTable = () => {
 
   const refresh = async () => {
     setActivePage(1);
-    if (searchKeyword === '') {
-      await loadUsers(activePage, pageSize);
+    const { searchKeyword, searchGroup } = getFormValues();
+    if (searchKeyword === '' && searchGroup === '') {
+      await loadUsers(1, pageSize);
     } else {
-      await searchUsers(activePage, pageSize, searchKeyword, searchGroup);
+      await searchUsers(1, pageSize, searchKeyword, searchGroup);
     }
   };
 
@@ -488,41 +508,76 @@ const UsersTable = () => {
           </Button>
         </div>
 
-        <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
-          <div className="relative w-full md:w-64">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
-              value={searchKeyword}
-              onChange={handleKeywordChange}
-              className="!rounded-full"
-              showClear
-            />
-          </div>
-          <div className="w-full md:w-48">
-            <Select
-              placeholder={t('选择分组')}
-              optionList={groupOptions}
-              value={searchGroup}
-              onChange={(value) => {
-                setSearchGroup(value);
-                searchUsers(activePage, pageSize, searchKeyword, value);
-              }}
-              className="!rounded-full w-full"
-              showClear
-            />
+        <Form
+          initValues={formInitValues}
+          getFormApi={(api) => setFormApi(api)}
+          onSubmit={() => {
+            setActivePage(1);
+            searchUsers(1, pageSize);
+          }}
+          allowEmpty={true}
+          autoComplete="off"
+          layout="horizontal"
+          trigger="change"
+          stopValidateWithError={false}
+          className="w-full md:w-auto order-1 md:order-2"
+        >
+          <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
+            <div className="relative w-full md:w-64">
+              <Form.Input
+                field="searchKeyword"
+                prefix={<IconSearch />}
+                placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="w-full md:w-48">
+              <Form.Select
+                field="searchGroup"
+                placeholder={t('选择分组')}
+                optionList={groupOptions}
+                onChange={(value) => {
+                  // 分组变化时自动搜索
+                  setTimeout(() => {
+                    setActivePage(1);
+                    searchUsers(1, pageSize);
+                  }, 100);
+                }}
+                className="!rounded-full w-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="flex gap-2 w-full md:w-auto">
+              <Button
+                type="primary"
+                htmlType="submit"
+                loading={searching}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('查询')}
+              </Button>
+              <Button
+                theme="light"
+                onClick={() => {
+                  if (formApi) {
+                    formApi.reset();
+                    // 重置后立即查询,使用setTimeout确保表单重置完成
+                    setTimeout(() => {
+                      setActivePage(1);
+                      loadUsers(1, pageSize);
+                    }, 100);
+                  }
+                }}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('重置')}
+              </Button>
+            </div>
           </div>
-          <Button
-            type="primary"
-            onClick={() => {
-              searchUsers(activePage, pageSize, searchKeyword, searchGroup);
-            }}
-            loading={searching}
-            className="!rounded-full w-full md:w-auto"
-          >
-            {t('查询')}
-          </Button>
-        </div>
+        </Form>
       </div>
     </div>
   );