Commit 51695bef by t0ng7u

️ refactor: restructure MjLogsTable into modular component architecture

Refactor the monolithic MjLogsTable component (971 lines) into a modular,
maintainable architecture following the same pattern as LogsTable refactor.

## What Changed

### 🏗️ Architecture
- Split large single file into focused, single-responsibility components
- Introduced custom hook `useMjLogsData` for centralized state management
- Created dedicated column definitions file for better organization
- Implemented specialized modal components for Midjourney-specific features

### 📁 New Structure
```
web/src/components/table/mj-logs/
├── index.jsx                    # Main page component orchestrator
├── MjLogsTable.jsx             # Pure table rendering component
├── MjLogsActions.jsx           # Actions area (banner + compact mode)
├── MjLogsFilters.jsx           # Search form component
├── MjLogsColumnDefs.js         # Column definitions and renderers
└── modals/
    ├── ColumnSelectorModal.jsx # Column visibility settings
    └── ContentModal.jsx        # Content viewer (text + image preview)

web/src/hooks/mj-logs/
└── useMjLogsData.js            # Custom hook for state & logic
```

### 🎯 Key Improvements
- **Maintainability**: Clear separation of concerns, easier to understand
- **Reusability**: Modular components can be reused independently
- **Performance**: Optimized with `useMemo` for column rendering
- **Testing**: Single-responsibility components easier to test
- **Developer Experience**: Better code organization and readability

### 🎨 Midjourney-Specific Features Preserved
- All task type rendering with icons (IMAGINE, UPSCALE, VARIATION, etc.)
- Status rendering with appropriate colors and icons
- Image preview functionality for generated artwork
- Progress indicators for task completion
- Admin-only columns for channel and submission results
- Banner notification system for callback settings

### 🔧 Technical Details
- Centralized all business logic in `useMjLogsData` custom hook
- Extracted comprehensive column definitions with Lucide React icons
- Split complex UI into focused components (table, actions, filters, modals)
- Maintained responsive design patterns for mobile compatibility
- Preserved admin permission handling for restricted features

### 🐛 Fixes
- Improved component prop passing patterns
- Enhanced type safety through better state management
- Optimized rendering performance with proper memoization

## Breaking Changes
None - all existing imports and functionality preserved.
parent acf6ec93
import React, { useEffect, useState } from 'react'; // 重构后的 MjLogsTable - 使用新的模块化架构
import { useTranslation } from 'react-i18next'; export { default } from './mj-logs/index.jsx';
import { \ No newline at end of file
Palette,
ZoomIn,
Shuffle,
Move,
FileText,
Blend,
Upload,
Minimize2,
RotateCcw,
PaintBucket,
Focus,
Move3D,
Monitor,
UserCheck,
HelpCircle,
CheckCircle,
Clock,
Copy,
FileX,
Pause,
XCircle,
Loader,
AlertCircle,
Hash,
} from 'lucide-react';
import {
API,
copy,
isAdmin,
showError,
showSuccess,
timestamp2string
} from '../../helpers';
import {
Button,
Checkbox,
Empty,
Form,
ImagePreview,
Layout,
Modal,
Progress,
Skeleton,
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';
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',
DURATION: 'duration',
CHANNEL: 'channel',
TYPE: 'type',
TASK_ID: 'task_id',
SUBMIT_RESULT: 'submit_result',
TASK_STATUS: 'task_status',
PROGRESS: 'progress',
IMAGE: 'image',
PROMPT: 'prompt',
PROMPT_EN: 'prompt_en',
FAIL_REASON: 'fail_reason',
};
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 [compactMode, setCompactMode] = useTableCompactMode('mjLogs');
// 加载保存的列偏好设置
useEffect(() => {
const savedColumns = localStorage.getItem('mj-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.DURATION]: true,
[COLUMN_KEYS.CHANNEL]: isAdminUser,
[COLUMN_KEYS.TYPE]: true,
[COLUMN_KEYS.TASK_ID]: true,
[COLUMN_KEYS.SUBMIT_RESULT]: isAdminUser,
[COLUMN_KEYS.TASK_STATUS]: true,
[COLUMN_KEYS.PROGRESS]: true,
[COLUMN_KEYS.IMAGE]: true,
[COLUMN_KEYS.PROMPT]: true,
[COLUMN_KEYS.PROMPT_EN]: true,
[COLUMN_KEYS.FAIL_REASON]: true,
};
};
// 初始化默认列可见性
const initDefaultColumns = () => {
const defaults = getDefaultColumnVisibility();
setVisibleColumns(defaults);
localStorage.setItem('mj-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 || key === COLUMN_KEYS.SUBMIT_RESULT) && !isAdminUser) {
updatedColumns[key] = false;
} else {
updatedColumns[key] = checked;
}
});
setVisibleColumns(updatedColumns);
};
// 更新表格时保存列可见性
useEffect(() => {
if (Object.keys(visibleColumns).length > 0) {
localStorage.setItem('mj-logs-table-columns', JSON.stringify(visibleColumns));
}
}, [visibleColumns]);
function renderType(type) {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' shape='circle' prefixIcon={<Palette size={14} />}>
{t('绘图')}
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' shape='circle' prefixIcon={<ZoomIn size={14} />}>
{t('放大')}
</Tag>
);
case 'VIDEO':
return (
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('视频')}
</Tag>
);
case 'EDITS':
return (
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('编辑')}
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('变换')}
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('强变换')}
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('弱变换')}
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' shape='circle' prefixIcon={<Move size={14} />}>
{t('平移')}
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' shape='circle' prefixIcon={<FileText size={14} />}>
{t('图生文')}
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' shape='circle' prefixIcon={<Blend size={14} />}>
{t('图混合')}
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' shape='circle' prefixIcon={<Upload size={14} />}>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' shape='circle' prefixIcon={<Minimize2 size={14} />}>
{t('缩词')}
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' shape='circle' prefixIcon={<RotateCcw size={14} />}>
{t('重绘')}
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' shape='circle' prefixIcon={<PaintBucket size={14} />}>
{t('局部重绘-提交')}
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' shape='circle' prefixIcon={<Focus size={14} />}>
{t('变焦')}
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' shape='circle' prefixIcon={<Move3D size={14} />}>
{t('自定义变焦-提交')}
</Tag>
);
case 'MODAL':
return (
<Tag color='green' shape='circle' prefixIcon={<Monitor size={14} />}>
{t('窗口处理')}
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' shape='circle' prefixIcon={<UserCheck size={14} />}>
{t('换脸')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
}
}
function renderCode(code) {
switch (code) {
case 1:
return (
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('已提交')}
</Tag>
);
case 21:
return (
<Tag color='lime' shape='circle' prefixIcon={<Clock size={14} />}>
{t('等待中')}
</Tag>
);
case 22:
return (
<Tag color='orange' shape='circle' prefixIcon={<Copy size={14} />}>
{t('重复提交')}
</Tag>
);
case 0:
return (
<Tag color='yellow' shape='circle' prefixIcon={<FileX size={14} />}>
{t('未提交')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
}
}
function renderStatus(type) {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('成功')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
{t('未启动')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
{t('队列中')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' shape='circle' prefixIcon={<Loader size={14} />}>
{t('执行中')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('失败')}
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
{t('窗口等待')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
}
}
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}`; // 格式化输出
};
// 修改renderDuration函数以包含颜色逻辑
function renderDuration(submit_time, finishTime) {
if (!submit_time || !finishTime) return 'N/A';
const start = new Date(submit_time);
const finish = new Date(finishTime);
const durationMs = finish - start;
const durationSec = (durationMs / 1000).toFixed(1);
const color = durationSec > 60 ? 'red' : 'green';
return (
<Tag color={color} shape='circle' prefixIcon={<Clock size={14} />}>
{durationSec} {t('秒')}
</Tag>
);
}
// 定义所有列
const allColumns = [
{
key: COLUMN_KEYS.SUBMIT_TIME,
title: t('提交时间'),
dataIndex: 'submit_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text / 1000)}</div>;
},
},
{
key: COLUMN_KEYS.DURATION,
title: t('花费时间'),
dataIndex: 'finish_time',
render: (finish, record) => {
return renderDuration(record.submit_time, finish);
},
},
{
key: COLUMN_KEYS.CHANNEL,
title: t('渠道'),
dataIndex: 'channel_id',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return isAdminUser ? (
<div>
<Tag
color={colors[parseInt(text) % colors.length]}
shape='circle'
prefixIcon={<Hash size={14} />}
onClick={() => {
copyText(text);
}}
>
{' '}
{text}{' '}
</Tag>
</div>
) : (
<></>
);
},
},
{
key: COLUMN_KEYS.TYPE,
title: t('类型'),
dataIndex: 'action',
render: (text, record, index) => {
return <div>{renderType(text)}</div>;
},
},
{
key: COLUMN_KEYS.TASK_ID,
title: t('任务ID'),
dataIndex: 'mj_id',
render: (text, record, index) => {
return <div>{text}</div>;
},
},
{
key: COLUMN_KEYS.SUBMIT_RESULT,
title: t('提交结果'),
dataIndex: 'code',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return isAdminUser ? <div>{renderCode(text)}</div> : <></>;
},
},
{
key: COLUMN_KEYS.TASK_STATUS,
title: t('任务状态'),
dataIndex: 'status',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return <div>{renderStatus(text)}</div>;
},
},
{
key: COLUMN_KEYS.PROGRESS,
title: t('进度'),
dataIndex: 'progress',
render: (text, record, index) => {
return (
<div>
{
<Progress
stroke={
record.status === 'FAILURE'
? 'var(--semi-color-warning)'
: null
}
percent={text ? parseInt(text.replace('%', '')) : 0}
showInfo={true}
aria-label='drawing progress'
style={{ minWidth: '160px' }}
/>
}
</div>
);
},
},
{
key: COLUMN_KEYS.IMAGE,
title: t('结果图片'),
dataIndex: 'image_url',
render: (text, record, index) => {
if (!text) {
return t('无');
}
return (
<Button
size="small"
onClick={() => {
setModalImageUrl(text);
setIsModalOpenurl(true);
}}
>
{t('查看图片')}
</Button>
);
},
},
{
key: COLUMN_KEYS.PROMPT,
title: 'Prompt',
dataIndex: 'prompt',
render: (text, record, index) => {
if (!text) {
return t('无');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
},
},
{
key: COLUMN_KEYS.PROMPT_EN,
title: 'PromptEn',
dataIndex: 'prompt_en',
render: (text, record, index) => {
if (!text) {
return t('无');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
},
},
{
key: COLUMN_KEYS.FAIL_REASON,
title: t('失败原因'),
dataIndex: 'fail_reason',
fixed: 'right',
render: (text, record, index) => {
if (!text) {
return t('无');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
},
},
];
// 根据可见性设置过滤列
const getVisibleColumns = () => {
return allColumns.filter((column) => visibleColumns[column.key]);
};
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [logCount, setLogCount] = useState(0);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
const [showBanner, setShowBanner] = useState(false);
// 定义模态框图片URL的状态和更新函数
const [modalImageUrl, setModalImageUrl] = useState('');
let now = new Date();
// Form 初始值
const formInitValues = {
channel_id: '',
mj_id: '',
dateRange: [
timestamp2string(now.getTime() / 1000 - 2592000),
timestamp2string(now.getTime() / 1000 + 3600)
],
};
// Form API 引用
const [formApi, setFormApi] = useState(null);
const [stat, setStat] = useState({
quota: 0,
token: 0,
});
// 获取表单值的辅助函数
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
// 处理时间范围
let start_timestamp = timestamp2string(now.getTime() / 1000 - 2592000);
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 || '',
mj_id: formValues.mj_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, mj_id, start_timestamp, end_timestamp } = getFormValues();
let localStartTimestamp = Date.parse(start_timestamp);
let localEndTimestamp = Date.parse(end_timestamp);
const url = isAdminUser
? `/api/mj/?p=${page}&page_size=${size}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
: `/api/mj/self/?p=${page}&page_size=${size}&mj_id=${mj_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('mj-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 {
// setSearchKeyword(text);
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
}
};
useEffect(() => {
const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
setPageSize(localPageSize);
loadLogs(1, localPageSize).then();
}, []);
useEffect(() => {
const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
if (mjNotifyEnabled !== 'true') {
setShowBanner(true);
}
}, []);
// 列选择器模态框
const renderColumnSelector = () => {
return (
<Modal
title={t('列设置')}
visible={showColumnSelector}
onCancel={() => setShowColumnSelector(false)}
footer={
<div className="flex justify-end">
<Button onClick={() => initDefaultColumns()}>
{t('重置')}
</Button>
<Button onClick={() => setShowColumnSelector(false)}>
{t('取消')}
</Button>
<Button onClick={() => setShowColumnSelector(false)}>
{t('确定')}
</Button>
</div>
}
>
<div style={{ marginBottom: 20 }}>
<Checkbox
checked={Object.values(visibleColumns).every((v) => v === true)}
indeterminate={
Object.values(visibleColumns).some((v) => v === true) &&
!Object.values(visibleColumns).every((v) => v === true)
}
onChange={(e) => handleSelectAll(e.target.checked)}
>
{t('全选')}
</Checkbox>
</div>
<div className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4" style={{ border: '1px solid var(--semi-color-border)' }}>
{allColumns.map((column) => {
// 为非管理员用户跳过管理员专用列
if (
!isAdminUser &&
(column.key === COLUMN_KEYS.CHANNEL ||
column.key === COLUMN_KEYS.SUBMIT_RESULT)
) {
return null;
}
return (
<div key={column.key} className="w-1/2 mb-4 pr-2">
<Checkbox
checked={!!visibleColumns[column.key]}
onChange={(e) =>
handleColumnVisibilityChange(column.key, e.target.checked)
}
>
{column.title}
</Checkbox>
</div>
);
})}
</div>
</Modal>
);
};
return (
<>
{renderColumnSelector()}
<Layout>
<CardPro
type="type2"
className="mb-4"
statsArea={
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
<IconEyeOpened className="mr-2" />
{loading ? (
<Skeleton.Title
style={{
width: 300,
marginBottom: 0,
marginTop: 0
}}
/>
) : (
<Text>
{isAdminUser && showBanner
? t('当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。')
: t('Midjourney 任务记录')}
</Text>
)}
</div>
<Button
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
</Button>
</div>
}
searchArea={
<Form
initValues={formInitValues}
getFormApi={(api) => setFormApi(api)}
onSubmit={refresh}
allowEmpty={true}
autoComplete="off"
layout="vertical"
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('结束时间')]}
showClear
pure
size="small"
/>
</div>
{/* 任务 ID */}
<Form.Input
field='mj_id'
prefix={<IconSearch />}
placeholder={t('任务 ID')}
showClear
pure
size="small"
/>
{/* 渠道 ID - 仅管理员可见 */}
{isAdminUser && (
<Form.Input
field='channel_id'
prefix={<IconSearch />}
placeholder={t('渠道 ID')}
showClear
pure
size="small"
/>
)}
</div>
{/* 操作按钮区域 */}
<div className="flex justify-between items-center">
<div></div>
<div className="flex gap-2">
<Button
type='tertiary'
htmlType='submit'
loading={loading}
size="small"
>
{t('查询')}
</Button>
<Button
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout(() => {
refresh();
}, 100);
}
}}
size="small"
>
{t('重置')}
</Button>
<Button
type='tertiary'
onClick={() => setShowColumnSelector(true)}
size="small"
>
{t('列设置')}
</Button>
</div>
</div>
</div>
</Form>
}
>
<Table
columns={compactMode ? getVisibleColumns().map(({ fixed, ...rest }) => rest) : getVisibleColumns()}
dataSource={logs}
rowKey='key'
loading={loading}
scroll={compactMode ? undefined : { x: 'max-content' }}
className="rounded-xl overflow-hidden"
size="middle"
empty={
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
description={t('搜索无结果')}
style={{ padding: 30 }}
/>
}
pagination={{
currentPage: activePage,
pageSize: pageSize,
total: logCount,
pageSizeOptions: [10, 20, 50, 100],
showSizeChanger: true,
onPageSizeChange: handlePageSizeChange,
onPageChange: handlePageChange,
}}
/>
</CardPro>
<Modal
visible={isModalOpen}
onOk={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
closable={null}
bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
width={800} // 设置模态框宽度
>
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
</Modal>
<ImagePreview
src={modalImageUrl}
visible={isModalOpenurl}
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
/>
</Layout>
</>
);
};
export default LogsTable;
// 重构后的 LogsTable - 使用新的模块化架构 // 重构后的 UsageLogsTable - 使用新的模块化架构
export { default } from './usage-logs/index.jsx'; export { default } from './usage-logs/index.jsx';
\ No newline at end of file
import React from 'react';
import { Button, Skeleton, Typography } from '@douyinfe/semi-ui';
import { IconEyeOpened } from '@douyinfe/semi-icons';
const { Text } = Typography;
const MjLogsActions = ({
loading,
showBanner,
isAdminUser,
compactMode,
setCompactMode,
t,
}) => {
return (
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
<IconEyeOpened className="mr-2" />
{loading ? (
<Skeleton.Title
style={{
width: 300,
marginBottom: 0,
marginTop: 0
}}
/>
) : (
<Text>
{isAdminUser && showBanner
? t('当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。')
: t('Midjourney 任务记录')}
</Text>
)}
</div>
<Button
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
</Button>
</div>
);
};
export default MjLogsActions;
\ No newline at end of file
import React from 'react';
import {
Button,
Progress,
Tag,
Typography
} from '@douyinfe/semi-ui';
import {
Palette,
ZoomIn,
Shuffle,
Move,
FileText,
Blend,
Upload,
Minimize2,
RotateCcw,
PaintBucket,
Focus,
Move3D,
Monitor,
UserCheck,
HelpCircle,
CheckCircle,
Clock,
Copy,
FileX,
Pause,
XCircle,
Loader,
AlertCircle,
Hash,
Video
} from 'lucide-react';
const colors = [
'amber',
'blue',
'cyan',
'green',
'grey',
'indigo',
'light-blue',
'lime',
'orange',
'pink',
'purple',
'red',
'teal',
'violet',
'yellow',
];
// Render functions
function renderType(type, t) {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' shape='circle' prefixIcon={<Palette size={14} />}>
{t('绘图')}
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' shape='circle' prefixIcon={<ZoomIn size={14} />}>
{t('放大')}
</Tag>
);
case 'VIDEO':
return (
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('视频')}
</Tag>
);
case 'EDITS':
return (
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('编辑')}
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('变换')}
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('强变换')}
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('弱变换')}
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' shape='circle' prefixIcon={<Move size={14} />}>
{t('平移')}
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' shape='circle' prefixIcon={<FileText size={14} />}>
{t('图生文')}
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' shape='circle' prefixIcon={<Blend size={14} />}>
{t('图混合')}
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' shape='circle' prefixIcon={<Upload size={14} />}>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' shape='circle' prefixIcon={<Minimize2 size={14} />}>
{t('缩词')}
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' shape='circle' prefixIcon={<RotateCcw size={14} />}>
{t('重绘')}
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' shape='circle' prefixIcon={<PaintBucket size={14} />}>
{t('局部重绘-提交')}
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' shape='circle' prefixIcon={<Focus size={14} />}>
{t('变焦')}
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' shape='circle' prefixIcon={<Move3D size={14} />}>
{t('自定义变焦-提交')}
</Tag>
);
case 'MODAL':
return (
<Tag color='green' shape='circle' prefixIcon={<Monitor size={14} />}>
{t('窗口处理')}
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' shape='circle' prefixIcon={<UserCheck size={14} />}>
{t('换脸')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
}
}
function renderCode(code, t) {
switch (code) {
case 1:
return (
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('已提交')}
</Tag>
);
case 21:
return (
<Tag color='lime' shape='circle' prefixIcon={<Clock size={14} />}>
{t('等待中')}
</Tag>
);
case 22:
return (
<Tag color='orange' shape='circle' prefixIcon={<Copy size={14} />}>
{t('重复提交')}
</Tag>
);
case 0:
return (
<Tag color='yellow' shape='circle' prefixIcon={<FileX size={14} />}>
{t('未提交')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
}
}
function renderStatus(type, t) {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('成功')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
{t('未启动')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
{t('队列中')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' shape='circle' prefixIcon={<Loader size={14} />}>
{t('执行中')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('失败')}
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
{t('窗口等待')}
</Tag>
);
default:
return (
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
}
}
const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000);
const year = date.getFullYear();
const month = ('0' + (date.getMonth() + 1)).slice(-2);
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, t) {
if (!submit_time || !finishTime) return 'N/A';
const start = new Date(submit_time);
const finish = new Date(finishTime);
const durationMs = finish - start;
const durationSec = (durationMs / 1000).toFixed(1);
const color = durationSec > 60 ? 'red' : 'green';
return (
<Tag color={color} shape='circle' prefixIcon={<Clock size={14} />}>
{durationSec} {t('秒')}
</Tag>
);
}
export const getMjLogsColumns = ({
t,
COLUMN_KEYS,
copyText,
openContentModal,
openImageModal,
isAdminUser,
}) => {
return [
{
key: COLUMN_KEYS.SUBMIT_TIME,
title: t('提交时间'),
dataIndex: 'submit_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text / 1000)}</div>;
},
},
{
key: COLUMN_KEYS.DURATION,
title: t('花费时间'),
dataIndex: 'finish_time',
render: (finish, record) => {
return renderDuration(record.submit_time, finish, t);
},
},
{
key: COLUMN_KEYS.CHANNEL,
title: t('渠道'),
dataIndex: 'channel_id',
render: (text, record, index) => {
return isAdminUser ? (
<div>
<Tag
color={colors[parseInt(text) % colors.length]}
shape='circle'
prefixIcon={<Hash size={14} />}
onClick={() => {
copyText(text);
}}
>
{' '}
{text}{' '}
</Tag>
</div>
) : (
<></>
);
},
},
{
key: COLUMN_KEYS.TYPE,
title: t('类型'),
dataIndex: 'action',
render: (text, record, index) => {
return <div>{renderType(text, t)}</div>;
},
},
{
key: COLUMN_KEYS.TASK_ID,
title: t('任务ID'),
dataIndex: 'mj_id',
render: (text, record, index) => {
return <div>{text}</div>;
},
},
{
key: COLUMN_KEYS.SUBMIT_RESULT,
title: t('提交结果'),
dataIndex: 'code',
render: (text, record, index) => {
return isAdminUser ? <div>{renderCode(text, t)}</div> : <></>;
},
},
{
key: COLUMN_KEYS.TASK_STATUS,
title: t('任务状态'),
dataIndex: 'status',
render: (text, record, index) => {
return <div>{renderStatus(text, t)}</div>;
},
},
{
key: COLUMN_KEYS.PROGRESS,
title: t('进度'),
dataIndex: 'progress',
render: (text, record, index) => {
return (
<div>
{
<Progress
stroke={
record.status === 'FAILURE'
? 'var(--semi-color-warning)'
: null
}
percent={text ? parseInt(text.replace('%', '')) : 0}
showInfo={true}
aria-label='drawing progress'
style={{ minWidth: '160px' }}
/>
}
</div>
);
},
},
{
key: COLUMN_KEYS.IMAGE,
title: t('结果图片'),
dataIndex: 'image_url',
render: (text, record, index) => {
if (!text) {
return t('无');
}
return (
<Button
size="small"
onClick={() => {
openImageModal(text);
}}
>
{t('查看图片')}
</Button>
);
},
},
{
key: COLUMN_KEYS.PROMPT,
title: 'Prompt',
dataIndex: 'prompt',
render: (text, record, index) => {
if (!text) {
return t('无');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
openContentModal(text);
}}
>
{text}
</Typography.Text>
);
},
},
{
key: COLUMN_KEYS.PROMPT_EN,
title: 'PromptEn',
dataIndex: 'prompt_en',
render: (text, record, index) => {
if (!text) {
return t('无');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
openContentModal(text);
}}
>
{text}
</Typography.Text>
);
},
},
{
key: COLUMN_KEYS.FAIL_REASON,
title: t('失败原因'),
dataIndex: 'fail_reason',
fixed: 'right',
render: (text, record, index) => {
if (!text) {
return t('无');
}
return (
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
openContentModal(text);
}}
>
{text}
</Typography.Text>
);
},
},
];
};
\ No newline at end of file
import React from 'react';
import { Button, Form } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons';
const MjLogsFilters = ({
formInitValues,
setFormApi,
refresh,
setShowColumnSelector,
formApi,
loading,
isAdminUser,
t,
}) => {
return (
<Form
initValues={formInitValues}
getFormApi={(api) => setFormApi(api)}
onSubmit={refresh}
allowEmpty={true}
autoComplete="off"
layout="vertical"
trigger="change"
stopValidateWithError={false}
>
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
{/* 时间选择器 */}
<div className="col-span-1 lg:col-span-2">
<Form.DatePicker
field='dateRange'
className="w-full"
type='dateTimeRange'
placeholder={[t('开始时间'), t('结束时间')]}
showClear
pure
size="small"
/>
</div>
{/* 任务 ID */}
<Form.Input
field='mj_id'
prefix={<IconSearch />}
placeholder={t('任务 ID')}
showClear
pure
size="small"
/>
{/* 渠道 ID - 仅管理员可见 */}
{isAdminUser && (
<Form.Input
field='channel_id'
prefix={<IconSearch />}
placeholder={t('渠道 ID')}
showClear
pure
size="small"
/>
)}
</div>
{/* 操作按钮区域 */}
<div className="flex justify-between items-center">
<div></div>
<div className="flex gap-2">
<Button
type='tertiary'
htmlType='submit'
loading={loading}
size="small"
>
{t('查询')}
</Button>
<Button
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
setTimeout(() => {
refresh();
}, 100);
}
}}
size="small"
>
{t('重置')}
</Button>
<Button
type='tertiary'
onClick={() => setShowColumnSelector(true)}
size="small"
>
{t('列设置')}
</Button>
</div>
</div>
</div>
</Form>
);
};
export default MjLogsFilters;
\ No newline at end of file
import React, { useMemo } from 'react';
import { Table, Empty } from '@douyinfe/semi-ui';
import {
IllustrationNoResult,
IllustrationNoResultDark,
} from '@douyinfe/semi-illustrations';
import { getMjLogsColumns } from './MjLogsColumnDefs.js';
const MjLogsTable = (mjLogsData) => {
const {
logs,
loading,
activePage,
pageSize,
logCount,
compactMode,
visibleColumns,
handlePageChange,
handlePageSizeChange,
copyText,
openContentModal,
openImageModal,
isAdminUser,
t,
COLUMN_KEYS,
} = mjLogsData;
// Get all columns
const allColumns = useMemo(() => {
return getMjLogsColumns({
t,
COLUMN_KEYS,
copyText,
openContentModal,
openImageModal,
isAdminUser,
});
}, [
t,
COLUMN_KEYS,
copyText,
openContentModal,
openImageModal,
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 (
<Table
columns={tableColumns}
dataSource={logs}
rowKey='key'
loading={loading}
scroll={compactMode ? undefined : { x: 'max-content' }}
className="rounded-xl overflow-hidden"
size="middle"
empty={
<Empty
image={
<IllustrationNoResult style={{ width: 150, height: 150 }} />
}
darkModeImage={
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
}
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 MjLogsTable;
\ No newline at end of file
import React from 'react';
import { Layout } from '@douyinfe/semi-ui';
import CardPro from '../../common/ui/CardPro.js';
import MjLogsTable from './MjLogsTable.jsx';
import MjLogsActions from './MjLogsActions.jsx';
import MjLogsFilters from './MjLogsFilters.jsx';
import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx';
import ContentModal from './modals/ContentModal.jsx';
import { useMjLogsData } from '../../../hooks/mj-logs/useMjLogsData.js';
const MjLogsPage = () => {
const mjLogsData = useMjLogsData();
return (
<>
{/* Modals */}
<ColumnSelectorModal {...mjLogsData} />
<ContentModal {...mjLogsData} />
<Layout>
<CardPro
type="type2"
statsArea={<MjLogsActions {...mjLogsData} />}
searchArea={<MjLogsFilters {...mjLogsData} />}
>
<MjLogsTable {...mjLogsData} />
</CardPro>
</Layout>
</>
);
};
export default MjLogsPage;
\ No newline at end of file
import React from 'react';
import { Modal, Button, Checkbox } from '@douyinfe/semi-ui';
import { getMjLogsColumns } from '../MjLogsColumnDefs.js';
const ColumnSelectorModal = ({
showColumnSelector,
setShowColumnSelector,
visibleColumns,
handleColumnVisibilityChange,
handleSelectAll,
initDefaultColumns,
COLUMN_KEYS,
isAdminUser,
copyText,
openContentModal,
openImageModal,
t,
}) => {
// Get all columns for display in selector
const allColumns = getMjLogsColumns({
t,
COLUMN_KEYS,
copyText,
openContentModal,
openImageModal,
isAdminUser,
});
return (
<Modal
title={t('列设置')}
visible={showColumnSelector}
onCancel={() => setShowColumnSelector(false)}
footer={
<div className="flex justify-end">
<Button onClick={() => initDefaultColumns()}>
{t('重置')}
</Button>
<Button onClick={() => setShowColumnSelector(false)}>
{t('取消')}
</Button>
<Button onClick={() => setShowColumnSelector(false)}>
{t('确定')}
</Button>
</div>
}
>
<div style={{ marginBottom: 20 }}>
<Checkbox
checked={Object.values(visibleColumns).every((v) => v === true)}
indeterminate={
Object.values(visibleColumns).some((v) => v === true) &&
!Object.values(visibleColumns).every((v) => v === true)
}
onChange={(e) => handleSelectAll(e.target.checked)}
>
{t('全选')}
</Checkbox>
</div>
<div
className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4"
style={{ border: '1px solid var(--semi-color-border)' }}
>
{allColumns.map((column) => {
// Skip admin-only columns for non-admin users
if (
!isAdminUser &&
(column.key === COLUMN_KEYS.CHANNEL ||
column.key === COLUMN_KEYS.SUBMIT_RESULT)
) {
return null;
}
return (
<div key={column.key} className="w-1/2 mb-4 pr-2">
<Checkbox
checked={!!visibleColumns[column.key]}
onChange={(e) =>
handleColumnVisibilityChange(column.key, e.target.checked)
}
>
{column.title}
</Checkbox>
</div>
);
})}
</div>
</Modal>
);
};
export default ColumnSelectorModal;
\ No newline at end of file
import React from 'react';
import { Modal, ImagePreview } from '@douyinfe/semi-ui';
const ContentModal = ({
isModalOpen,
setIsModalOpen,
modalContent,
isModalOpenurl,
setIsModalOpenurl,
modalImageUrl,
}) => {
return (
<>
{/* Text Content Modal */}
<Modal
visible={isModalOpen}
onOk={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
closable={null}
bodyStyle={{ height: '400px', overflow: 'auto' }}
width={800}
>
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
</Modal>
{/* Image Preview Modal */}
<ImagePreview
src={modalImageUrl}
visible={isModalOpenurl}
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
/>
</>
);
};
export default ContentModal;
\ No newline at end of file
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 useMjLogsData = () => {
const { t } = useTranslation();
// Define column keys for selection
const COLUMN_KEYS = {
SUBMIT_TIME: 'submit_time',
DURATION: 'duration',
CHANNEL: 'channel',
TYPE: 'type',
TASK_ID: 'task_id',
SUBMIT_RESULT: 'submit_result',
TASK_STATUS: 'task_status',
PROGRESS: 'progress',
IMAGE: 'image',
PROMPT: 'prompt',
PROMPT_EN: 'prompt_en',
FAIL_REASON: 'fail_reason',
};
// Basic state
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [logCount, setLogCount] = useState(0);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [showBanner, setShowBanner] = useState(false);
// User and admin
const isAdminUser = isAdmin();
// Modal states
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
const [modalImageUrl, setModalImageUrl] = useState('');
// Form state
const [formApi, setFormApi] = useState(null);
let now = new Date();
const formInitValues = {
channel_id: '',
mj_id: '',
dateRange: [
timestamp2string(now.getTime() / 1000 - 2592000),
timestamp2string(now.getTime() / 1000 + 3600)
],
};
// Column visibility state
const [visibleColumns, setVisibleColumns] = useState({});
const [showColumnSelector, setShowColumnSelector] = useState(false);
// Compact mode
const [compactMode, setCompactMode] = useTableCompactMode('mjLogs');
// Load saved column preferences from localStorage
useEffect(() => {
const savedColumns = localStorage.getItem('mj-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();
}
}, []);
// Check banner notification
useEffect(() => {
const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
if (mjNotifyEnabled !== 'true') {
setShowBanner(true);
}
}, []);
// Get default column visibility based on user role
const getDefaultColumnVisibility = () => {
return {
[COLUMN_KEYS.SUBMIT_TIME]: true,
[COLUMN_KEYS.DURATION]: true,
[COLUMN_KEYS.CHANNEL]: isAdminUser,
[COLUMN_KEYS.TYPE]: true,
[COLUMN_KEYS.TASK_ID]: true,
[COLUMN_KEYS.SUBMIT_RESULT]: isAdminUser,
[COLUMN_KEYS.TASK_STATUS]: true,
[COLUMN_KEYS.PROGRESS]: true,
[COLUMN_KEYS.IMAGE]: true,
[COLUMN_KEYS.PROMPT]: true,
[COLUMN_KEYS.PROMPT_EN]: true,
[COLUMN_KEYS.FAIL_REASON]: true,
};
};
// Initialize default column visibility
const initDefaultColumns = () => {
const defaults = getDefaultColumnVisibility();
setVisibleColumns(defaults);
localStorage.setItem('mj-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 || key === COLUMN_KEYS.SUBMIT_RESULT) &&
!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('mj-logs-table-columns', JSON.stringify(visibleColumns));
}
}, [visibleColumns]);
// Get form values helper function
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
let start_timestamp = timestamp2string(now.getTime() / 1000 - 2592000);
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 || '',
mj_id: formValues.mj_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, mj_id, start_timestamp, end_timestamp } = getFormValues();
let localStartTimestamp = Date.parse(start_timestamp);
let localEndTimestamp = Date.parse(end_timestamp);
const url = isAdminUser
? `/api/mj/?p=${page}&page_size=${size}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
: `/api/mj/self/?p=${page}&page_size=${size}&mj_id=${mj_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('mj-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);
};
const openImageModal = (imageUrl) => {
setModalImageUrl(imageUrl);
setIsModalOpenurl(true);
};
// Initialize data
useEffect(() => {
const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
setPageSize(localPageSize);
loadLogs(1, localPageSize).then();
}, []);
return {
// Basic state
logs,
loading,
activePage,
logCount,
pageSize,
showBanner,
isAdminUser,
// Modal state
isModalOpen,
setIsModalOpen,
modalContent,
isModalOpenurl,
setIsModalOpenurl,
modalImageUrl,
// 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,
openImageModal,
enrichLogs,
syncPageData,
// Translation
t,
};
};
\ No newline at end of file
...@@ -2,600 +2,600 @@ import { useState, useEffect } from 'react'; ...@@ -2,600 +2,600 @@ import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Modal } from '@douyinfe/semi-ui'; import { Modal } from '@douyinfe/semi-ui';
import { import {
API, API,
getTodayStartTimestamp, getTodayStartTimestamp,
isAdmin, isAdmin,
showError, showError,
showSuccess, showSuccess,
timestamp2string, timestamp2string,
renderQuota, renderQuota,
renderNumber, renderNumber,
getLogOther, getLogOther,
copy, copy,
renderClaudeLogContent, renderClaudeLogContent,
renderLogContent, renderLogContent,
renderAudioModelPrice, renderAudioModelPrice,
renderClaudeModelPrice, renderClaudeModelPrice,
renderModelPrice renderModelPrice
} from '../../helpers'; } from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants'; import { ITEMS_PER_PAGE } from '../../constants';
import { useTableCompactMode } from '../common/useTableCompactMode'; import { useTableCompactMode } from '../common/useTableCompactMode';
export const useLogsData = () => { export const useLogsData = () => {
const { t } = useTranslation(); const { t } = useTranslation();
// Define column keys for selection // Define column keys for selection
const COLUMN_KEYS = { const COLUMN_KEYS = {
TIME: 'time', TIME: 'time',
CHANNEL: 'channel', CHANNEL: 'channel',
USERNAME: 'username', USERNAME: 'username',
TOKEN: 'token', TOKEN: 'token',
GROUP: 'group', GROUP: 'group',
TYPE: 'type', TYPE: 'type',
MODEL: 'model', MODEL: 'model',
USE_TIME: 'use_time', USE_TIME: 'use_time',
PROMPT: 'prompt', PROMPT: 'prompt',
COMPLETION: 'completion', COMPLETION: 'completion',
COST: 'cost', COST: 'cost',
RETRY: 'retry', RETRY: 'retry',
IP: 'ip', IP: 'ip',
DETAILS: 'details', DETAILS: 'details',
}; };
// Basic state // Basic state
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [expandData, setExpandData] = useState({}); const [expandData, setExpandData] = useState({});
const [showStat, setShowStat] = useState(false); const [showStat, setShowStat] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingStat, setLoadingStat] = useState(false); const [loadingStat, setLoadingStat] = useState(false);
const [activePage, setActivePage] = useState(1); const [activePage, setActivePage] = useState(1);
const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [logType, setLogType] = useState(0); const [logType, setLogType] = useState(0);
// User and admin // User and admin
const isAdminUser = isAdmin(); const isAdminUser = isAdmin();
// Statistics state // Statistics state
const [stat, setStat] = useState({ const [stat, setStat] = useState({
quota: 0, quota: 0,
token: 0, token: 0,
}); });
// Form state // Form state
const [formApi, setFormApi] = useState(null); const [formApi, setFormApi] = useState(null);
let now = new Date(); let now = new Date();
const formInitValues = { const formInitValues = {
username: '', username: '',
token_name: '', token_name: '',
model_name: '', model_name: '',
channel: '', channel: '',
group: '', group: '',
dateRange: [ dateRange: [
timestamp2string(getTodayStartTimestamp()), timestamp2string(getTodayStartTimestamp()),
timestamp2string(now.getTime() / 1000 + 3600), timestamp2string(now.getTime() / 1000 + 3600),
], ],
logType: '0', logType: '0',
}; };
// Column visibility state // Column visibility state
const [visibleColumns, setVisibleColumns] = useState({}); const [visibleColumns, setVisibleColumns] = useState({});
const [showColumnSelector, setShowColumnSelector] = useState(false); const [showColumnSelector, setShowColumnSelector] = useState(false);
// Compact mode // Compact mode
const [compactMode, setCompactMode] = useTableCompactMode('logs'); const [compactMode, setCompactMode] = useTableCompactMode('logs');
// User info modal state // User info modal state
const [showUserInfo, setShowUserInfoModal] = useState(false); const [showUserInfo, setShowUserInfoModal] = useState(false);
const [userInfoData, setUserInfoData] = useState(null); const [userInfoData, setUserInfoData] = useState(null);
// Load saved column preferences from localStorage // Load saved column preferences from localStorage
useEffect(() => { useEffect(() => {
const savedColumns = localStorage.getItem('logs-table-columns'); const savedColumns = localStorage.getItem('logs-table-columns');
if (savedColumns) { if (savedColumns) {
try { try {
const parsed = JSON.parse(savedColumns); const parsed = JSON.parse(savedColumns);
const defaults = getDefaultColumnVisibility(); const defaults = getDefaultColumnVisibility();
const merged = { ...defaults, ...parsed }; const merged = { ...defaults, ...parsed };
setVisibleColumns(merged); setVisibleColumns(merged);
} catch (e) { } catch (e) {
console.error('Failed to parse saved column preferences', e); console.error('Failed to parse saved column preferences', e);
initDefaultColumns(); initDefaultColumns();
} }
} else { } else {
initDefaultColumns(); initDefaultColumns();
} }
}, []); }, []);
// Get default column visibility based on user role // Get default column visibility based on user role
const getDefaultColumnVisibility = () => { const getDefaultColumnVisibility = () => {
return { return {
[COLUMN_KEYS.TIME]: true, [COLUMN_KEYS.TIME]: true,
[COLUMN_KEYS.CHANNEL]: isAdminUser, [COLUMN_KEYS.CHANNEL]: isAdminUser,
[COLUMN_KEYS.USERNAME]: isAdminUser, [COLUMN_KEYS.USERNAME]: isAdminUser,
[COLUMN_KEYS.TOKEN]: true, [COLUMN_KEYS.TOKEN]: true,
[COLUMN_KEYS.GROUP]: true, [COLUMN_KEYS.GROUP]: true,
[COLUMN_KEYS.TYPE]: true, [COLUMN_KEYS.TYPE]: true,
[COLUMN_KEYS.MODEL]: true, [COLUMN_KEYS.MODEL]: true,
[COLUMN_KEYS.USE_TIME]: true, [COLUMN_KEYS.USE_TIME]: true,
[COLUMN_KEYS.PROMPT]: true, [COLUMN_KEYS.PROMPT]: true,
[COLUMN_KEYS.COMPLETION]: true, [COLUMN_KEYS.COMPLETION]: true,
[COLUMN_KEYS.COST]: true, [COLUMN_KEYS.COST]: true,
[COLUMN_KEYS.RETRY]: isAdminUser, [COLUMN_KEYS.RETRY]: isAdminUser,
[COLUMN_KEYS.IP]: true, [COLUMN_KEYS.IP]: true,
[COLUMN_KEYS.DETAILS]: true, [COLUMN_KEYS.DETAILS]: true,
}; };
}; };
// Initialize default column visibility // Initialize default column visibility
const initDefaultColumns = () => { const initDefaultColumns = () => {
const defaults = getDefaultColumnVisibility(); const defaults = getDefaultColumnVisibility();
setVisibleColumns(defaults); setVisibleColumns(defaults);
localStorage.setItem('logs-table-columns', JSON.stringify(defaults)); localStorage.setItem('logs-table-columns', JSON.stringify(defaults));
}; };
// Handle column visibility change // Handle column visibility change
const handleColumnVisibilityChange = (columnKey, checked) => { const handleColumnVisibilityChange = (columnKey, checked) => {
const updatedColumns = { ...visibleColumns, [columnKey]: checked }; const updatedColumns = { ...visibleColumns, [columnKey]: checked };
setVisibleColumns(updatedColumns); setVisibleColumns(updatedColumns);
}; };
// Handle "Select All" checkbox // Handle "Select All" checkbox
const handleSelectAll = (checked) => { const handleSelectAll = (checked) => {
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]); const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
const updatedColumns = {}; const updatedColumns = {};
allKeys.forEach((key) => { allKeys.forEach((key) => {
if ( if (
(key === COLUMN_KEYS.CHANNEL || (key === COLUMN_KEYS.CHANNEL ||
key === COLUMN_KEYS.USERNAME || key === COLUMN_KEYS.USERNAME ||
key === COLUMN_KEYS.RETRY) && key === COLUMN_KEYS.RETRY) &&
!isAdminUser !isAdminUser
) { ) {
updatedColumns[key] = false; updatedColumns[key] = false;
} else { } else {
updatedColumns[key] = checked; updatedColumns[key] = checked;
} }
}); });
setVisibleColumns(updatedColumns); setVisibleColumns(updatedColumns);
}; };
// Update table when column visibility changes // Update table when column visibility changes
useEffect(() => { useEffect(() => {
if (Object.keys(visibleColumns).length > 0) { if (Object.keys(visibleColumns).length > 0) {
localStorage.setItem( localStorage.setItem(
'logs-table-columns', 'logs-table-columns',
JSON.stringify(visibleColumns), JSON.stringify(visibleColumns),
); );
} }
}, [visibleColumns]); }, [visibleColumns]);
// 获取表单值的辅助函数,确保所有值都是字符串 // 获取表单值的辅助函数,确保所有值都是字符串
const getFormValues = () => { const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {}; const formValues = formApi ? formApi.getValues() : {};
let start_timestamp = timestamp2string(getTodayStartTimestamp()); let start_timestamp = timestamp2string(getTodayStartTimestamp());
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600); let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
if ( if (
formValues.dateRange && formValues.dateRange &&
Array.isArray(formValues.dateRange) && Array.isArray(formValues.dateRange) &&
formValues.dateRange.length === 2 formValues.dateRange.length === 2
) { ) {
start_timestamp = formValues.dateRange[0]; start_timestamp = formValues.dateRange[0];
end_timestamp = formValues.dateRange[1]; end_timestamp = formValues.dateRange[1];
} }
return { return {
username: formValues.username || '', username: formValues.username || '',
token_name: formValues.token_name || '', token_name: formValues.token_name || '',
model_name: formValues.model_name || '', model_name: formValues.model_name || '',
start_timestamp, start_timestamp,
end_timestamp, end_timestamp,
channel: formValues.channel || '', channel: formValues.channel || '',
group: formValues.group || '', group: formValues.group || '',
logType: formValues.logType ? parseInt(formValues.logType) : 0, logType: formValues.logType ? parseInt(formValues.logType) : 0,
}; };
}; };
// Statistics functions // Statistics functions
const getLogSelfStat = async () => { const getLogSelfStat = async () => {
const { const {
token_name, token_name,
model_name, model_name,
start_timestamp, start_timestamp,
end_timestamp, end_timestamp,
group, group,
logType: formLogType, logType: formLogType,
} = getFormValues(); } = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType; 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=${currentLogType}&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;
if (success) { if (success) {
setStat(data); setStat(data);
} else { } else {
showError(message); showError(message);
} }
}; };
const getLogStat = async () => { const getLogStat = async () => {
const { const {
username, username,
token_name, token_name,
model_name, model_name,
start_timestamp, start_timestamp,
end_timestamp, end_timestamp,
channel, channel,
group, group,
logType: formLogType, logType: formLogType,
} = getFormValues(); } = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType; 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=${currentLogType}&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;
if (success) { if (success) {
setStat(data); setStat(data);
} else { } else {
showError(message); showError(message);
} }
}; };
const handleEyeClick = async () => { const handleEyeClick = async () => {
if (loadingStat) { if (loadingStat) {
return; return;
} }
setLoadingStat(true); setLoadingStat(true);
if (isAdminUser) { if (isAdminUser) {
await getLogStat(); await getLogStat();
} else { } else {
await getLogSelfStat(); await getLogSelfStat();
} }
setShowStat(true); setShowStat(true);
setLoadingStat(false); setLoadingStat(false);
}; };
// User info function // User info function
const showUserInfoFunc = async (userId) => { const showUserInfoFunc = async (userId) => {
if (!isAdminUser) { if (!isAdminUser) {
return; return;
} }
const res = await API.get(`/api/user/${userId}`); const res = await API.get(`/api/user/${userId}`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setUserInfoData(data); setUserInfoData(data);
setShowUserInfoModal(true); setShowUserInfoModal(true);
} else { } else {
showError(message); showError(message);
} }
}; };
// Format logs data // Format logs data
const setLogsFormat = (logs) => { const setLogsFormat = (logs) => {
let expandDatesLocal = {}; let expandDatesLocal = {};
for (let i = 0; i < logs.length; i++) { for (let i = 0; i < logs.length; i++) {
logs[i].timestamp2string = timestamp2string(logs[i].created_at); logs[i].timestamp2string = timestamp2string(logs[i].created_at);
logs[i].key = logs[i].id; logs[i].key = logs[i].id;
let other = getLogOther(logs[i].other); let other = getLogOther(logs[i].other);
let expandDataLocal = []; let expandDataLocal = [];
if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) { if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
expandDataLocal.push({ expandDataLocal.push({
key: t('渠道信息'), key: t('渠道信息'),
value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`, value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`,
}); });
} }
if (other?.ws || other?.audio) { if (other?.ws || other?.audio) {
expandDataLocal.push({ expandDataLocal.push({
key: t('语音输入'), key: t('语音输入'),
value: other.audio_input, value: other.audio_input,
}); });
expandDataLocal.push({ expandDataLocal.push({
key: t('语音输出'), key: t('语音输出'),
value: other.audio_output, value: other.audio_output,
}); });
expandDataLocal.push({ expandDataLocal.push({
key: t('文字输入'), key: t('文字输入'),
value: other.text_input, value: other.text_input,
}); });
expandDataLocal.push({ expandDataLocal.push({
key: t('文字输出'), key: t('文字输出'),
value: other.text_output, value: other.text_output,
}); });
} }
if (other?.cache_tokens > 0) { if (other?.cache_tokens > 0) {
expandDataLocal.push({ expandDataLocal.push({
key: t('缓存 Tokens'), key: t('缓存 Tokens'),
value: other.cache_tokens, value: other.cache_tokens,
}); });
} }
if (other?.cache_creation_tokens > 0) { if (other?.cache_creation_tokens > 0) {
expandDataLocal.push({ expandDataLocal.push({
key: t('缓存创建 Tokens'), key: t('缓存创建 Tokens'),
value: other.cache_creation_tokens, value: other.cache_creation_tokens,
}); });
} }
if (logs[i].type === 2) { if (logs[i].type === 2) {
expandDataLocal.push({ expandDataLocal.push({
key: t('日志详情'), key: t('日志详情'),
value: other?.claude value: other?.claude
? renderClaudeLogContent( ? renderClaudeLogContent(
other?.model_ratio, other?.model_ratio,
other.completion_ratio, other.completion_ratio,
other.model_price, other.model_price,
other.group_ratio, other.group_ratio,
other?.user_group_ratio, other?.user_group_ratio,
other.cache_ratio || 1.0, other.cache_ratio || 1.0,
other.cache_creation_ratio || 1.0, other.cache_creation_ratio || 1.0,
) )
: renderLogContent( : renderLogContent(
other?.model_ratio, other?.model_ratio,
other.completion_ratio, other.completion_ratio,
other.model_price, other.model_price,
other.group_ratio, other.group_ratio,
other?.user_group_ratio, other?.user_group_ratio,
false, false,
1.0, 1.0,
other.web_search || false, other.web_search || false,
other.web_search_call_count || 0, other.web_search_call_count || 0,
other.file_search || false, other.file_search || false,
other.file_search_call_count || 0, other.file_search_call_count || 0,
), ),
}); });
} }
if (logs[i].type === 2) { if (logs[i].type === 2) {
let modelMapped = let modelMapped =
other?.is_model_mapped && other?.is_model_mapped &&
other?.upstream_model_name && other?.upstream_model_name &&
other?.upstream_model_name !== ''; other?.upstream_model_name !== '';
if (modelMapped) { if (modelMapped) {
expandDataLocal.push({ expandDataLocal.push({
key: t('请求并计费模型'), key: t('请求并计费模型'),
value: logs[i].model_name, value: logs[i].model_name,
}); });
expandDataLocal.push({ expandDataLocal.push({
key: t('实际模型'), key: t('实际模型'),
value: other.upstream_model_name, value: other.upstream_model_name,
}); });
} }
let content = ''; let content = '';
if (other?.ws || other?.audio) { if (other?.ws || other?.audio) {
content = renderAudioModelPrice( content = renderAudioModelPrice(
other?.text_input, other?.text_input,
other?.text_output, other?.text_output,
other?.model_ratio, other?.model_ratio,
other?.model_price, other?.model_price,
other?.completion_ratio, other?.completion_ratio,
other?.audio_input, other?.audio_input,
other?.audio_output, other?.audio_output,
other?.audio_ratio, other?.audio_ratio,
other?.audio_completion_ratio, other?.audio_completion_ratio,
other?.group_ratio, other?.group_ratio,
other?.user_group_ratio, other?.user_group_ratio,
other?.cache_tokens || 0, other?.cache_tokens || 0,
other?.cache_ratio || 1.0, other?.cache_ratio || 1.0,
); );
} else if (other?.claude) { } else if (other?.claude) {
content = renderClaudeModelPrice( content = renderClaudeModelPrice(
logs[i].prompt_tokens, logs[i].prompt_tokens,
logs[i].completion_tokens, logs[i].completion_tokens,
other.model_ratio, other.model_ratio,
other.model_price, other.model_price,
other.completion_ratio, other.completion_ratio,
other.group_ratio, other.group_ratio,
other?.user_group_ratio, other?.user_group_ratio,
other.cache_tokens || 0, other.cache_tokens || 0,
other.cache_ratio || 1.0, other.cache_ratio || 1.0,
other.cache_creation_tokens || 0, other.cache_creation_tokens || 0,
other.cache_creation_ratio || 1.0, other.cache_creation_ratio || 1.0,
); );
} else { } else {
content = renderModelPrice( content = renderModelPrice(
logs[i].prompt_tokens, logs[i].prompt_tokens,
logs[i].completion_tokens, logs[i].completion_tokens,
other?.model_ratio, other?.model_ratio,
other?.model_price, other?.model_price,
other?.completion_ratio, other?.completion_ratio,
other?.group_ratio, other?.group_ratio,
other?.user_group_ratio, other?.user_group_ratio,
other?.cache_tokens || 0, other?.cache_tokens || 0,
other?.cache_ratio || 1.0, other?.cache_ratio || 1.0,
other?.image || false, other?.image || false,
other?.image_ratio || 0, other?.image_ratio || 0,
other?.image_output || 0, other?.image_output || 0,
other?.web_search || false, other?.web_search || false,
other?.web_search_call_count || 0, other?.web_search_call_count || 0,
other?.web_search_price || 0, other?.web_search_price || 0,
other?.file_search || false, other?.file_search || false,
other?.file_search_call_count || 0, other?.file_search_call_count || 0,
other?.file_search_price || 0, other?.file_search_price || 0,
other?.audio_input_seperate_price || false, other?.audio_input_seperate_price || false,
other?.audio_input_token_count || 0, other?.audio_input_token_count || 0,
other?.audio_input_price || 0, other?.audio_input_price || 0,
); );
} }
expandDataLocal.push({ expandDataLocal.push({
key: t('计费过程'), key: t('计费过程'),
value: content, value: content,
}); });
if (other?.reasoning_effort) { if (other?.reasoning_effort) {
expandDataLocal.push({ expandDataLocal.push({
key: t('Reasoning Effort'), key: t('Reasoning Effort'),
value: other.reasoning_effort, value: other.reasoning_effort,
}); });
} }
} }
expandDatesLocal[logs[i].key] = expandDataLocal; expandDatesLocal[logs[i].key] = expandDataLocal;
} }
setExpandData(expandDatesLocal); setExpandData(expandDatesLocal);
setLogs(logs); setLogs(logs);
}; };
// Load logs function // Load logs function
const loadLogs = async (startIdx, pageSize, customLogType = null) => { const loadLogs = async (startIdx, pageSize, customLogType = null) => {
setLoading(true); setLoading(true);
let url = ''; let url = '';
const { const {
username, username,
token_name, token_name,
model_name, model_name,
start_timestamp, start_timestamp,
end_timestamp, end_timestamp,
channel, channel,
group, group,
logType: formLogType, logType: formLogType,
} = getFormValues(); } = getFormValues();
const currentLogType = const currentLogType =
customLogType !== null customLogType !== null
? customLogType ? customLogType
: formLogType !== undefined : formLogType !== undefined
? formLogType ? formLogType
: logType; : 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=${currentLogType}&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=${currentLogType}&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);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
const newPageData = data.items; const newPageData = data.items;
setActivePage(data.page); setActivePage(data.page);
setPageSize(data.page_size); setPageSize(data.page_size);
setLogCount(data.total); setLogCount(data.total);
setLogsFormat(newPageData); setLogsFormat(newPageData);
} else { } else {
showError(message); showError(message);
} }
setLoading(false); setLoading(false);
}; };
// Page handlers // Page handlers
const handlePageChange = (page) => { const handlePageChange = (page) => {
setActivePage(page); setActivePage(page);
loadLogs(page, pageSize).then((r) => { }); loadLogs(page, pageSize).then((r) => { });
}; };
const handlePageSizeChange = async (size) => { const handlePageSizeChange = async (size) => {
localStorage.setItem('page-size', size + ''); localStorage.setItem('page-size', size + '');
setPageSize(size); setPageSize(size);
setActivePage(1); setActivePage(1);
loadLogs(activePage, size) loadLogs(activePage, size)
.then() .then()
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
}); });
}; };
// Refresh function // Refresh function
const refresh = async () => { const refresh = async () => {
setActivePage(1); setActivePage(1);
handleEyeClick(); handleEyeClick();
await loadLogs(1, pageSize); await loadLogs(1, pageSize);
}; };
// Copy text function // Copy text function
const copyText = async (e, text) => { const copyText = async (e, text) => {
e.stopPropagation(); e.stopPropagation();
if (await copy(text)) { if (await copy(text)) {
showSuccess('已复制:' + text); showSuccess('已复制:' + text);
} else { } else {
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text }); Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
} }
}; };
// Initialize data // Initialize data
useEffect(() => { useEffect(() => {
const localPageSize = const localPageSize =
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
setPageSize(localPageSize); setPageSize(localPageSize);
loadLogs(activePage, localPageSize) loadLogs(activePage, localPageSize)
.then() .then()
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
}); });
}, []); }, []);
// Initialize statistics when formApi is available // Initialize statistics when formApi is available
useEffect(() => { useEffect(() => {
if (formApi) { if (formApi) {
handleEyeClick(); handleEyeClick();
} }
}, [formApi]); }, [formApi]);
// Check if any record has expandable content // Check if any record has expandable content
const hasExpandableRows = () => { const hasExpandableRows = () => {
return logs.some( return logs.some(
(log) => expandData[log.key] && expandData[log.key].length > 0, (log) => expandData[log.key] && expandData[log.key].length > 0,
); );
}; };
return { return {
// Basic state // Basic state
logs, logs,
expandData, expandData,
showStat, showStat,
loading, loading,
loadingStat, loadingStat,
activePage, activePage,
logCount, logCount,
pageSize, pageSize,
logType, logType,
stat, stat,
isAdminUser, isAdminUser,
// Form state // Form state
formApi, formApi,
setFormApi, setFormApi,
formInitValues, formInitValues,
getFormValues, getFormValues,
// Column visibility // Column visibility
visibleColumns, visibleColumns,
showColumnSelector, showColumnSelector,
setShowColumnSelector, setShowColumnSelector,
handleColumnVisibilityChange, handleColumnVisibilityChange,
handleSelectAll, handleSelectAll,
initDefaultColumns, initDefaultColumns,
COLUMN_KEYS, COLUMN_KEYS,
// Compact mode // Compact mode
compactMode, compactMode,
setCompactMode, setCompactMode,
// User info modal // User info modal
showUserInfo, showUserInfo,
setShowUserInfoModal, setShowUserInfoModal,
userInfoData, userInfoData,
showUserInfoFunc, showUserInfoFunc,
// Functions // Functions
loadLogs, loadLogs,
handlePageChange, handlePageChange,
handlePageSizeChange, handlePageSizeChange,
refresh, refresh,
copyText, copyText,
handleEyeClick, handleEyeClick,
setLogsFormat, setLogsFormat,
hasExpandableRows, hasExpandableRows,
setLogType, setLogType,
// Translation // Translation
t, t,
}; };
}; };
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment