diff --git a/web/src/components/table/TaskLogsTable.js b/web/src/components/table/TaskLogsTable.js
index 0e3abbb7..a6996611 100644
--- a/web/src/components/table/TaskLogsTable.js
+++ b/web/src/components/table/TaskLogsTable.js
@@ -1,801 +1,2 @@
-import React, { useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import {
- Music,
- FileText,
- HelpCircle,
- CheckCircle,
- Pause,
- Clock,
- Play,
- XCircle,
- Loader,
- List,
- Hash,
- Video,
- Sparkles
-} from 'lucide-react';
-import {
- API,
- copy,
- isAdmin,
- showError,
- showSuccess,
- timestamp2string
-} from '../../helpers';
-
-import {
- Button,
- Checkbox,
- Empty,
- Form,
- Layout,
- Modal,
- Progress,
- Table,
- Tag,
- Typography
-} from '@douyinfe/semi-ui';
-import CardPro from '../common/ui/CardPro';
-import {
- IllustrationNoResult,
- IllustrationNoResultDark
-} from '@douyinfe/semi-illustrations';
-import { ITEMS_PER_PAGE } from '../../constants';
-import {
- IconEyeOpened,
- IconSearch,
-} from '@douyinfe/semi-icons';
-import { useTableCompactMode } from '../../hooks/common/useTableCompactMode';
-import { TASK_ACTION_GENERATE, TASK_ACTION_TEXT_GENERATE } from '../../constants/common.constant';
-
-const { Text } = Typography;
-
-const colors = [
- 'amber',
- 'blue',
- 'cyan',
- 'green',
- 'grey',
- 'indigo',
- 'light-blue',
- 'lime',
- 'orange',
- 'pink',
- 'purple',
- 'red',
- 'teal',
- 'violet',
- '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',
- RESULT_URL: 'result_url',
-};
-
-const renderTimestamp = (timestampInSeconds) => {
- const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
-
- const year = date.getFullYear(); // 获取年份
- const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
- const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
- const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
- const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
- const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
-
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
-};
-
-function renderDuration(submit_time, finishTime) {
- if (!submit_time || !finishTime) return 'N/A';
- const durationSec = finishTime - submit_time;
- const color = durationSec > 60 ? 'red' : 'green';
-
- // 返回带有样式的颜色标签
- return (
- }>
- {durationSec} 秒
-
- );
-}
-
-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 [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,
- [COLUMN_KEYS.RESULT_URL]: 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('生成歌词')}
-
- );
- case TASK_ACTION_GENERATE:
- return (
- }>
- {t('图生视频')}
-
- );
- case TASK_ACTION_TEXT_GENERATE:
- return (
- }>
- {t('文生视频')}
-
- );
- default:
- return (
- }>
- {t('未知')}
-
- );
- }
- };
-
- const renderPlatform = (platform) => {
- switch (platform) {
- case 'suno':
- return (
- }>
- Suno
-
- );
- case 'kling':
- return (
- }>
- Kling
-
- );
- case 'jimeng':
- return (
- }>
- Jimeng
-
- );
- 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 = [
- {
- key: COLUMN_KEYS.SUBMIT_TIME,
- title: t('提交时间'),
- dataIndex: 'submit_time',
- render: (text, record, index) => {
- return
{text ? renderTimestamp(text) : '-'}
;
- },
- },
- {
- key: COLUMN_KEYS.FINISH_TIME,
- title: t('结束时间'),
- dataIndex: 'finish_time',
- render: (text, record, index) => {
- return {text ? renderTimestamp(text) : '-'}
;
- },
- },
- {
- key: COLUMN_KEYS.DURATION,
- title: t('花费时间'),
- dataIndex: 'finish_time',
- render: (finish, record) => {
- return <>{finish ? renderDuration(record.submit_time, finish) : '-'}>;
- },
- },
- {
- key: COLUMN_KEYS.CHANNEL,
- title: t('渠道'),
- dataIndex: 'channel_id',
- className: isAdminUser ? 'tableShow' : 'tableHiddle',
- render: (text, record, index) => {
- return isAdminUser ? (
-
- }
- onClick={() => {
- copyText(text);
- }}
- >
- {text}
-
-
- ) : (
- <>>
- );
- },
- },
- {
- key: COLUMN_KEYS.PLATFORM,
- title: t('平台'),
- dataIndex: 'platform',
- render: (text, record, index) => {
- return {renderPlatform(text)}
;
- },
- },
- {
- key: COLUMN_KEYS.TYPE,
- title: t('类型'),
- dataIndex: 'action',
- render: (text, record, index) => {
- return {renderType(text)}
;
- },
- },
- {
- key: COLUMN_KEYS.TASK_ID,
- title: t('任务ID'),
- dataIndex: 'task_id',
- render: (text, record, index) => {
- return (
- {
- setModalContent(JSON.stringify(record, null, 2));
- setIsModalOpen(true);
- }}
- >
- {text}
-
- );
- },
- },
- {
- key: COLUMN_KEYS.TASK_STATUS,
- title: t('任务状态'),
- dataIndex: 'status',
- render: (text, record, index) => {
- return {renderStatus(text)}
;
- },
- },
- {
- 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',
- fixed: 'right',
- render: (text, record, index) => {
- // 仅当为视频生成任务且成功,且 fail_reason 是 URL 时显示可点击链接
- const isVideoTask = record.action === TASK_ACTION_GENERATE || record.action === TASK_ACTION_TEXT_GENERATE;
- const isSuccess = record.status === 'SUCCESS';
- const isUrl = typeof text === 'string' && /^https?:\/\//.test(text);
- if (isSuccess && isVideoTask && isUrl) {
- return (
-
- {t('点击预览视频')}
-
- );
- }
- if (!text) {
- return t('无');
- }
- return (
- {
- setModalContent(text);
- setIsModalOpen(true);
- }}
- >
- {text}
-
- );
- },
- },
- ];
-
- // 根据可见性设置过滤列
- const getVisibleColumns = () => {
- return allColumns.filter((column) => visibleColumns[column.key]);
- };
-
- const [activePage, setActivePage] = useState(1);
- const [logCount, setLogCount] = useState(0);
- const [logs, setLogs] = useState([]);
- const [loading, setLoading] = useState(false);
-
- const [compactMode, setCompactMode] = useTableCompactMode('taskLogs');
-
- useEffect(() => {
- const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
- setPageSize(localPageSize);
- loadLogs(1, localPageSize).then();
- }, []);
-
- let now = new Date();
- // 初始化start_timestamp为前一天
- let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
-
- // Form 初始值
- const formInitValues = {
- channel_id: '',
- task_id: '',
- dateRange: [
- timestamp2string(zeroNow.getTime() / 1000),
- timestamp2string(now.getTime() / 1000 + 3600)
- ],
- };
-
- // Form API 引用
- const [formApi, setFormApi] = useState(null);
-
- // 获取表单值的辅助函数
- const getFormValues = () => {
- const formValues = formApi ? formApi.getValues() : {};
-
- // 处理时间范围
- let start_timestamp = timestamp2string(zeroNow.getTime() / 1000);
- 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 {
- channel_id: formValues.channel_id || '',
- task_id: formValues.task_id || '',
- start_timestamp,
- end_timestamp,
- };
- };
-
- const enrichLogs = (items) => {
- return items.map((log) => ({
- ...log,
- timestamp2string: timestamp2string(log.created_at),
- key: '' + log.id,
- }));
- };
-
- const syncPageData = (payload) => {
- const items = enrichLogs(payload.items || []);
- setLogs(items);
- setLogCount(payload.total || 0);
- setActivePage(payload.page || 1);
- setPageSize(payload.page_size || pageSize);
- };
-
- const loadLogs = async (page = 1, size = pageSize) => {
- setLoading(true);
- const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues();
- let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
- let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
- let url = isAdminUser
- ? `/api/task/?p=${page}&page_size=${size}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
- : `/api/task/self?p=${page}&page_size=${size}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
- const res = await API.get(url);
- const { success, message, data } = res.data;
- if (success) {
- syncPageData(data);
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- const pageData = logs;
-
- const handlePageChange = (page) => {
- loadLogs(page, pageSize).then();
- };
-
- const handlePageSizeChange = async (size) => {
- localStorage.setItem('task-page-size', size + '');
- await loadLogs(1, size);
- };
-
- const refresh = async () => {
- await loadLogs(1, pageSize);
- };
-
- const copyText = async (text) => {
- if (await copy(text)) {
- showSuccess(t('已复制:') + text);
- } else {
- Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
- }
- };
-
- // 列选择器模态框
- 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;
- }
-
- return (
-
-
- handleColumnVisibilityChange(column.key, e.target.checked)
- }
- >
- {column.title}
-
-
- );
- })}
-
-
- );
- };
-
- return (
- <>
- {renderColumnSelector()}
-
-
-
-
- {t('任务记录')}
-
-
-
- }
- searchArea={
-
- }
- >
- rest) : getVisibleColumns()}
- dataSource={logs}
- rowKey='key'
- loading={loading}
- scroll={compactMode ? undefined : { x: 'max-content' }}
- className="rounded-xl overflow-hidden"
- size="middle"
- empty={
- }
- darkModeImage={}
- description={t('搜索无结果')}
- style={{ padding: 30 }}
- />
- }
- pagination={{
- currentPage: activePage,
- pageSize: pageSize,
- total: logCount,
- pageSizeOptions: [10, 20, 50, 100],
- showSizeChanger: true,
- onPageSizeChange: handlePageSizeChange,
- onPageChange: handlePageChange,
- }}
- />
-
-
- setIsModalOpen(false)}
- onCancel={() => setIsModalOpen(false)}
- closable={null}
- bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
- width={800} // 设置模态框宽度
- >
- {modalContent}
-
-
- >
- );
-};
-
-export default LogsTable;
+// 重构后的 TaskLogsTable - 使用新的模块化架构
+export { default } from './task-logs/index.jsx';
\ No newline at end of file
diff --git a/web/src/components/table/task-logs/TaskLogsActions.jsx b/web/src/components/table/task-logs/TaskLogsActions.jsx
new file mode 100644
index 00000000..0e1cec11
--- /dev/null
+++ b/web/src/components/table/task-logs/TaskLogsActions.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { Button, Typography } from '@douyinfe/semi-ui';
+import { IconEyeOpened } from '@douyinfe/semi-icons';
+
+const { Text } = Typography;
+
+const TaskLogsActions = ({
+ compactMode,
+ setCompactMode,
+ t,
+}) => {
+ return (
+
+
+
+ {t('任务记录')}
+
+
+
+ );
+};
+
+export default TaskLogsActions;
\ No newline at end of file
diff --git a/web/src/components/table/task-logs/TaskLogsColumnDefs.js b/web/src/components/table/task-logs/TaskLogsColumnDefs.js
new file mode 100644
index 00000000..92936abc
--- /dev/null
+++ b/web/src/components/table/task-logs/TaskLogsColumnDefs.js
@@ -0,0 +1,351 @@
+import React from 'react';
+import {
+ Progress,
+ Tag,
+ Typography
+} from '@douyinfe/semi-ui';
+import {
+ Music,
+ FileText,
+ HelpCircle,
+ CheckCircle,
+ Pause,
+ Clock,
+ Play,
+ XCircle,
+ Loader,
+ List,
+ Hash,
+ Video,
+ Sparkles
+} from 'lucide-react';
+import { TASK_ACTION_GENERATE, TASK_ACTION_TEXT_GENERATE } from '../../../constants/common.constant';
+
+const colors = [
+ 'amber',
+ 'blue',
+ 'cyan',
+ 'green',
+ 'grey',
+ 'indigo',
+ 'light-blue',
+ 'lime',
+ 'orange',
+ 'pink',
+ 'purple',
+ 'red',
+ 'teal',
+ 'violet',
+ 'yellow',
+];
+
+// Render functions
+const renderTimestamp = (timestampInSeconds) => {
+ const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
+
+ const year = date.getFullYear(); // 获取年份
+ const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
+ const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
+ const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
+ const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
+ const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
+
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
+};
+
+function renderDuration(submit_time, finishTime) {
+ if (!submit_time || !finishTime) return 'N/A';
+ const durationSec = finishTime - submit_time;
+ const color = durationSec > 60 ? 'red' : 'green';
+
+ // 返回带有样式的颜色标签
+ return (
+ }>
+ {durationSec} 秒
+
+ );
+}
+
+const renderType = (type, t) => {
+ switch (type) {
+ case 'MUSIC':
+ return (
+ }>
+ {t('生成音乐')}
+
+ );
+ case 'LYRICS':
+ return (
+ }>
+ {t('生成歌词')}
+
+ );
+ case TASK_ACTION_GENERATE:
+ return (
+ }>
+ {t('图生视频')}
+
+ );
+ case TASK_ACTION_TEXT_GENERATE:
+ return (
+ }>
+ {t('文生视频')}
+
+ );
+ default:
+ return (
+ }>
+ {t('未知')}
+
+ );
+ }
+};
+
+const renderPlatform = (platform, t) => {
+ switch (platform) {
+ case 'suno':
+ return (
+ }>
+ Suno
+
+ );
+ case 'kling':
+ return (
+ }>
+ Kling
+
+ );
+ case 'jimeng':
+ return (
+ }>
+ Jimeng
+
+ );
+ default:
+ return (
+ }>
+ {t('未知')}
+
+ );
+ }
+};
+
+const renderStatus = (type, t) => {
+ 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('未知')}
+
+ );
+ }
+};
+
+export const getTaskLogsColumns = ({
+ t,
+ COLUMN_KEYS,
+ copyText,
+ openContentModal,
+ isAdminUser,
+}) => {
+ return [
+ {
+ key: COLUMN_KEYS.SUBMIT_TIME,
+ title: t('提交时间'),
+ dataIndex: 'submit_time',
+ render: (text, record, index) => {
+ return {text ? renderTimestamp(text) : '-'}
;
+ },
+ },
+ {
+ key: COLUMN_KEYS.FINISH_TIME,
+ title: t('结束时间'),
+ dataIndex: 'finish_time',
+ render: (text, record, index) => {
+ return {text ? renderTimestamp(text) : '-'}
;
+ },
+ },
+ {
+ key: COLUMN_KEYS.DURATION,
+ title: t('花费时间'),
+ dataIndex: 'finish_time',
+ render: (finish, record) => {
+ return <>{finish ? renderDuration(record.submit_time, finish) : '-'}>;
+ },
+ },
+ {
+ key: COLUMN_KEYS.CHANNEL,
+ title: t('渠道'),
+ dataIndex: 'channel_id',
+ render: (text, record, index) => {
+ return isAdminUser ? (
+
+ }
+ onClick={() => {
+ copyText(text);
+ }}
+ >
+ {text}
+
+
+ ) : (
+ <>>
+ );
+ },
+ },
+ {
+ key: COLUMN_KEYS.PLATFORM,
+ title: t('平台'),
+ dataIndex: 'platform',
+ render: (text, record, index) => {
+ return {renderPlatform(text, t)}
;
+ },
+ },
+ {
+ key: COLUMN_KEYS.TYPE,
+ title: t('类型'),
+ dataIndex: 'action',
+ render: (text, record, index) => {
+ return {renderType(text, t)}
;
+ },
+ },
+ {
+ key: COLUMN_KEYS.TASK_ID,
+ title: t('任务ID'),
+ dataIndex: 'task_id',
+ render: (text, record, index) => {
+ return (
+ {
+ openContentModal(JSON.stringify(record, null, 2));
+ }}
+ >
+ {text}
+
+ );
+ },
+ },
+ {
+ key: COLUMN_KEYS.TASK_STATUS,
+ title: t('任务状态'),
+ dataIndex: 'status',
+ render: (text, record, index) => {
+ return {renderStatus(text, t)}
;
+ },
+ },
+ {
+ 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',
+ fixed: 'right',
+ render: (text, record, index) => {
+ // 仅当为视频生成任务且成功,且 fail_reason 是 URL 时显示可点击链接
+ const isVideoTask = record.action === TASK_ACTION_GENERATE || record.action === TASK_ACTION_TEXT_GENERATE;
+ const isSuccess = record.status === 'SUCCESS';
+ const isUrl = typeof text === 'string' && /^https?:\/\//.test(text);
+ if (isSuccess && isVideoTask && isUrl) {
+ return (
+
+ {t('点击预览视频')}
+
+ );
+ }
+ if (!text) {
+ return t('无');
+ }
+ return (
+ {
+ openContentModal(text);
+ }}
+ >
+ {text}
+
+ );
+ },
+ },
+ ];
+};
\ No newline at end of file
diff --git a/web/src/components/table/task-logs/TaskLogsFilters.jsx b/web/src/components/table/task-logs/TaskLogsFilters.jsx
new file mode 100644
index 00000000..509f57b7
--- /dev/null
+++ b/web/src/components/table/task-logs/TaskLogsFilters.jsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import { Button, Form } from '@douyinfe/semi-ui';
+import { IconSearch } from '@douyinfe/semi-icons';
+
+const TaskLogsFilters = ({
+ formInitValues,
+ setFormApi,
+ refresh,
+ setShowColumnSelector,
+ formApi,
+ loading,
+ isAdminUser,
+ t,
+}) => {
+ return (
+
+ );
+};
+
+export default TaskLogsFilters;
\ No newline at end of file
diff --git a/web/src/components/table/task-logs/TaskLogsTable.jsx b/web/src/components/table/task-logs/TaskLogsTable.jsx
new file mode 100644
index 00000000..b9ec6cb6
--- /dev/null
+++ b/web/src/components/table/task-logs/TaskLogsTable.jsx
@@ -0,0 +1,93 @@
+import React, { useMemo } from 'react';
+import { Table, Empty } from '@douyinfe/semi-ui';
+import {
+ IllustrationNoResult,
+ IllustrationNoResultDark,
+} from '@douyinfe/semi-illustrations';
+import { getTaskLogsColumns } from './TaskLogsColumnDefs.js';
+
+const TaskLogsTable = (taskLogsData) => {
+ const {
+ logs,
+ loading,
+ activePage,
+ pageSize,
+ logCount,
+ compactMode,
+ visibleColumns,
+ handlePageChange,
+ handlePageSizeChange,
+ copyText,
+ openContentModal,
+ isAdminUser,
+ t,
+ COLUMN_KEYS,
+ } = taskLogsData;
+
+ // Get all columns
+ const allColumns = useMemo(() => {
+ return getTaskLogsColumns({
+ t,
+ COLUMN_KEYS,
+ copyText,
+ openContentModal,
+ isAdminUser,
+ });
+ }, [
+ t,
+ COLUMN_KEYS,
+ copyText,
+ openContentModal,
+ isAdminUser,
+ ]);
+
+ // Filter columns based on visibility settings
+ const getVisibleColumns = () => {
+ return allColumns.filter((column) => visibleColumns[column.key]);
+ };
+
+ const visibleColumnsList = useMemo(() => {
+ return getVisibleColumns();
+ }, [visibleColumns, allColumns]);
+
+ const tableColumns = useMemo(() => {
+ return compactMode
+ ? visibleColumnsList.map(({ fixed, ...rest }) => rest)
+ : visibleColumnsList;
+ }, [compactMode, visibleColumnsList]);
+
+ return (
+
+ }
+ darkModeImage={
+
+ }
+ description={t('搜索无结果')}
+ style={{ padding: 30 }}
+ />
+ }
+ pagination={{
+ currentPage: activePage,
+ pageSize: pageSize,
+ total: logCount,
+ pageSizeOptions: [10, 20, 50, 100],
+ showSizeChanger: true,
+ onPageSizeChange: handlePageSizeChange,
+ onPageChange: handlePageChange,
+ }}
+ />
+ );
+};
+
+export default TaskLogsTable;
\ No newline at end of file
diff --git a/web/src/components/table/task-logs/index.jsx b/web/src/components/table/task-logs/index.jsx
new file mode 100644
index 00000000..f0c2b1b7
--- /dev/null
+++ b/web/src/components/table/task-logs/index.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Layout } from '@douyinfe/semi-ui';
+import CardPro from '../../common/ui/CardPro.js';
+import TaskLogsTable from './TaskLogsTable.jsx';
+import TaskLogsActions from './TaskLogsActions.jsx';
+import TaskLogsFilters from './TaskLogsFilters.jsx';
+import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx';
+import ContentModal from './modals/ContentModal.jsx';
+import { useTaskLogsData } from '../../../hooks/task-logs/useTaskLogsData.js';
+
+const TaskLogsPage = () => {
+ const taskLogsData = useTaskLogsData();
+
+ return (
+ <>
+ {/* Modals */}
+
+
+
+
+ }
+ searchArea={}
+ >
+
+
+
+ >
+ );
+};
+
+export default TaskLogsPage;
\ No newline at end of file
diff --git a/web/src/components/table/task-logs/modals/ColumnSelectorModal.jsx b/web/src/components/table/task-logs/modals/ColumnSelectorModal.jsx
new file mode 100644
index 00000000..23624a72
--- /dev/null
+++ b/web/src/components/table/task-logs/modals/ColumnSelectorModal.jsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { Modal, Button, Checkbox } from '@douyinfe/semi-ui';
+import { getTaskLogsColumns } from '../TaskLogsColumnDefs.js';
+
+const ColumnSelectorModal = ({
+ showColumnSelector,
+ setShowColumnSelector,
+ visibleColumns,
+ handleColumnVisibilityChange,
+ handleSelectAll,
+ initDefaultColumns,
+ COLUMN_KEYS,
+ isAdminUser,
+ copyText,
+ openContentModal,
+ t,
+}) => {
+ // Get all columns for display in selector
+ const allColumns = getTaskLogsColumns({
+ t,
+ COLUMN_KEYS,
+ copyText,
+ openContentModal,
+ isAdminUser,
+ });
+
+ 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) => {
+ // Skip admin-only columns for non-admin users
+ if (!isAdminUser && column.key === COLUMN_KEYS.CHANNEL) {
+ return null;
+ }
+
+ return (
+
+
+ handleColumnVisibilityChange(column.key, e.target.checked)
+ }
+ >
+ {column.title}
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default ColumnSelectorModal;
\ No newline at end of file
diff --git a/web/src/components/table/task-logs/modals/ContentModal.jsx b/web/src/components/table/task-logs/modals/ContentModal.jsx
new file mode 100644
index 00000000..f82baf90
--- /dev/null
+++ b/web/src/components/table/task-logs/modals/ContentModal.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { Modal } from '@douyinfe/semi-ui';
+
+const ContentModal = ({
+ isModalOpen,
+ setIsModalOpen,
+ modalContent,
+}) => {
+ return (
+ setIsModalOpen(false)}
+ onCancel={() => setIsModalOpen(false)}
+ closable={null}
+ bodyStyle={{ height: '400px', overflow: 'auto' }}
+ width={800}
+ >
+ {modalContent}
+
+ );
+};
+
+export default ContentModal;
\ No newline at end of file
diff --git a/web/src/hooks/task-logs/useTaskLogsData.js b/web/src/hooks/task-logs/useTaskLogsData.js
new file mode 100644
index 00000000..64f1cc93
--- /dev/null
+++ b/web/src/hooks/task-logs/useTaskLogsData.js
@@ -0,0 +1,280 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Modal } from '@douyinfe/semi-ui';
+import {
+ API,
+ copy,
+ isAdmin,
+ showError,
+ showSuccess,
+ timestamp2string
+} from '../../helpers';
+import { ITEMS_PER_PAGE } from '../../constants';
+import { useTableCompactMode } from '../common/useTableCompactMode';
+
+export const useTaskLogsData = () => {
+ const { t } = useTranslation();
+
+ // Define column keys for selection
+ 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',
+ RESULT_URL: 'result_url',
+ };
+
+ // Basic state
+ const [logs, setLogs] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [activePage, setActivePage] = useState(1);
+ const [logCount, setLogCount] = useState(0);
+ const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
+
+ // User and admin
+ const isAdminUser = isAdmin();
+
+ // Modal state
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [modalContent, setModalContent] = useState('');
+
+ // Form state
+ const [formApi, setFormApi] = useState(null);
+ let now = new Date();
+ let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+
+ const formInitValues = {
+ channel_id: '',
+ task_id: '',
+ dateRange: [
+ timestamp2string(zeroNow.getTime() / 1000),
+ timestamp2string(now.getTime() / 1000 + 3600)
+ ],
+ };
+
+ // Column visibility state
+ const [visibleColumns, setVisibleColumns] = useState({});
+ const [showColumnSelector, setShowColumnSelector] = useState(false);
+
+ // Compact mode
+ const [compactMode, setCompactMode] = useTableCompactMode('taskLogs');
+
+ // Load saved column preferences from localStorage
+ 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();
+ }
+ }, []);
+
+ // Get default column visibility based on user role
+ 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,
+ [COLUMN_KEYS.RESULT_URL]: true,
+ };
+ };
+
+ // Initialize default column visibility
+ const initDefaultColumns = () => {
+ const defaults = getDefaultColumnVisibility();
+ setVisibleColumns(defaults);
+ localStorage.setItem('task-logs-table-columns', JSON.stringify(defaults));
+ };
+
+ // Handle column visibility change
+ const handleColumnVisibilityChange = (columnKey, checked) => {
+ const updatedColumns = { ...visibleColumns, [columnKey]: checked };
+ setVisibleColumns(updatedColumns);
+ };
+
+ // Handle "Select All" checkbox
+ 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);
+ };
+
+ // Update table when column visibility changes
+ useEffect(() => {
+ if (Object.keys(visibleColumns).length > 0) {
+ localStorage.setItem('task-logs-table-columns', JSON.stringify(visibleColumns));
+ }
+ }, [visibleColumns]);
+
+ // Get form values helper function
+ const getFormValues = () => {
+ const formValues = formApi ? formApi.getValues() : {};
+
+ // 处理时间范围
+ let start_timestamp = timestamp2string(zeroNow.getTime() / 1000);
+ 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 {
+ channel_id: formValues.channel_id || '',
+ task_id: formValues.task_id || '',
+ start_timestamp,
+ end_timestamp,
+ };
+ };
+
+ // Enrich logs data
+ const enrichLogs = (items) => {
+ return items.map((log) => ({
+ ...log,
+ timestamp2string: timestamp2string(log.created_at),
+ key: '' + log.id,
+ }));
+ };
+
+ // Sync page data
+ const syncPageData = (payload) => {
+ const items = enrichLogs(payload.items || []);
+ setLogs(items);
+ setLogCount(payload.total || 0);
+ setActivePage(payload.page || 1);
+ setPageSize(payload.page_size || pageSize);
+ };
+
+ // Load logs function
+ const loadLogs = async (page = 1, size = pageSize) => {
+ setLoading(true);
+ const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues();
+ let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
+ let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
+ let url = isAdminUser
+ ? `/api/task/?p=${page}&page_size=${size}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
+ : `/api/task/self?p=${page}&page_size=${size}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
+ const res = await API.get(url);
+ const { success, message, data } = res.data;
+ if (success) {
+ syncPageData(data);
+ } else {
+ showError(message);
+ }
+ setLoading(false);
+ };
+
+ // Page handlers
+ const handlePageChange = (page) => {
+ loadLogs(page, pageSize).then();
+ };
+
+ const handlePageSizeChange = async (size) => {
+ localStorage.setItem('task-page-size', size + '');
+ await loadLogs(1, size);
+ };
+
+ // Refresh function
+ const refresh = async () => {
+ await loadLogs(1, pageSize);
+ };
+
+ // Copy text function
+ const copyText = async (text) => {
+ if (await copy(text)) {
+ showSuccess(t('已复制:') + text);
+ } else {
+ Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
+ }
+ };
+
+ // Modal handlers
+ const openContentModal = (content) => {
+ setModalContent(content);
+ setIsModalOpen(true);
+ };
+
+ // Initialize data
+ useEffect(() => {
+ const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
+ setPageSize(localPageSize);
+ loadLogs(1, localPageSize).then();
+ }, []);
+
+ return {
+ // Basic state
+ logs,
+ loading,
+ activePage,
+ logCount,
+ pageSize,
+ isAdminUser,
+
+ // Modal state
+ isModalOpen,
+ setIsModalOpen,
+ modalContent,
+
+ // Form state
+ formApi,
+ setFormApi,
+ formInitValues,
+ getFormValues,
+
+ // Column visibility
+ visibleColumns,
+ showColumnSelector,
+ setShowColumnSelector,
+ handleColumnVisibilityChange,
+ handleSelectAll,
+ initDefaultColumns,
+ COLUMN_KEYS,
+
+ // Compact mode
+ compactMode,
+ setCompactMode,
+
+ // Functions
+ loadLogs,
+ handlePageChange,
+ handlePageSizeChange,
+ refresh,
+ copyText,
+ openContentModal,
+ enrichLogs,
+ syncPageData,
+
+ // Translation
+ t,
+ };
+};
\ No newline at end of file
diff --git a/web/src/pages/Log/index.js b/web/src/pages/Log/index.js
index fa919964..f4bed060 100644
--- a/web/src/pages/Log/index.js
+++ b/web/src/pages/Log/index.js
@@ -1,9 +1,9 @@
import React from 'react';
-import LogsTable from '../../components/table/LogsTable';
+import UsageLogsTable from '../../components/table/UsageLogsTable';
const Token = () => (
-
+
);