From 4eef3feef3b39dc8b22ed9ddc673b2763e53945f Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Sun, 8 Jun 2025 17:28:28 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20refactor(LogsTable):=20enhance=20Fo?= =?UTF-8?q?rm=20component=20with=20auto-search=20and=20state=20synchroniza?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- web/src/components/table/LogsTable.js | 357 ++++++++++++++++---------- 1 file changed, 219 insertions(+), 138 deletions(-) diff --git a/web/src/components/table/LogsTable.js b/web/src/components/table/LogsTable.js index ea0dde7b..d6fd7c1f 100644 --- a/web/src/components/table/LogsTable.js +++ b/web/src/components/table/LogsTable.js @@ -29,7 +29,6 @@ import { Descriptions, Modal, Popover, - Select, Space, Spin, Table, @@ -39,8 +38,7 @@ import { Card, Typography, Divider, - Input, - DatePicker, + Form, } from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../../constants'; 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; -function renderTimestamp(timestamp) { - return <>{timestamp2string(timestamp)}; -} - -const MODE_OPTIONS = [ - { key: 'all', text: 'all', value: 'all' }, - { key: 'self', text: 'current user', value: 'self' }, -]; - const colors = [ 'amber', 'blue', @@ -737,39 +726,67 @@ const LogsTable = () => { const [logType, setLogType] = useState(0); const isAdminUser = isAdmin(); let now = new Date(); - // 初始化start_timestamp为今天0点 - const [inputs, setInputs] = useState({ + + // Form 初始值 + const formInitValues = { username: '', token_name: '', model_name: '', - start_timestamp: timestamp2string(getTodayStartTimestamp()), - end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), channel: '', group: '', - }); - const { - username, - token_name, - model_name, - start_timestamp, - end_timestamp, - channel, - group, - } = inputs; + dateRange: [ + timestamp2string(getTodayStartTimestamp()), + timestamp2string(now.getTime() / 1000 + 3600) + ], + logType: '0', + }; const [stat, setStat] = useState({ quota: 0, token: 0, }); - 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(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 { + 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 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); let res = await API.get(url); const { success, message, data } = res.data; @@ -781,9 +798,20 @@ const LogsTable = () => { }; 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 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); let res = await API.get(url); const { success, message, data } = res.data; @@ -1016,16 +1044,30 @@ const LogsTable = () => { setLogs(logs); }; - const loadLogs = async (startIdx, pageSize, logType = 0) => { + const loadLogs = async (startIdx, pageSize, customLogType = null) => { setLoading(true); 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 localEndTimestamp = Date.parse(end_timestamp) / 1000; 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 { - 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); const res = await API.get(url); @@ -1045,7 +1087,7 @@ const LogsTable = () => { const handlePageChange = (page) => { setActivePage(page); - loadLogs(page, pageSize, logType).then((r) => { }); + loadLogs(page, pageSize).then((r) => { }); // 不传入logType,让其从表单获取最新值 }; const handlePageSizeChange = async (size) => { @@ -1062,7 +1104,7 @@ const LogsTable = () => { const refresh = async () => { setActivePage(1); handleEyeClick(); - await loadLogs(activePage, pageSize, logType); + await loadLogs(1, pageSize); // 不传入logType,让其从表单获取最新值 }; const copyText = async (e, text) => { @@ -1083,9 +1125,15 @@ const LogsTable = () => { .catch((reason) => { showError(reason); }); - handleEyeClick(); }, []); + // 当 formApi 可用时,初始化统计 + useEffect(() => { + if (formApi) { + handleEyeClick(); + } + }, [formApi]); + const expandRowRender = (record, index) => { return ; }; @@ -1149,115 +1197,148 @@ const LogsTable = () => { {/* 搜索表单区域 */} -
-
- {/* 时间选择器 */} -
- { - if (Array.isArray(value) && value.length === 2) { - handleInputChange(value[0], 'start_timestamp'); - handleInputChange(value[1], 'end_timestamp'); - } - }} +
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} + > +
+
+ {/* 时间选择器 */} +
+ +
+ + {/* 日志类型选择器 */} + + {t('全部')} + {t('充值')} + {t('消费')} + {t('管理')} + {t('系统')} + {t('错误')} + + + {/* 其他搜索字段 */} + } + placeholder={t('令牌名称')} + className='!rounded-full' + showClear + pure /> + + } + placeholder={t('模型名称')} + className='!rounded-full' + showClear + pure + /> + + } + placeholder={t('分组')} + className='!rounded-full' + showClear + pure + /> + + {isAdminUser && ( + <> + } + placeholder={t('渠道 ID')} + className='!rounded-full' + showClear + pure + /> + } + placeholder={t('用户名称')} + className='!rounded-full' + showClear + pure + /> + + )}
- {/* 日志类型选择器 */} - - - {/* 其他搜索字段 */} - } - placeholder={t('令牌名称')} - value={token_name} - onChange={(value) => handleInputChange(value, 'token_name')} - className='!rounded-full' - showClear - /> - - } - placeholder={t('模型名称')} - value={model_name} - onChange={(value) => handleInputChange(value, 'model_name')} - className='!rounded-full' - showClear - /> - - } - placeholder={t('分组')} - value={group} - onChange={(value) => handleInputChange(value, 'group')} - className='!rounded-full' - showClear - /> - - {isAdminUser && ( - <> - } - placeholder={t('渠道 ID')} - value={channel} - onChange={(value) => handleInputChange(value, 'channel')} + {/* 操作按钮区域 */} +
+
+
+ +
- - {/* 操作按钮区域 */} -
-
-
- - + > + {t('重置')} + + +
-
+
} shadows='always'