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()}
- handleInputChange(value, 'channel_id')}
- />
- )}
- handleInputChange(value, 'task_id')}
- />
+
+
+
+
+ {loading ? (
+
+ ) : (
+ {t('任务记录')}
+ )}
+
+
- handleInputChange(value, 'start_timestamp')}
- />
- handleInputChange(value, 'end_timestamp')}
- />
-
- >
-
-
+
+
+ {/* 搜索表单区域 */}
+
+
+
+ {/* 操作按钮区域 */}
+
+
+
+
+ }
+ onClick={() => setShowColumnSelector(true)}
+ className="!rounded-full"
+ >
+ {t('列设置')}
+
+
+
+
+
+ }
+ 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