refactor(LogsTable): enhance Form component with auto-search and state synchronization

- Refactor Form component to use Semi Design best practices
- Remove duplicate initValues configuration for DatePicker
- Add real-time value change monitoring with onValueChange
- Implement auto-search functionality for log type selector changes
- Fix state synchronization issues causing stale values in search requests
- Optimize form layout with proper vertical layout configuration
- Enhance user experience with placeholders, clear buttons, and search icons
- Remove logType parameter passing to prevent async state update conflicts
- Ensure all form controls use latest values from formApi instead of stale state
- Add proper validation triggers and error handling configuration
- Improve reset button logic with proper timing for form state updates

The changes resolve the issue where users needed to select log type twice
for the search request to use the correct value, and ensure all form
interactions provide immediate and accurate results.
This commit is contained in:
Apple\Apple
2025-06-08 17:28:28 +08:00
parent 1a6f332223
commit 4eef3feef3

View File

@@ -29,7 +29,6 @@ import {
Descriptions, Descriptions,
Modal, Modal,
Popover, Popover,
Select,
Space, Space,
Spin, Spin,
Table, Table,
@@ -39,8 +38,7 @@ import {
Card, Card,
Typography, Typography,
Divider, Divider,
Input, Form,
DatePicker,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../../constants'; import { ITEMS_PER_PAGE } from '../../constants';
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
@@ -48,15 +46,6 @@ import { IconSetting, IconSearch, IconForward } from '@douyinfe/semi-icons';
const { Text } = Typography; const { Text } = Typography;
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>;
}
const MODE_OPTIONS = [
{ key: 'all', text: 'all', value: 'all' },
{ key: 'self', text: 'current user', value: 'self' },
];
const colors = [ const colors = [
'amber', 'amber',
'blue', 'blue',
@@ -737,39 +726,67 @@ const LogsTable = () => {
const [logType, setLogType] = useState(0); const [logType, setLogType] = useState(0);
const isAdminUser = isAdmin(); const isAdminUser = isAdmin();
let now = new Date(); let now = new Date();
// 初始化start_timestamp为今天0点
const [inputs, setInputs] = useState({ // Form 初始值
const formInitValues = {
username: '', username: '',
token_name: '', token_name: '',
model_name: '', model_name: '',
start_timestamp: timestamp2string(getTodayStartTimestamp()),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
channel: '', channel: '',
group: '', group: '',
}); dateRange: [
const { timestamp2string(getTodayStartTimestamp()),
username, timestamp2string(now.getTime() / 1000 + 3600)
token_name, ],
model_name, logType: '0',
start_timestamp, };
end_timestamp,
channel,
group,
} = inputs;
const [stat, setStat] = useState({ const [stat, setStat] = useState({
quota: 0, quota: 0,
token: 0, token: 0,
}); });
const handleInputChange = (value, name) => { // Form API 引用
setInputs((inputs) => ({ ...inputs, [name]: value })); const [formApi, setFormApi] = useState(null);
// 获取表单值的辅助函数,确保所有值都是字符串
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
// 处理时间范围
let start_timestamp = timestamp2string(getTodayStartTimestamp());
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 {
username: formValues.username || '',
token_name: formValues.token_name || '',
model_name: formValues.model_name || '',
start_timestamp,
end_timestamp,
channel: formValues.channel || '',
group: formValues.group || '',
logType: formValues.logType ? parseInt(formValues.logType) : 0,
};
}; };
const getLogSelfStat = async () => { const getLogSelfStat = async () => {
const {
token_name,
model_name,
start_timestamp,
end_timestamp,
group,
logType: formLogType,
} = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`; let url = `/api/log/self/stat?type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
url = encodeURI(url); url = encodeURI(url);
let res = await API.get(url); let res = await API.get(url);
const { success, message, data } = res.data; const { success, message, data } = res.data;
@@ -781,9 +798,20 @@ const LogsTable = () => {
}; };
const getLogStat = async () => { const getLogStat = async () => {
const {
username,
token_name,
model_name,
start_timestamp,
end_timestamp,
channel,
group,
logType: formLogType,
} = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`; let url = `/api/log/stat?type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
url = encodeURI(url); url = encodeURI(url);
let res = await API.get(url); let res = await API.get(url);
const { success, message, data } = res.data; const { success, message, data } = res.data;
@@ -1016,16 +1044,30 @@ const LogsTable = () => {
setLogs(logs); setLogs(logs);
}; };
const loadLogs = async (startIdx, pageSize, logType = 0) => { const loadLogs = async (startIdx, pageSize, customLogType = null) => {
setLoading(true); setLoading(true);
let url = ''; let url = '';
const {
username,
token_name,
model_name,
start_timestamp,
end_timestamp,
channel,
group,
logType: formLogType,
} = getFormValues();
// 使用传入的 logType 或者表单中的 logType 或者状态中的 logType
const currentLogType = customLogType !== null ? customLogType : formLogType !== undefined ? formLogType : logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) { if (isAdminUser) {
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`; url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
} else { } else {
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`; url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
} }
url = encodeURI(url); url = encodeURI(url);
const res = await API.get(url); const res = await API.get(url);
@@ -1045,7 +1087,7 @@ const LogsTable = () => {
const handlePageChange = (page) => { const handlePageChange = (page) => {
setActivePage(page); setActivePage(page);
loadLogs(page, pageSize, logType).then((r) => { }); loadLogs(page, pageSize).then((r) => { }); // 不传入logType让其从表单获取最新值
}; };
const handlePageSizeChange = async (size) => { const handlePageSizeChange = async (size) => {
@@ -1062,7 +1104,7 @@ const LogsTable = () => {
const refresh = async () => { const refresh = async () => {
setActivePage(1); setActivePage(1);
handleEyeClick(); handleEyeClick();
await loadLogs(activePage, pageSize, logType); await loadLogs(1, pageSize); // 不传入logType让其从表单获取最新值
}; };
const copyText = async (e, text) => { const copyText = async (e, text) => {
@@ -1083,9 +1125,15 @@ const LogsTable = () => {
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
}); });
handleEyeClick();
}, []); }, []);
// 当 formApi 可用时,初始化统计
useEffect(() => {
if (formApi) {
handleEyeClick();
}
}, [formApi]);
const expandRowRender = (record, index) => { const expandRowRender = (record, index) => {
return <Descriptions data={expandData[record.key]} />; return <Descriptions data={expandData[record.key]} />;
}; };
@@ -1149,86 +1197,102 @@ const LogsTable = () => {
<Divider margin='12px' /> <Divider margin='12px' />
{/* 搜索表单区域 */} {/* 搜索表单区域 */}
<Form
initValues={formInitValues}
getFormApi={(api) => setFormApi(api)}
onSubmit={refresh}
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}
>
<div className='flex flex-col gap-4'> <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='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
{/* 时间选择器 */} {/* 时间选择器 */}
<div className='col-span-1 lg:col-span-2'> <div className='col-span-1 lg:col-span-2'>
<DatePicker <Form.DatePicker
field='dateRange'
className='w-full' className='w-full'
value={[start_timestamp, end_timestamp]}
type='dateTimeRange' type='dateTimeRange'
onChange={(value) => { placeholder={[t('开始时间'), t('结束时间')]}
if (Array.isArray(value) && value.length === 2) { pure
handleInputChange(value[0], 'start_timestamp');
handleInputChange(value[1], 'end_timestamp');
}
}}
/> />
</div> </div>
{/* 日志类型选择器 */} {/* 日志类型选择器 */}
<Select <Form.Select
value={logType.toString()} field='logType'
placeholder={t('日志类型')} placeholder={t('日志类型')}
className='!rounded-full' className='!rounded-full'
onChange={(value) => { showClear
setLogType(parseInt(value)); pure
loadLogs(0, pageSize, parseInt(value));
}}
> >
<Select.Option value='0'>{t('全部')}</Select.Option> <Form.Select.Option value='0'>{t('全部')}</Form.Select.Option>
<Select.Option value='1'>{t('充值')}</Select.Option> <Form.Select.Option value='1'>{t('充值')}</Form.Select.Option>
<Select.Option value='2'>{t('消费')}</Select.Option> <Form.Select.Option value='2'>{t('消费')}</Form.Select.Option>
<Select.Option value='3'>{t('管理')}</Select.Option> <Form.Select.Option value='3'>{t('管理')}</Form.Select.Option>
<Select.Option value='4'>{t('系统')}</Select.Option> <Form.Select.Option value='4'>{t('系统')}</Form.Select.Option>
<Select.Option value='5'>{t('错误')}</Select.Option> <Form.Select.Option value='5'>{t('错误')}</Form.Select.Option>
</Select> </Form.Select>
{/* 其他搜索字段 */} {/* 其他搜索字段 */}
<Input <Form.Input
field='token_name'
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('令牌名称')} placeholder={t('令牌名称')}
value={token_name}
onChange={(value) => handleInputChange(value, 'token_name')}
className='!rounded-full' className='!rounded-full'
showClear showClear
pure
/> />
<Input <Form.Input
field='model_name'
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('模型名称')} placeholder={t('模型名称')}
value={model_name}
onChange={(value) => handleInputChange(value, 'model_name')}
className='!rounded-full' className='!rounded-full'
showClear showClear
pure
/> />
<Input <Form.Input
field='group'
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('分组')} placeholder={t('分组')}
value={group}
onChange={(value) => handleInputChange(value, 'group')}
className='!rounded-full' className='!rounded-full'
showClear showClear
pure
/> />
{isAdminUser && ( {isAdminUser && (
<> <>
<Input <Form.Input
field='channel'
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('渠道 ID')} placeholder={t('渠道 ID')}
value={channel}
onChange={(value) => handleInputChange(value, 'channel')}
className='!rounded-full' className='!rounded-full'
showClear showClear
pure
/> />
<Input <Form.Input
field='username'
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('用户名称')} placeholder={t('用户名称')}
value={username}
onChange={(value) => handleInputChange(value, 'username')}
className='!rounded-full' className='!rounded-full'
showClear showClear
pure
/> />
</> </>
)} )}
@@ -1240,12 +1304,28 @@ const LogsTable = () => {
<div className='flex gap-2'> <div className='flex gap-2'>
<Button <Button
type='primary' type='primary'
onClick={refresh} htmlType='submit'
loading={loading} loading={loading}
className='!rounded-full' className='!rounded-full'
> >
{t('查询')} {t('查询')}
</Button> </Button>
<Button
theme='light'
onClick={() => {
if (formApi) {
formApi.reset();
setLogType(0);
// 重置后立即查询使用setTimeout确保表单重置完成
setTimeout(() => {
refresh();
}, 100);
}
}}
className='!rounded-full'
>
{t('重置')}
</Button>
<Button <Button
theme='light' theme='light'
type='tertiary' type='tertiary'
@@ -1258,6 +1338,7 @@ const LogsTable = () => {
</div> </div>
</div> </div>
</div> </div>
</Form>
</div> </div>
} }
shadows='always' shadows='always'