|
|
@@ -2,600 +2,600 @@ import { useState, useEffect } from 'react';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
import { Modal } from '@douyinfe/semi-ui';
|
|
|
import {
|
|
|
- API,
|
|
|
- getTodayStartTimestamp,
|
|
|
- isAdmin,
|
|
|
- showError,
|
|
|
- showSuccess,
|
|
|
- timestamp2string,
|
|
|
- renderQuota,
|
|
|
- renderNumber,
|
|
|
- getLogOther,
|
|
|
- copy,
|
|
|
- renderClaudeLogContent,
|
|
|
- renderLogContent,
|
|
|
- renderAudioModelPrice,
|
|
|
- renderClaudeModelPrice,
|
|
|
- renderModelPrice
|
|
|
+ API,
|
|
|
+ getTodayStartTimestamp,
|
|
|
+ isAdmin,
|
|
|
+ showError,
|
|
|
+ showSuccess,
|
|
|
+ timestamp2string,
|
|
|
+ renderQuota,
|
|
|
+ renderNumber,
|
|
|
+ getLogOther,
|
|
|
+ copy,
|
|
|
+ renderClaudeLogContent,
|
|
|
+ renderLogContent,
|
|
|
+ renderAudioModelPrice,
|
|
|
+ renderClaudeModelPrice,
|
|
|
+ renderModelPrice
|
|
|
} from '../../helpers';
|
|
|
import { ITEMS_PER_PAGE } from '../../constants';
|
|
|
import { useTableCompactMode } from '../common/useTableCompactMode';
|
|
|
|
|
|
export const useLogsData = () => {
|
|
|
- const { t } = useTranslation();
|
|
|
-
|
|
|
- // Define column keys for selection
|
|
|
- const COLUMN_KEYS = {
|
|
|
- TIME: 'time',
|
|
|
- CHANNEL: 'channel',
|
|
|
- USERNAME: 'username',
|
|
|
- TOKEN: 'token',
|
|
|
- GROUP: 'group',
|
|
|
- TYPE: 'type',
|
|
|
- MODEL: 'model',
|
|
|
- USE_TIME: 'use_time',
|
|
|
- PROMPT: 'prompt',
|
|
|
- COMPLETION: 'completion',
|
|
|
- COST: 'cost',
|
|
|
- RETRY: 'retry',
|
|
|
- IP: 'ip',
|
|
|
- DETAILS: 'details',
|
|
|
- };
|
|
|
-
|
|
|
- // Basic state
|
|
|
- const [logs, setLogs] = useState([]);
|
|
|
- const [expandData, setExpandData] = useState({});
|
|
|
- const [showStat, setShowStat] = useState(false);
|
|
|
- const [loading, setLoading] = useState(false);
|
|
|
- const [loadingStat, setLoadingStat] = useState(false);
|
|
|
- const [activePage, setActivePage] = useState(1);
|
|
|
- const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
|
|
|
- const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
|
|
- const [logType, setLogType] = useState(0);
|
|
|
-
|
|
|
- // User and admin
|
|
|
- const isAdminUser = isAdmin();
|
|
|
-
|
|
|
- // Statistics state
|
|
|
- const [stat, setStat] = useState({
|
|
|
- quota: 0,
|
|
|
- token: 0,
|
|
|
- });
|
|
|
-
|
|
|
- // Form state
|
|
|
- const [formApi, setFormApi] = useState(null);
|
|
|
- let now = new Date();
|
|
|
- const formInitValues = {
|
|
|
- username: '',
|
|
|
- token_name: '',
|
|
|
- model_name: '',
|
|
|
- channel: '',
|
|
|
- group: '',
|
|
|
- dateRange: [
|
|
|
- timestamp2string(getTodayStartTimestamp()),
|
|
|
- timestamp2string(now.getTime() / 1000 + 3600),
|
|
|
- ],
|
|
|
- logType: '0',
|
|
|
- };
|
|
|
-
|
|
|
- // Column visibility state
|
|
|
- const [visibleColumns, setVisibleColumns] = useState({});
|
|
|
- const [showColumnSelector, setShowColumnSelector] = useState(false);
|
|
|
-
|
|
|
- // Compact mode
|
|
|
- const [compactMode, setCompactMode] = useTableCompactMode('logs');
|
|
|
-
|
|
|
- // User info modal state
|
|
|
- const [showUserInfo, setShowUserInfoModal] = useState(false);
|
|
|
- const [userInfoData, setUserInfoData] = useState(null);
|
|
|
-
|
|
|
- // Load saved column preferences from localStorage
|
|
|
- useEffect(() => {
|
|
|
- const savedColumns = localStorage.getItem('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.TIME]: true,
|
|
|
- [COLUMN_KEYS.CHANNEL]: isAdminUser,
|
|
|
- [COLUMN_KEYS.USERNAME]: isAdminUser,
|
|
|
- [COLUMN_KEYS.TOKEN]: true,
|
|
|
- [COLUMN_KEYS.GROUP]: true,
|
|
|
- [COLUMN_KEYS.TYPE]: true,
|
|
|
- [COLUMN_KEYS.MODEL]: true,
|
|
|
- [COLUMN_KEYS.USE_TIME]: true,
|
|
|
- [COLUMN_KEYS.PROMPT]: true,
|
|
|
- [COLUMN_KEYS.COMPLETION]: true,
|
|
|
- [COLUMN_KEYS.COST]: true,
|
|
|
- [COLUMN_KEYS.RETRY]: isAdminUser,
|
|
|
- [COLUMN_KEYS.IP]: true,
|
|
|
- [COLUMN_KEYS.DETAILS]: true,
|
|
|
- };
|
|
|
- };
|
|
|
-
|
|
|
- // Initialize default column visibility
|
|
|
- const initDefaultColumns = () => {
|
|
|
- const defaults = getDefaultColumnVisibility();
|
|
|
- setVisibleColumns(defaults);
|
|
|
- localStorage.setItem('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.USERNAME ||
|
|
|
- key === COLUMN_KEYS.RETRY) &&
|
|
|
- !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(
|
|
|
- 'logs-table-columns',
|
|
|
- JSON.stringify(visibleColumns),
|
|
|
- );
|
|
|
- }
|
|
|
- }, [visibleColumns]);
|
|
|
-
|
|
|
- // 获取表单值的辅助函数,确保所有值都是字符串
|
|
|
- const getFormValues = () => {
|
|
|
- const formValues = formApi ? formApi.getValues() : {};
|
|
|
-
|
|
|
- let start_timestamp = timestamp2string(getTodayStartTimestamp());
|
|
|
- let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
|
|
|
-
|
|
|
- if (
|
|
|
- formValues.dateRange &&
|
|
|
- Array.isArray(formValues.dateRange) &&
|
|
|
- formValues.dateRange.length === 2
|
|
|
- ) {
|
|
|
- start_timestamp = formValues.dateRange[0];
|
|
|
- end_timestamp = formValues.dateRange[1];
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- username: formValues.username || '',
|
|
|
- token_name: formValues.token_name || '',
|
|
|
- model_name: formValues.model_name || '',
|
|
|
- start_timestamp,
|
|
|
- end_timestamp,
|
|
|
- channel: formValues.channel || '',
|
|
|
- group: formValues.group || '',
|
|
|
- logType: formValues.logType ? parseInt(formValues.logType) : 0,
|
|
|
- };
|
|
|
- };
|
|
|
-
|
|
|
- // Statistics functions
|
|
|
- const getLogSelfStat = async () => {
|
|
|
- const {
|
|
|
- token_name,
|
|
|
- model_name,
|
|
|
- start_timestamp,
|
|
|
- end_timestamp,
|
|
|
- group,
|
|
|
- logType: formLogType,
|
|
|
- } = getFormValues();
|
|
|
- const currentLogType = formLogType !== undefined ? formLogType : logType;
|
|
|
- let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
- let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
- let url = `/api/log/self/stat?type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
|
|
- url = encodeURI(url);
|
|
|
- let res = await API.get(url);
|
|
|
- const { success, message, data } = res.data;
|
|
|
- if (success) {
|
|
|
- setStat(data);
|
|
|
- } else {
|
|
|
- showError(message);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const getLogStat = async () => {
|
|
|
- const {
|
|
|
- username,
|
|
|
- token_name,
|
|
|
- model_name,
|
|
|
- start_timestamp,
|
|
|
- end_timestamp,
|
|
|
- channel,
|
|
|
- group,
|
|
|
- logType: formLogType,
|
|
|
- } = getFormValues();
|
|
|
- const currentLogType = formLogType !== undefined ? formLogType : logType;
|
|
|
- let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
- let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
- let url = `/api/log/stat?type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
|
|
- url = encodeURI(url);
|
|
|
- let res = await API.get(url);
|
|
|
- const { success, message, data } = res.data;
|
|
|
- if (success) {
|
|
|
- setStat(data);
|
|
|
- } else {
|
|
|
- showError(message);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const handleEyeClick = async () => {
|
|
|
- if (loadingStat) {
|
|
|
- return;
|
|
|
- }
|
|
|
- setLoadingStat(true);
|
|
|
- if (isAdminUser) {
|
|
|
- await getLogStat();
|
|
|
- } else {
|
|
|
- await getLogSelfStat();
|
|
|
- }
|
|
|
- setShowStat(true);
|
|
|
- setLoadingStat(false);
|
|
|
- };
|
|
|
-
|
|
|
- // User info function
|
|
|
- const showUserInfoFunc = async (userId) => {
|
|
|
- if (!isAdminUser) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const res = await API.get(`/api/user/${userId}`);
|
|
|
- const { success, message, data } = res.data;
|
|
|
- if (success) {
|
|
|
- setUserInfoData(data);
|
|
|
- setShowUserInfoModal(true);
|
|
|
- } else {
|
|
|
- showError(message);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // Format logs data
|
|
|
- const setLogsFormat = (logs) => {
|
|
|
- let expandDatesLocal = {};
|
|
|
- for (let i = 0; i < logs.length; i++) {
|
|
|
- logs[i].timestamp2string = timestamp2string(logs[i].created_at);
|
|
|
- logs[i].key = logs[i].id;
|
|
|
- let other = getLogOther(logs[i].other);
|
|
|
- let expandDataLocal = [];
|
|
|
-
|
|
|
- if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('渠道信息'),
|
|
|
- value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`,
|
|
|
- });
|
|
|
- }
|
|
|
- if (other?.ws || other?.audio) {
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('语音输入'),
|
|
|
- value: other.audio_input,
|
|
|
- });
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('语音输出'),
|
|
|
- value: other.audio_output,
|
|
|
- });
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('文字输入'),
|
|
|
- value: other.text_input,
|
|
|
- });
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('文字输出'),
|
|
|
- value: other.text_output,
|
|
|
- });
|
|
|
- }
|
|
|
- if (other?.cache_tokens > 0) {
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('缓存 Tokens'),
|
|
|
- value: other.cache_tokens,
|
|
|
- });
|
|
|
- }
|
|
|
- if (other?.cache_creation_tokens > 0) {
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('缓存创建 Tokens'),
|
|
|
- value: other.cache_creation_tokens,
|
|
|
- });
|
|
|
- }
|
|
|
- if (logs[i].type === 2) {
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('日志详情'),
|
|
|
- value: other?.claude
|
|
|
- ? renderClaudeLogContent(
|
|
|
- other?.model_ratio,
|
|
|
- other.completion_ratio,
|
|
|
- other.model_price,
|
|
|
- other.group_ratio,
|
|
|
- other?.user_group_ratio,
|
|
|
- other.cache_ratio || 1.0,
|
|
|
- other.cache_creation_ratio || 1.0,
|
|
|
- )
|
|
|
- : renderLogContent(
|
|
|
- other?.model_ratio,
|
|
|
- other.completion_ratio,
|
|
|
- other.model_price,
|
|
|
- other.group_ratio,
|
|
|
- other?.user_group_ratio,
|
|
|
- false,
|
|
|
- 1.0,
|
|
|
- other.web_search || false,
|
|
|
- other.web_search_call_count || 0,
|
|
|
- other.file_search || false,
|
|
|
- other.file_search_call_count || 0,
|
|
|
- ),
|
|
|
- });
|
|
|
- }
|
|
|
- if (logs[i].type === 2) {
|
|
|
- let modelMapped =
|
|
|
- other?.is_model_mapped &&
|
|
|
- other?.upstream_model_name &&
|
|
|
- other?.upstream_model_name !== '';
|
|
|
- if (modelMapped) {
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('请求并计费模型'),
|
|
|
- value: logs[i].model_name,
|
|
|
- });
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('实际模型'),
|
|
|
- value: other.upstream_model_name,
|
|
|
- });
|
|
|
- }
|
|
|
- let content = '';
|
|
|
- if (other?.ws || other?.audio) {
|
|
|
- content = renderAudioModelPrice(
|
|
|
- other?.text_input,
|
|
|
- other?.text_output,
|
|
|
- other?.model_ratio,
|
|
|
- other?.model_price,
|
|
|
- other?.completion_ratio,
|
|
|
- other?.audio_input,
|
|
|
- other?.audio_output,
|
|
|
- other?.audio_ratio,
|
|
|
- other?.audio_completion_ratio,
|
|
|
- other?.group_ratio,
|
|
|
- other?.user_group_ratio,
|
|
|
- other?.cache_tokens || 0,
|
|
|
- other?.cache_ratio || 1.0,
|
|
|
- );
|
|
|
- } else if (other?.claude) {
|
|
|
- content = renderClaudeModelPrice(
|
|
|
- logs[i].prompt_tokens,
|
|
|
- logs[i].completion_tokens,
|
|
|
- other.model_ratio,
|
|
|
- other.model_price,
|
|
|
- other.completion_ratio,
|
|
|
- other.group_ratio,
|
|
|
- other?.user_group_ratio,
|
|
|
- other.cache_tokens || 0,
|
|
|
- other.cache_ratio || 1.0,
|
|
|
- other.cache_creation_tokens || 0,
|
|
|
- other.cache_creation_ratio || 1.0,
|
|
|
- );
|
|
|
- } else {
|
|
|
- content = renderModelPrice(
|
|
|
- logs[i].prompt_tokens,
|
|
|
- logs[i].completion_tokens,
|
|
|
- other?.model_ratio,
|
|
|
- other?.model_price,
|
|
|
- other?.completion_ratio,
|
|
|
- other?.group_ratio,
|
|
|
- other?.user_group_ratio,
|
|
|
- other?.cache_tokens || 0,
|
|
|
- other?.cache_ratio || 1.0,
|
|
|
- other?.image || false,
|
|
|
- other?.image_ratio || 0,
|
|
|
- other?.image_output || 0,
|
|
|
- other?.web_search || false,
|
|
|
- other?.web_search_call_count || 0,
|
|
|
- other?.web_search_price || 0,
|
|
|
- other?.file_search || false,
|
|
|
- other?.file_search_call_count || 0,
|
|
|
- other?.file_search_price || 0,
|
|
|
- other?.audio_input_seperate_price || false,
|
|
|
- other?.audio_input_token_count || 0,
|
|
|
- other?.audio_input_price || 0,
|
|
|
- );
|
|
|
- }
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('计费过程'),
|
|
|
- value: content,
|
|
|
- });
|
|
|
- if (other?.reasoning_effort) {
|
|
|
- expandDataLocal.push({
|
|
|
- key: t('Reasoning Effort'),
|
|
|
- value: other.reasoning_effort,
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- expandDatesLocal[logs[i].key] = expandDataLocal;
|
|
|
- }
|
|
|
-
|
|
|
- setExpandData(expandDatesLocal);
|
|
|
- setLogs(logs);
|
|
|
- };
|
|
|
-
|
|
|
- // Load logs function
|
|
|
- const loadLogs = async (startIdx, pageSize, customLogType = null) => {
|
|
|
- setLoading(true);
|
|
|
-
|
|
|
- let url = '';
|
|
|
- const {
|
|
|
- username,
|
|
|
- token_name,
|
|
|
- model_name,
|
|
|
- start_timestamp,
|
|
|
- end_timestamp,
|
|
|
- channel,
|
|
|
- group,
|
|
|
- logType: formLogType,
|
|
|
- } = getFormValues();
|
|
|
-
|
|
|
- const currentLogType =
|
|
|
- customLogType !== null
|
|
|
- ? customLogType
|
|
|
- : formLogType !== undefined
|
|
|
- ? formLogType
|
|
|
- : logType;
|
|
|
-
|
|
|
- let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
- let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
- if (isAdminUser) {
|
|
|
- url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
|
|
- } else {
|
|
|
- url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
|
|
- }
|
|
|
- url = encodeURI(url);
|
|
|
- const res = await API.get(url);
|
|
|
- const { success, message, data } = res.data;
|
|
|
- if (success) {
|
|
|
- const newPageData = data.items;
|
|
|
- setActivePage(data.page);
|
|
|
- setPageSize(data.page_size);
|
|
|
- setLogCount(data.total);
|
|
|
-
|
|
|
- setLogsFormat(newPageData);
|
|
|
- } else {
|
|
|
- showError(message);
|
|
|
- }
|
|
|
- setLoading(false);
|
|
|
- };
|
|
|
-
|
|
|
- // Page handlers
|
|
|
- const handlePageChange = (page) => {
|
|
|
- setActivePage(page);
|
|
|
- loadLogs(page, pageSize).then((r) => { });
|
|
|
- };
|
|
|
-
|
|
|
- const handlePageSizeChange = async (size) => {
|
|
|
- localStorage.setItem('page-size', size + '');
|
|
|
- setPageSize(size);
|
|
|
- setActivePage(1);
|
|
|
- loadLogs(activePage, size)
|
|
|
- .then()
|
|
|
- .catch((reason) => {
|
|
|
- showError(reason);
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- // Refresh function
|
|
|
- const refresh = async () => {
|
|
|
- setActivePage(1);
|
|
|
- handleEyeClick();
|
|
|
- await loadLogs(1, pageSize);
|
|
|
- };
|
|
|
-
|
|
|
- // Copy text function
|
|
|
- const copyText = async (e, text) => {
|
|
|
- e.stopPropagation();
|
|
|
- if (await copy(text)) {
|
|
|
- showSuccess('已复制:' + text);
|
|
|
- } else {
|
|
|
- Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // Initialize data
|
|
|
- useEffect(() => {
|
|
|
- const localPageSize =
|
|
|
- parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
|
|
|
- setPageSize(localPageSize);
|
|
|
- loadLogs(activePage, localPageSize)
|
|
|
- .then()
|
|
|
- .catch((reason) => {
|
|
|
- showError(reason);
|
|
|
- });
|
|
|
- }, []);
|
|
|
-
|
|
|
- // Initialize statistics when formApi is available
|
|
|
- useEffect(() => {
|
|
|
- if (formApi) {
|
|
|
- handleEyeClick();
|
|
|
- }
|
|
|
- }, [formApi]);
|
|
|
-
|
|
|
- // Check if any record has expandable content
|
|
|
- const hasExpandableRows = () => {
|
|
|
- return logs.some(
|
|
|
- (log) => expandData[log.key] && expandData[log.key].length > 0,
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- return {
|
|
|
- // Basic state
|
|
|
- logs,
|
|
|
- expandData,
|
|
|
- showStat,
|
|
|
- loading,
|
|
|
- loadingStat,
|
|
|
- activePage,
|
|
|
- logCount,
|
|
|
- pageSize,
|
|
|
- logType,
|
|
|
- stat,
|
|
|
- isAdminUser,
|
|
|
-
|
|
|
- // Form state
|
|
|
- formApi,
|
|
|
- setFormApi,
|
|
|
- formInitValues,
|
|
|
- getFormValues,
|
|
|
-
|
|
|
- // Column visibility
|
|
|
- visibleColumns,
|
|
|
- showColumnSelector,
|
|
|
- setShowColumnSelector,
|
|
|
- handleColumnVisibilityChange,
|
|
|
- handleSelectAll,
|
|
|
- initDefaultColumns,
|
|
|
- COLUMN_KEYS,
|
|
|
-
|
|
|
- // Compact mode
|
|
|
- compactMode,
|
|
|
- setCompactMode,
|
|
|
-
|
|
|
- // User info modal
|
|
|
- showUserInfo,
|
|
|
- setShowUserInfoModal,
|
|
|
- userInfoData,
|
|
|
- showUserInfoFunc,
|
|
|
-
|
|
|
- // Functions
|
|
|
- loadLogs,
|
|
|
- handlePageChange,
|
|
|
- handlePageSizeChange,
|
|
|
- refresh,
|
|
|
- copyText,
|
|
|
- handleEyeClick,
|
|
|
- setLogsFormat,
|
|
|
- hasExpandableRows,
|
|
|
- setLogType,
|
|
|
-
|
|
|
- // Translation
|
|
|
- t,
|
|
|
- };
|
|
|
+ const { t } = useTranslation();
|
|
|
+
|
|
|
+ // Define column keys for selection
|
|
|
+ const COLUMN_KEYS = {
|
|
|
+ TIME: 'time',
|
|
|
+ CHANNEL: 'channel',
|
|
|
+ USERNAME: 'username',
|
|
|
+ TOKEN: 'token',
|
|
|
+ GROUP: 'group',
|
|
|
+ TYPE: 'type',
|
|
|
+ MODEL: 'model',
|
|
|
+ USE_TIME: 'use_time',
|
|
|
+ PROMPT: 'prompt',
|
|
|
+ COMPLETION: 'completion',
|
|
|
+ COST: 'cost',
|
|
|
+ RETRY: 'retry',
|
|
|
+ IP: 'ip',
|
|
|
+ DETAILS: 'details',
|
|
|
+ };
|
|
|
+
|
|
|
+ // Basic state
|
|
|
+ const [logs, setLogs] = useState([]);
|
|
|
+ const [expandData, setExpandData] = useState({});
|
|
|
+ const [showStat, setShowStat] = useState(false);
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [loadingStat, setLoadingStat] = useState(false);
|
|
|
+ const [activePage, setActivePage] = useState(1);
|
|
|
+ const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
|
|
|
+ const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
|
|
+ const [logType, setLogType] = useState(0);
|
|
|
+
|
|
|
+ // User and admin
|
|
|
+ const isAdminUser = isAdmin();
|
|
|
+
|
|
|
+ // Statistics state
|
|
|
+ const [stat, setStat] = useState({
|
|
|
+ quota: 0,
|
|
|
+ token: 0,
|
|
|
+ });
|
|
|
+
|
|
|
+ // Form state
|
|
|
+ const [formApi, setFormApi] = useState(null);
|
|
|
+ let now = new Date();
|
|
|
+ const formInitValues = {
|
|
|
+ username: '',
|
|
|
+ token_name: '',
|
|
|
+ model_name: '',
|
|
|
+ channel: '',
|
|
|
+ group: '',
|
|
|
+ dateRange: [
|
|
|
+ timestamp2string(getTodayStartTimestamp()),
|
|
|
+ timestamp2string(now.getTime() / 1000 + 3600),
|
|
|
+ ],
|
|
|
+ logType: '0',
|
|
|
+ };
|
|
|
+
|
|
|
+ // Column visibility state
|
|
|
+ const [visibleColumns, setVisibleColumns] = useState({});
|
|
|
+ const [showColumnSelector, setShowColumnSelector] = useState(false);
|
|
|
+
|
|
|
+ // Compact mode
|
|
|
+ const [compactMode, setCompactMode] = useTableCompactMode('logs');
|
|
|
+
|
|
|
+ // User info modal state
|
|
|
+ const [showUserInfo, setShowUserInfoModal] = useState(false);
|
|
|
+ const [userInfoData, setUserInfoData] = useState(null);
|
|
|
+
|
|
|
+ // Load saved column preferences from localStorage
|
|
|
+ useEffect(() => {
|
|
|
+ const savedColumns = localStorage.getItem('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.TIME]: true,
|
|
|
+ [COLUMN_KEYS.CHANNEL]: isAdminUser,
|
|
|
+ [COLUMN_KEYS.USERNAME]: isAdminUser,
|
|
|
+ [COLUMN_KEYS.TOKEN]: true,
|
|
|
+ [COLUMN_KEYS.GROUP]: true,
|
|
|
+ [COLUMN_KEYS.TYPE]: true,
|
|
|
+ [COLUMN_KEYS.MODEL]: true,
|
|
|
+ [COLUMN_KEYS.USE_TIME]: true,
|
|
|
+ [COLUMN_KEYS.PROMPT]: true,
|
|
|
+ [COLUMN_KEYS.COMPLETION]: true,
|
|
|
+ [COLUMN_KEYS.COST]: true,
|
|
|
+ [COLUMN_KEYS.RETRY]: isAdminUser,
|
|
|
+ [COLUMN_KEYS.IP]: true,
|
|
|
+ [COLUMN_KEYS.DETAILS]: true,
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ // Initialize default column visibility
|
|
|
+ const initDefaultColumns = () => {
|
|
|
+ const defaults = getDefaultColumnVisibility();
|
|
|
+ setVisibleColumns(defaults);
|
|
|
+ localStorage.setItem('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.USERNAME ||
|
|
|
+ key === COLUMN_KEYS.RETRY) &&
|
|
|
+ !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(
|
|
|
+ 'logs-table-columns',
|
|
|
+ JSON.stringify(visibleColumns),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }, [visibleColumns]);
|
|
|
+
|
|
|
+ // 获取表单值的辅助函数,确保所有值都是字符串
|
|
|
+ const getFormValues = () => {
|
|
|
+ const formValues = formApi ? formApi.getValues() : {};
|
|
|
+
|
|
|
+ let start_timestamp = timestamp2string(getTodayStartTimestamp());
|
|
|
+ let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
|
|
|
+
|
|
|
+ if (
|
|
|
+ formValues.dateRange &&
|
|
|
+ Array.isArray(formValues.dateRange) &&
|
|
|
+ formValues.dateRange.length === 2
|
|
|
+ ) {
|
|
|
+ start_timestamp = formValues.dateRange[0];
|
|
|
+ end_timestamp = formValues.dateRange[1];
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ username: formValues.username || '',
|
|
|
+ token_name: formValues.token_name || '',
|
|
|
+ model_name: formValues.model_name || '',
|
|
|
+ start_timestamp,
|
|
|
+ end_timestamp,
|
|
|
+ channel: formValues.channel || '',
|
|
|
+ group: formValues.group || '',
|
|
|
+ logType: formValues.logType ? parseInt(formValues.logType) : 0,
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ // Statistics functions
|
|
|
+ const getLogSelfStat = async () => {
|
|
|
+ const {
|
|
|
+ token_name,
|
|
|
+ model_name,
|
|
|
+ start_timestamp,
|
|
|
+ end_timestamp,
|
|
|
+ group,
|
|
|
+ logType: formLogType,
|
|
|
+ } = getFormValues();
|
|
|
+ const currentLogType = formLogType !== undefined ? formLogType : logType;
|
|
|
+ let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
+ let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
+ let url = `/api/log/self/stat?type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
|
|
+ url = encodeURI(url);
|
|
|
+ let res = await API.get(url);
|
|
|
+ const { success, message, data } = res.data;
|
|
|
+ if (success) {
|
|
|
+ setStat(data);
|
|
|
+ } else {
|
|
|
+ showError(message);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const getLogStat = async () => {
|
|
|
+ const {
|
|
|
+ username,
|
|
|
+ token_name,
|
|
|
+ model_name,
|
|
|
+ start_timestamp,
|
|
|
+ end_timestamp,
|
|
|
+ channel,
|
|
|
+ group,
|
|
|
+ logType: formLogType,
|
|
|
+ } = getFormValues();
|
|
|
+ const currentLogType = formLogType !== undefined ? formLogType : logType;
|
|
|
+ let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
+ let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
+ let url = `/api/log/stat?type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
|
|
+ url = encodeURI(url);
|
|
|
+ let res = await API.get(url);
|
|
|
+ const { success, message, data } = res.data;
|
|
|
+ if (success) {
|
|
|
+ setStat(data);
|
|
|
+ } else {
|
|
|
+ showError(message);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleEyeClick = async () => {
|
|
|
+ if (loadingStat) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setLoadingStat(true);
|
|
|
+ if (isAdminUser) {
|
|
|
+ await getLogStat();
|
|
|
+ } else {
|
|
|
+ await getLogSelfStat();
|
|
|
+ }
|
|
|
+ setShowStat(true);
|
|
|
+ setLoadingStat(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ // User info function
|
|
|
+ const showUserInfoFunc = async (userId) => {
|
|
|
+ if (!isAdminUser) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const res = await API.get(`/api/user/${userId}`);
|
|
|
+ const { success, message, data } = res.data;
|
|
|
+ if (success) {
|
|
|
+ setUserInfoData(data);
|
|
|
+ setShowUserInfoModal(true);
|
|
|
+ } else {
|
|
|
+ showError(message);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Format logs data
|
|
|
+ const setLogsFormat = (logs) => {
|
|
|
+ let expandDatesLocal = {};
|
|
|
+ for (let i = 0; i < logs.length; i++) {
|
|
|
+ logs[i].timestamp2string = timestamp2string(logs[i].created_at);
|
|
|
+ logs[i].key = logs[i].id;
|
|
|
+ let other = getLogOther(logs[i].other);
|
|
|
+ let expandDataLocal = [];
|
|
|
+
|
|
|
+ if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('渠道信息'),
|
|
|
+ value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (other?.ws || other?.audio) {
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('语音输入'),
|
|
|
+ value: other.audio_input,
|
|
|
+ });
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('语音输出'),
|
|
|
+ value: other.audio_output,
|
|
|
+ });
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('文字输入'),
|
|
|
+ value: other.text_input,
|
|
|
+ });
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('文字输出'),
|
|
|
+ value: other.text_output,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (other?.cache_tokens > 0) {
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('缓存 Tokens'),
|
|
|
+ value: other.cache_tokens,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (other?.cache_creation_tokens > 0) {
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('缓存创建 Tokens'),
|
|
|
+ value: other.cache_creation_tokens,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (logs[i].type === 2) {
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('日志详情'),
|
|
|
+ value: other?.claude
|
|
|
+ ? renderClaudeLogContent(
|
|
|
+ other?.model_ratio,
|
|
|
+ other.completion_ratio,
|
|
|
+ other.model_price,
|
|
|
+ other.group_ratio,
|
|
|
+ other?.user_group_ratio,
|
|
|
+ other.cache_ratio || 1.0,
|
|
|
+ other.cache_creation_ratio || 1.0,
|
|
|
+ )
|
|
|
+ : renderLogContent(
|
|
|
+ other?.model_ratio,
|
|
|
+ other.completion_ratio,
|
|
|
+ other.model_price,
|
|
|
+ other.group_ratio,
|
|
|
+ other?.user_group_ratio,
|
|
|
+ false,
|
|
|
+ 1.0,
|
|
|
+ other.web_search || false,
|
|
|
+ other.web_search_call_count || 0,
|
|
|
+ other.file_search || false,
|
|
|
+ other.file_search_call_count || 0,
|
|
|
+ ),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (logs[i].type === 2) {
|
|
|
+ let modelMapped =
|
|
|
+ other?.is_model_mapped &&
|
|
|
+ other?.upstream_model_name &&
|
|
|
+ other?.upstream_model_name !== '';
|
|
|
+ if (modelMapped) {
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('请求并计费模型'),
|
|
|
+ value: logs[i].model_name,
|
|
|
+ });
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('实际模型'),
|
|
|
+ value: other.upstream_model_name,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ let content = '';
|
|
|
+ if (other?.ws || other?.audio) {
|
|
|
+ content = renderAudioModelPrice(
|
|
|
+ other?.text_input,
|
|
|
+ other?.text_output,
|
|
|
+ other?.model_ratio,
|
|
|
+ other?.model_price,
|
|
|
+ other?.completion_ratio,
|
|
|
+ other?.audio_input,
|
|
|
+ other?.audio_output,
|
|
|
+ other?.audio_ratio,
|
|
|
+ other?.audio_completion_ratio,
|
|
|
+ other?.group_ratio,
|
|
|
+ other?.user_group_ratio,
|
|
|
+ other?.cache_tokens || 0,
|
|
|
+ other?.cache_ratio || 1.0,
|
|
|
+ );
|
|
|
+ } else if (other?.claude) {
|
|
|
+ content = renderClaudeModelPrice(
|
|
|
+ logs[i].prompt_tokens,
|
|
|
+ logs[i].completion_tokens,
|
|
|
+ other.model_ratio,
|
|
|
+ other.model_price,
|
|
|
+ other.completion_ratio,
|
|
|
+ other.group_ratio,
|
|
|
+ other?.user_group_ratio,
|
|
|
+ other.cache_tokens || 0,
|
|
|
+ other.cache_ratio || 1.0,
|
|
|
+ other.cache_creation_tokens || 0,
|
|
|
+ other.cache_creation_ratio || 1.0,
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ content = renderModelPrice(
|
|
|
+ logs[i].prompt_tokens,
|
|
|
+ logs[i].completion_tokens,
|
|
|
+ other?.model_ratio,
|
|
|
+ other?.model_price,
|
|
|
+ other?.completion_ratio,
|
|
|
+ other?.group_ratio,
|
|
|
+ other?.user_group_ratio,
|
|
|
+ other?.cache_tokens || 0,
|
|
|
+ other?.cache_ratio || 1.0,
|
|
|
+ other?.image || false,
|
|
|
+ other?.image_ratio || 0,
|
|
|
+ other?.image_output || 0,
|
|
|
+ other?.web_search || false,
|
|
|
+ other?.web_search_call_count || 0,
|
|
|
+ other?.web_search_price || 0,
|
|
|
+ other?.file_search || false,
|
|
|
+ other?.file_search_call_count || 0,
|
|
|
+ other?.file_search_price || 0,
|
|
|
+ other?.audio_input_seperate_price || false,
|
|
|
+ other?.audio_input_token_count || 0,
|
|
|
+ other?.audio_input_price || 0,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('计费过程'),
|
|
|
+ value: content,
|
|
|
+ });
|
|
|
+ if (other?.reasoning_effort) {
|
|
|
+ expandDataLocal.push({
|
|
|
+ key: t('Reasoning Effort'),
|
|
|
+ value: other.reasoning_effort,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ expandDatesLocal[logs[i].key] = expandDataLocal;
|
|
|
+ }
|
|
|
+
|
|
|
+ setExpandData(expandDatesLocal);
|
|
|
+ setLogs(logs);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Load logs function
|
|
|
+ const loadLogs = async (startIdx, pageSize, customLogType = null) => {
|
|
|
+ setLoading(true);
|
|
|
+
|
|
|
+ let url = '';
|
|
|
+ const {
|
|
|
+ username,
|
|
|
+ token_name,
|
|
|
+ model_name,
|
|
|
+ start_timestamp,
|
|
|
+ end_timestamp,
|
|
|
+ channel,
|
|
|
+ group,
|
|
|
+ logType: formLogType,
|
|
|
+ } = getFormValues();
|
|
|
+
|
|
|
+ const currentLogType =
|
|
|
+ customLogType !== null
|
|
|
+ ? customLogType
|
|
|
+ : formLogType !== undefined
|
|
|
+ ? formLogType
|
|
|
+ : logType;
|
|
|
+
|
|
|
+ let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
+ let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
+ if (isAdminUser) {
|
|
|
+ url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
|
|
+ } else {
|
|
|
+ url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
|
|
+ }
|
|
|
+ url = encodeURI(url);
|
|
|
+ const res = await API.get(url);
|
|
|
+ const { success, message, data } = res.data;
|
|
|
+ if (success) {
|
|
|
+ const newPageData = data.items;
|
|
|
+ setActivePage(data.page);
|
|
|
+ setPageSize(data.page_size);
|
|
|
+ setLogCount(data.total);
|
|
|
+
|
|
|
+ setLogsFormat(newPageData);
|
|
|
+ } else {
|
|
|
+ showError(message);
|
|
|
+ }
|
|
|
+ setLoading(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Page handlers
|
|
|
+ const handlePageChange = (page) => {
|
|
|
+ setActivePage(page);
|
|
|
+ loadLogs(page, pageSize).then((r) => { });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handlePageSizeChange = async (size) => {
|
|
|
+ localStorage.setItem('page-size', size + '');
|
|
|
+ setPageSize(size);
|
|
|
+ setActivePage(1);
|
|
|
+ loadLogs(activePage, size)
|
|
|
+ .then()
|
|
|
+ .catch((reason) => {
|
|
|
+ showError(reason);
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // Refresh function
|
|
|
+ const refresh = async () => {
|
|
|
+ setActivePage(1);
|
|
|
+ handleEyeClick();
|
|
|
+ await loadLogs(1, pageSize);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Copy text function
|
|
|
+ const copyText = async (e, text) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ if (await copy(text)) {
|
|
|
+ showSuccess('已复制:' + text);
|
|
|
+ } else {
|
|
|
+ Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Initialize data
|
|
|
+ useEffect(() => {
|
|
|
+ const localPageSize =
|
|
|
+ parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
|
|
|
+ setPageSize(localPageSize);
|
|
|
+ loadLogs(activePage, localPageSize)
|
|
|
+ .then()
|
|
|
+ .catch((reason) => {
|
|
|
+ showError(reason);
|
|
|
+ });
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // Initialize statistics when formApi is available
|
|
|
+ useEffect(() => {
|
|
|
+ if (formApi) {
|
|
|
+ handleEyeClick();
|
|
|
+ }
|
|
|
+ }, [formApi]);
|
|
|
+
|
|
|
+ // Check if any record has expandable content
|
|
|
+ const hasExpandableRows = () => {
|
|
|
+ return logs.some(
|
|
|
+ (log) => expandData[log.key] && expandData[log.key].length > 0,
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ // Basic state
|
|
|
+ logs,
|
|
|
+ expandData,
|
|
|
+ showStat,
|
|
|
+ loading,
|
|
|
+ loadingStat,
|
|
|
+ activePage,
|
|
|
+ logCount,
|
|
|
+ pageSize,
|
|
|
+ logType,
|
|
|
+ stat,
|
|
|
+ isAdminUser,
|
|
|
+
|
|
|
+ // Form state
|
|
|
+ formApi,
|
|
|
+ setFormApi,
|
|
|
+ formInitValues,
|
|
|
+ getFormValues,
|
|
|
+
|
|
|
+ // Column visibility
|
|
|
+ visibleColumns,
|
|
|
+ showColumnSelector,
|
|
|
+ setShowColumnSelector,
|
|
|
+ handleColumnVisibilityChange,
|
|
|
+ handleSelectAll,
|
|
|
+ initDefaultColumns,
|
|
|
+ COLUMN_KEYS,
|
|
|
+
|
|
|
+ // Compact mode
|
|
|
+ compactMode,
|
|
|
+ setCompactMode,
|
|
|
+
|
|
|
+ // User info modal
|
|
|
+ showUserInfo,
|
|
|
+ setShowUserInfoModal,
|
|
|
+ userInfoData,
|
|
|
+ showUserInfoFunc,
|
|
|
+
|
|
|
+ // Functions
|
|
|
+ loadLogs,
|
|
|
+ handlePageChange,
|
|
|
+ handlePageSizeChange,
|
|
|
+ refresh,
|
|
|
+ copyText,
|
|
|
+ handleEyeClick,
|
|
|
+ setLogsFormat,
|
|
|
+ hasExpandableRows,
|
|
|
+ setLogType,
|
|
|
+
|
|
|
+ // Translation
|
|
|
+ t,
|
|
|
+ };
|
|
|
};
|