From 0befa28e8ee85f20bade7aba57d010deab7bb712 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Fri, 23 May 2025 13:43:02 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8FRefactor:=20TaskLogs=20Page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/TaskLogsTable.js | 705 ++++++++++++++++++---------- web/src/i18n/locales/en.json | 4 +- 2 files changed, 469 insertions(+), 240 deletions(-) diff --git a/web/src/components/TaskLogsTable.js b/web/src/components/TaskLogsTable.js index 4d243133..c6c25a94 100644 --- a/web/src/components/TaskLogsTable.js +++ b/web/src/components/TaskLogsTable.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Label } from 'semantic-ui-react'; +import { useTranslation } from 'react-i18next'; import { API, copy, @@ -10,17 +10,28 @@ import { } from '../helpers'; import { - Table, - Tag, - Form, Button, + Card, + Checkbox, + DatePicker, + Divider, + Input, Layout, Modal, - Typography, Progress, - Card, + Skeleton, + Table, + Tag, + Typography, } from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../constants'; +import { + IconEyeOpened, + IconSearch, + IconSetting, +} from '@douyinfe/semi-icons'; + +const { Text } = Typography; const colors = [ 'amber', @@ -40,6 +51,20 @@ const colors = [ 'yellow', ]; +// 定义列键值常量 +const COLUMN_KEYS = { + SUBMIT_TIME: 'submit_time', + FINISH_TIME: 'finish_time', + DURATION: 'duration', + CHANNEL: 'channel', + PLATFORM: 'platform', + TYPE: 'type', + TASK_ID: 'task_id', + TASK_STATUS: 'task_status', + PROGRESS: 'progress', + FAIL_REASON: 'fail_reason', +}; + const renderTimestamp = (timestampInSeconds) => { const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒 @@ -79,101 +104,259 @@ function renderDuration(submit_time, finishTime) { } const LogsTable = () => { + const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); const [modalContent, setModalContent] = useState(''); + + // 列可见性状态 + const [visibleColumns, setVisibleColumns] = useState({}); + const [showColumnSelector, setShowColumnSelector] = useState(false); const isAdminUser = isAdmin(); - const columns = [ + const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); + + // 加载保存的列偏好设置 + useEffect(() => { + const savedColumns = localStorage.getItem('task-logs-table-columns'); + if (savedColumns) { + try { + const parsed = JSON.parse(savedColumns); + const defaults = getDefaultColumnVisibility(); + const merged = { ...defaults, ...parsed }; + setVisibleColumns(merged); + } catch (e) { + console.error('Failed to parse saved column preferences', e); + initDefaultColumns(); + } + } else { + initDefaultColumns(); + } + }, []); + + // 获取默认列可见性 + const getDefaultColumnVisibility = () => { + return { + [COLUMN_KEYS.SUBMIT_TIME]: true, + [COLUMN_KEYS.FINISH_TIME]: true, + [COLUMN_KEYS.DURATION]: true, + [COLUMN_KEYS.CHANNEL]: isAdminUser, + [COLUMN_KEYS.PLATFORM]: true, + [COLUMN_KEYS.TYPE]: true, + [COLUMN_KEYS.TASK_ID]: true, + [COLUMN_KEYS.TASK_STATUS]: true, + [COLUMN_KEYS.PROGRESS]: true, + [COLUMN_KEYS.FAIL_REASON]: true, + }; + }; + + // 初始化默认列可见性 + const initDefaultColumns = () => { + const defaults = getDefaultColumnVisibility(); + setVisibleColumns(defaults); + localStorage.setItem('task-logs-table-columns', JSON.stringify(defaults)); + }; + + // 处理列可见性变化 + const handleColumnVisibilityChange = (columnKey, checked) => { + const updatedColumns = { ...visibleColumns, [columnKey]: checked }; + setVisibleColumns(updatedColumns); + }; + + // 处理全选 + const handleSelectAll = (checked) => { + const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]); + const updatedColumns = {}; + + allKeys.forEach((key) => { + if (key === COLUMN_KEYS.CHANNEL && !isAdminUser) { + updatedColumns[key] = false; + } else { + updatedColumns[key] = checked; + } + }); + + setVisibleColumns(updatedColumns); + }; + + // 更新表格时保存列可见性 + useEffect(() => { + if (Object.keys(visibleColumns).length > 0) { + localStorage.setItem('task-logs-table-columns', JSON.stringify(visibleColumns)); + } + }, [visibleColumns]); + + const renderType = (type) => { + switch (type) { + case 'MUSIC': + return ( + + {t('生成音乐')} + + ); + case 'LYRICS': + return ( + + {t('生成歌词')} + + ); + default: + return ( + + {t('未知')} + + ); + } + }; + + const renderPlatform = (type) => { + switch (type) { + case 'suno': + return ( + + Suno + + ); + default: + return ( + + {t('未知')} + + ); + } + }; + + const renderStatus = (type) => { + switch (type) { + case 'SUCCESS': + return ( + + {t('成功')} + + ); + case 'NOT_START': + return ( + + {t('未启动')} + + ); + case 'SUBMITTED': + return ( + + {t('队列中')} + + ); + case 'IN_PROGRESS': + return ( + + {t('执行中')} + + ); + case 'FAILURE': + return ( + + {t('失败')} + + ); + case 'QUEUED': + return ( + + {t('排队中')} + + ); + case 'UNKNOWN': + return ( + + {t('未知')} + + ); + case '': + return ( + + {t('正在提交')} + + ); + default: + return ( + + {t('未知')} + + ); + } + }; + + // 定义所有列 + const allColumns = [ { - title: '提交时间', + key: COLUMN_KEYS.SUBMIT_TIME, + title: t('提交时间'), dataIndex: 'submit_time', render: (text, record, index) => { return
{text ? renderTimestamp(text) : '-'}
; }, }, { - title: '结束时间', + key: COLUMN_KEYS.FINISH_TIME, + title: t('结束时间'), dataIndex: 'finish_time', render: (text, record, index) => { return
{text ? renderTimestamp(text) : '-'}
; }, }, { - title: '进度', - dataIndex: 'progress', - width: 50, - render: (text, record, index) => { - return ( -
- { - // 转换例如100%为数字100,如果text未定义,返回0 - isNaN(text.replace('%', '')) ? ( - text - ) : ( - - ) - } -
- ); - }, - }, - { - title: '花费时间', - dataIndex: 'finish_time', // 以finish_time作为dataIndex - key: 'finish_time', + key: COLUMN_KEYS.DURATION, + title: t('花费时间'), + dataIndex: 'finish_time', render: (finish, record) => { - // 假设record.start_time是存在的,并且finish是完成时间的时间戳 return <>{finish ? renderDuration(record.submit_time, finish) : '-'}; }, }, { - title: '渠道', + key: COLUMN_KEYS.CHANNEL, + title: t('渠道'), dataIndex: 'channel_id', className: isAdminUser ? 'tableShow' : 'tableHiddle', render: (text, record, index) => { - return ( + return isAdminUser ? (
{ - copyText(text); // 假设copyText是用于文本复制的函数 + copyText(text); }} > - {' '} - {text}{' '} + {text}
+ ) : ( + <> ); }, }, { - title: '平台', + key: COLUMN_KEYS.PLATFORM, + title: t('平台'), dataIndex: 'platform', render: (text, record, index) => { return
{renderPlatform(text)}
; }, }, { - title: '类型', + key: COLUMN_KEYS.TYPE, + title: t('类型'), dataIndex: 'action', render: (text, record, index) => { return
{renderType(text)}
; }, }, { - title: '任务ID(点击查看详情)', + key: COLUMN_KEYS.TASK_ID, + title: t('任务ID'), dataIndex: 'task_id', render: (text, record, index) => { return ( { setModalContent(JSON.stringify(record, null, 2)); setIsModalOpen(true); @@ -185,22 +368,48 @@ const LogsTable = () => { }, }, { - title: '任务状态', + key: COLUMN_KEYS.TASK_STATUS, + title: t('任务状态'), dataIndex: 'status', render: (text, record, index) => { return
{renderStatus(text)}
; }, }, - { - title: '失败原因', + key: COLUMN_KEYS.PROGRESS, + title: t('进度'), + dataIndex: 'progress', + render: (text, record, index) => { + return ( +
+ { + isNaN(text?.replace('%', '')) ? ( + text || '-' + ) : ( + + ) + } +
+ ); + }, + }, + { + key: COLUMN_KEYS.FAIL_REASON, + title: t('失败原因'), dataIndex: 'fail_reason', render: (text, record, index) => { - // 如果text未定义,返回替代文本,例如空字符串''或其他 if (!text) { - return '无'; + return t('无'); } - return ( { }, ]; + // 根据可见性设置过滤列 + const getVisibleColumns = () => { + return allColumns.filter((column) => visibleColumns[column.key]); + }; + const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); @@ -249,16 +463,16 @@ const LogsTable = () => { // console.log(logCount); }; - const loadLogs = async (startIdx) => { + const loadLogs = async (startIdx, pageSize = ITEMS_PER_PAGE) => { setLoading(true); let url = ''; let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000); let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000); if (isAdminUser) { - url = `/api/task/?p=${startIdx}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; + url = `/api/task/?p=${startIdx}&page_size=${pageSize}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; } else { - url = `/api/task/self?p=${startIdx}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; + url = `/api/task/self?p=${startIdx}&page_size=${pageSize}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; } const res = await API.get(url); let { success, message, data } = res.data; @@ -267,7 +481,7 @@ const LogsTable = () => { setLogsFormat(data); } else { let newLogs = [...logs]; - newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); + newLogs.splice(startIdx * pageSize, data.length, ...data); setLogsFormat(newLogs); } } else { @@ -277,223 +491,236 @@ const LogsTable = () => { }; const pageData = logs.slice( - (activePage - 1) * ITEMS_PER_PAGE, - activePage * ITEMS_PER_PAGE, + (activePage - 1) * pageSize, + activePage * pageSize, ); const handlePageChange = (page) => { setActivePage(page); - if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) { - // In this case we have to load more data and then append them. - loadLogs(page - 1).then((r) => {}); + if (page === Math.ceil(logs.length / pageSize) + 1) { + loadLogs(page - 1, pageSize).then((r) => { }); } }; - const refresh = async () => { - // setLoading(true); + const handlePageSizeChange = async (size) => { + localStorage.setItem('task-page-size', size + ''); + setPageSize(size); setActivePage(1); - await loadLogs(0); + await loadLogs(0, size); + }; + + const refresh = async () => { + setActivePage(1); + await loadLogs(0, pageSize); }; const copyText = async (text) => { if (await copy(text)) { - showSuccess('已复制:' + text); + showSuccess(t('已复制:') + text); } else { - // setSearchKeyword(text); - Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); + Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text }); } }; useEffect(() => { - refresh().then(); + const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE; + setPageSize(localPageSize); + loadLogs(0, localPageSize).then(); }, [logType]); - const renderType = (type) => { - switch (type) { - case 'MUSIC': - return ( - - ); - case 'LYRICS': - return ( - - ); + // 列选择器模态框 + const renderColumnSelector = () => { + return ( + setShowColumnSelector(false)} + footer={ +
+ + + +
+ } + > +
+ v === true)} + indeterminate={ + Object.values(visibleColumns).some((v) => v === true) && + !Object.values(visibleColumns).every((v) => v === true) + } + onChange={(e) => handleSelectAll(e.target.checked)} + > + {t('全选')} + +
+
+ {allColumns.map((column) => { + // 为非管理员用户跳过管理员专用列 + if (!isAdminUser && column.key === COLUMN_KEYS.CHANNEL) { + return null; + } - default: - return ( - - ); - } - }; - - const renderPlatform = (type) => { - switch (type) { - case 'suno': - return ( - - ); - default: - return ( - - ); - } - }; - - const renderStatus = (type) => { - switch (type) { - case 'SUCCESS': - return ( - - ); - case 'NOT_START': - return ( - - ); - case 'SUBMITTED': - return ( - - ); - case 'IN_PROGRESS': - return ( - - ); - case 'FAILURE': - return ( - - ); - case 'QUEUED': - return ( - - ); - case 'UNKNOWN': - return ( - - ); - case '': - return ( - - ); - default: - return ( - - ); - } + return ( +
+ + handleColumnVisibilityChange(column.key, e.target.checked) + } + > + {column.title} + +
+ ); + })} +
+
+ ); }; return ( <> + {renderColumnSelector()} -
- <> - {isAdminUser && ( - handleInputChange(value, 'channel_id')} - /> - )} - handleInputChange(value, 'task_id')} - /> + +
+
+ + {loading ? ( + + ) : ( + {t('任务记录')} + )} +
+
- handleInputChange(value, 'start_timestamp')} - /> - handleInputChange(value, 'end_timestamp')} - /> - - - - + + + {/* 搜索表单区域 */} +
+
+ {/* 时间选择器 */} +
+ { + if (Array.isArray(value) && value.length === 2) { + handleInputChange(value[0], 'start_timestamp'); + handleInputChange(value[1], 'end_timestamp'); + } + }} + /> +
+ + {/* 任务 ID */} + } + placeholder={t('任务 ID')} + value={task_id} + onChange={(value) => handleInputChange(value, 'task_id')} + className="!rounded-full" + showClear + /> + + {/* 渠道 ID - 仅管理员可见 */} + {isAdminUser && ( + } + placeholder={t('渠道 ID')} + value={channel_id} + onChange={(value) => handleInputChange(value, 'channel_id')} + className="!rounded-full" + showClear + /> + )} +
+ + {/* 操作按钮区域 */} +
+
+
+ + +
+
+
+ + } + shadows='hover' + > + t('第 {{start}} - {{end}} 条,共 {{total}} 条', { + start: page.currentStart, + end: page.currentEnd, + total: logCount, + }), currentPage: activePage, - pageSize: ITEMS_PER_PAGE, + pageSize: pageSize, total: logCount, - pageSizeOpts: [10, 20, 50, 100], + pageSizeOptions: [10, 20, 50, 100], + showSizeChanger: true, + onPageSizeChange: (size) => { + handlePageSizeChange(size); + }, onPageChange: handlePageChange, }} - loading={loading} /> + setIsModalOpen(false)} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 817034d8..e1f7bd0c 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -512,6 +512,7 @@ "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!", "取消无限额度": "Cancel unlimited quota", "取消": "Cancel", + "重置": "Reset", "请输入新的剩余额度": "Please enter the new remaining quota", "请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code", "请输入用户名": "Please enter username", @@ -1430,5 +1431,6 @@ "20个": "20 items", "30个": "30 items", "100个": "100 items", - "Midjourney 任务记录": "Midjourney Task Records" + "Midjourney 任务记录": "Midjourney Task Records", + "任务记录": "Task Records" } \ No newline at end of file