♻️ 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.
This commit is contained in:
Apple\Apple
2025-06-08 18:41:04 +08:00
parent 4eef3feef3
commit 86354e305e
7 changed files with 592 additions and 529 deletions

View File

@@ -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}
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"
showClear
/>
</div>
<Button
type="primary"
onClick={() => {
searchChannels(searchKeyword, searchGroup, searchModel, enableTagMode);
}}
loading={searching}
className="!rounded-full w-full md:w-auto"
<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"
>
{t('查询')}
</Button>
<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}
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 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={{

View File

@@ -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>

View File

@@ -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>
<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 */}
<Input
prefix={<IconSearch />}
placeholder={t('任务 ID')}
value={mj_id}
onChange={(value) => handleInputChange(value, 'mj_id')}
className="!rounded-full"
showClear
/>
{/* 渠道 ID - 仅管理员可见 */}
{isAdminUser && (
<Input
{/* 任务 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'

View File

@@ -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 {

View File

@@ -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)
],
};
const handleInputChange = (value, name) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
// Form API 引用
const [formApi, setFormApi] = useState(null);
// 获取表单值的辅助函数
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>
<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 */}
<Input
prefix={<IconSearch />}
placeholder={t('任务 ID')}
value={task_id}
onChange={(value) => handleInputChange(value, 'task_id')}
className="!rounded-full"
showClear
/>
{/* 渠道 ID - 仅管理员可见 */}
{isAdminUser && (
<Input
{/* 任务 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'

View File

@@ -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
/>
<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>
<div className="relative w-full md:w-56">
<Input
prefix={<IconSearch />}
placeholder={t('密钥')}
value={searchToken}
onChange={handleSearchTokenChange}
className="!rounded-full"
showClear
/>
</div>
<Button
type="primary"
onClick={searchTokens}
loading={searching}
className="!rounded-full w-full md:w-auto"
>
{t('查询')}
</Button>
</div>
</Form>
</div>
</div>
);

View File

@@ -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
/>
<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>
<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
/>
</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>
);