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,115 +1197,148 @@ const LogsTable = () => {
<Divider margin='12px' /> <Divider margin='12px' />
{/* 搜索表单区域 */} {/* 搜索表单区域 */}
<div className='flex flex-col gap-4'> <Form
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'> initValues={formInitValues}
{/* 时间选择器 */} getFormApi={(api) => setFormApi(api)}
<div className='col-span-1 lg:col-span-2'> onSubmit={refresh}
<DatePicker allowEmpty={true}
className='w-full' autoComplete="off"
value={[start_timestamp, end_timestamp]} layout="vertical"
type='dateTimeRange' onValueChange={(values, changedValue) => {
onChange={(value) => { // 实时监听日志类型变化
if (Array.isArray(value) && value.length === 2) { if (changedValue.logType !== undefined) {
handleInputChange(value[0], 'start_timestamp'); setLogType(parseInt(changedValue.logType));
handleInputChange(value[1], 'end_timestamp'); // 日志类型变化时自动搜索不传入logType参数让其从表单获取最新值
} setTimeout(() => {
}} setActivePage(1);
handleEyeClick();
loadLogs(1, pageSize); // 不传入logType参数
}, 100);
}
}}
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('结束时间')]}
pure
/>
</div>
{/* 日志类型选择器 */}
<Form.Select
field='logType'
placeholder={t('日志类型')}
className='!rounded-full'
showClear
pure
>
<Form.Select.Option value='0'>{t('全部')}</Form.Select.Option>
<Form.Select.Option value='1'>{t('充值')}</Form.Select.Option>
<Form.Select.Option value='2'>{t('消费')}</Form.Select.Option>
<Form.Select.Option value='3'>{t('管理')}</Form.Select.Option>
<Form.Select.Option value='4'>{t('系统')}</Form.Select.Option>
<Form.Select.Option value='5'>{t('错误')}</Form.Select.Option>
</Form.Select>
{/* 其他搜索字段 */}
<Form.Input
field='token_name'
prefix={<IconSearch />}
placeholder={t('令牌名称')}
className='!rounded-full'
showClear
pure
/> />
<Form.Input
field='model_name'
prefix={<IconSearch />}
placeholder={t('模型名称')}
className='!rounded-full'
showClear
pure
/>
<Form.Input
field='group'
prefix={<IconSearch />}
placeholder={t('分组')}
className='!rounded-full'
showClear
pure
/>
{isAdminUser && (
<>
<Form.Input
field='channel'
prefix={<IconSearch />}
placeholder={t('渠道 ID')}
className='!rounded-full'
showClear
pure
/>
<Form.Input
field='username'
prefix={<IconSearch />}
placeholder={t('用户名称')}
className='!rounded-full'
showClear
pure
/>
</>
)}
</div> </div>
{/* 日志类型选择器 */} {/* 操作按钮区域 */}
<Select <div className='flex justify-between items-center pt-2'>
value={logType.toString()} <div></div>
placeholder={t('日志类型')} <div className='flex gap-2'>
className='!rounded-full' <Button
onChange={(value) => { type='primary'
setLogType(parseInt(value)); htmlType='submit'
loadLogs(0, pageSize, parseInt(value)); loading={loading}
}}
>
<Select.Option value='0'>{t('全部')}</Select.Option>
<Select.Option value='1'>{t('充值')}</Select.Option>
<Select.Option value='2'>{t('消费')}</Select.Option>
<Select.Option value='3'>{t('管理')}</Select.Option>
<Select.Option value='4'>{t('系统')}</Select.Option>
<Select.Option value='5'>{t('错误')}</Select.Option>
</Select>
{/* 其他搜索字段 */}
<Input
prefix={<IconSearch />}
placeholder={t('令牌名称')}
value={token_name}
onChange={(value) => handleInputChange(value, 'token_name')}
className='!rounded-full'
showClear
/>
<Input
prefix={<IconSearch />}
placeholder={t('模型名称')}
value={model_name}
onChange={(value) => handleInputChange(value, 'model_name')}
className='!rounded-full'
showClear
/>
<Input
prefix={<IconSearch />}
placeholder={t('分组')}
value={group}
onChange={(value) => handleInputChange(value, 'group')}
className='!rounded-full'
showClear
/>
{isAdminUser && (
<>
<Input
prefix={<IconSearch />}
placeholder={t('渠道 ID')}
value={channel}
onChange={(value) => handleInputChange(value, 'channel')}
className='!rounded-full' className='!rounded-full'
showClear >
/> {t('查询')}
<Input </Button>
prefix={<IconSearch />} <Button
placeholder={t('用户名称')} theme='light'
value={username} onClick={() => {
onChange={(value) => handleInputChange(value, 'username')} if (formApi) {
formApi.reset();
setLogType(0);
// 重置后立即查询使用setTimeout确保表单重置完成
setTimeout(() => {
refresh();
}, 100);
}
}}
className='!rounded-full' className='!rounded-full'
showClear >
/> {t('重置')}
</> </Button>
)} <Button
</div> theme='light'
type='tertiary'
{/* 操作按钮区域 */} icon={<IconSetting />}
<div className='flex justify-between items-center pt-2'> onClick={() => setShowColumnSelector(true)}
<div></div> className='!rounded-full'
<div className='flex gap-2'> >
<Button {t('列设置')}
type='primary' </Button>
onClick={refresh} </div>
loading={loading}
className='!rounded-full'
>
{t('查询')}
</Button>
<Button
theme='light'
type='tertiary'
icon={<IconSetting />}
onClick={() => setShowColumnSelector(true)}
className='!rounded-full'
>
{t('列设置')}
</Button>
</div> </div>
</div> </div>
</div> </Form>
</div> </div>
} }
shadows='always' shadows='always'