v 1.1.2 生图包管理界面
This commit is contained in:
@@ -0,0 +1,557 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Row, Col, Button, Typography, Space, message, Layout, Modal, Image } from 'antd';
|
||||
import { useGlassButtonStyles } from '@/hooks/useGlassButtonStyles';
|
||||
import CustomFooter from '@/components/Footer/index';
|
||||
import { CustomerServiceOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { systemConfig } from '../../../config/systemConfig';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
// 简化的套餐类型定义
|
||||
interface PricePackage {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
period: string;
|
||||
dailyQuota: number;
|
||||
concurrent: number;
|
||||
validDays: number;
|
||||
recommended?: boolean;
|
||||
unitPrice?: number; // 可选的单价
|
||||
}
|
||||
|
||||
const MJPriceInfo: React.FC = () => {
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
const { Header, Content, Footer } = Layout;
|
||||
|
||||
// 添加模态框状态
|
||||
const [isContactModalVisible, setIsContactModalVisible] = useState(false);
|
||||
|
||||
// 简化的套餐数据
|
||||
const packages: PricePackage[] = [
|
||||
{
|
||||
id: 'basic_1',
|
||||
name: '基础版_1',
|
||||
price: 120,
|
||||
currency: '¥',
|
||||
period: '月',
|
||||
dailyQuota: 150,
|
||||
concurrent: 6,
|
||||
validDays: 30,
|
||||
unitPrice: 0.0266,
|
||||
},
|
||||
{
|
||||
id: 'basic_2',
|
||||
name: '基础版_2',
|
||||
price: 149,
|
||||
currency: '¥',
|
||||
period: '月',
|
||||
dailyQuota: 200,
|
||||
concurrent: 6,
|
||||
validDays: 30,
|
||||
recommended: true,
|
||||
unitPrice: 0.0248
|
||||
},
|
||||
{
|
||||
id: 'basic_3',
|
||||
name: '基础版_3',
|
||||
price: 219,
|
||||
currency: '¥',
|
||||
period: '月',
|
||||
dailyQuota: 300,
|
||||
concurrent: 6,
|
||||
validDays: 30,
|
||||
unitPrice: 0.0243
|
||||
},
|
||||
{
|
||||
id: 'basic_4',
|
||||
name: '基础版_4',
|
||||
price: 279,
|
||||
currency: '¥',
|
||||
period: '月',
|
||||
dailyQuota: 400,
|
||||
concurrent: 6,
|
||||
validDays: 30,
|
||||
unitPrice: 0.023
|
||||
}, {
|
||||
id: 'pro_1',
|
||||
name: '高级版_1',
|
||||
price: 135,
|
||||
currency: '¥',
|
||||
period: '月',
|
||||
dailyQuota: 150,
|
||||
concurrent: 10,
|
||||
validDays: 30,
|
||||
unitPrice: 0.03,
|
||||
},
|
||||
{
|
||||
id: 'pro_2',
|
||||
name: '高级版_2',
|
||||
price: 165,
|
||||
currency: '¥',
|
||||
period: '月',
|
||||
dailyQuota: 200,
|
||||
concurrent: 10,
|
||||
validDays: 30,
|
||||
recommended: true,
|
||||
unitPrice: 0.0275
|
||||
},
|
||||
{
|
||||
id: 'pro_3',
|
||||
name: '高级版_3',
|
||||
price: 240,
|
||||
currency: '¥',
|
||||
period: '月',
|
||||
dailyQuota: 300,
|
||||
concurrent: 10,
|
||||
validDays: 30,
|
||||
unitPrice: 0.0266
|
||||
},
|
||||
{
|
||||
id: 'pro_4',
|
||||
name: '高级版_4',
|
||||
price: 299,
|
||||
currency: '¥',
|
||||
period: '月',
|
||||
dailyQuota: 400,
|
||||
concurrent: 10,
|
||||
validDays: 30,
|
||||
unitPrice: 0.0249
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
// 显示联系客服模态框
|
||||
const showContactModal = () => {
|
||||
setIsContactModalVisible(true);
|
||||
};
|
||||
|
||||
// 关闭联系客服模态框
|
||||
const handleContactModalClose = () => {
|
||||
setIsContactModalVisible(false);
|
||||
};
|
||||
|
||||
const renderPackageCard = (pkg: PricePackage) => {
|
||||
return (
|
||||
|
||||
<Card
|
||||
key={pkg.id}
|
||||
style={{
|
||||
height: '360px',
|
||||
border: pkg.recommended ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
borderRadius: '12px',
|
||||
boxShadow: pkg.recommended ? '0 8px 24px rgba(24,144,255,0.2)' : '0 2px 8px rgba(0,0,0,0.1)',
|
||||
transform: pkg.recommended ? 'scale(1.03)' : 'scale(1)',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
const card = e.currentTarget as HTMLElement;
|
||||
if (pkg.recommended) {
|
||||
card.style.transform = 'scale(1.08)';
|
||||
card.style.boxShadow = '0 16px 40px rgba(24,144,255,0.3)';
|
||||
card.style.borderColor = '#40a9ff';
|
||||
} else {
|
||||
card.style.transform = 'scale(1.05) translateY(-8px)';
|
||||
card.style.boxShadow = '0 12px 32px rgba(0,0,0,0.15)';
|
||||
card.style.borderColor = '#40a9ff';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const card = e.currentTarget as HTMLElement;
|
||||
if (pkg.recommended) {
|
||||
card.style.transform = 'scale(1.03)';
|
||||
card.style.boxShadow = '0 8px 24px rgba(24,144,255,0.2)';
|
||||
card.style.borderColor = '#1890ff';
|
||||
} else {
|
||||
card.style.transform = 'scale(1) translateY(0)';
|
||||
card.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||
card.style.borderColor = '#d9d9d9';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 推荐标签 */}
|
||||
{pkg.recommended && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '16px',
|
||||
right: '-30px',
|
||||
backgroundColor: '#1890ff',
|
||||
color: 'white',
|
||||
padding: '4px 40px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
transform: 'rotate(45deg)',
|
||||
transformOrigin: 'center',
|
||||
zIndex: 2
|
||||
}}>
|
||||
推荐
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{/* 套餐名称 */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
|
||||
<Title level={2} style={{
|
||||
margin: 0,
|
||||
color: pkg.recommended ? '#1890ff' : '#333',
|
||||
fontSize: '28px',
|
||||
transition: 'color 0.3s ease'
|
||||
}}>
|
||||
{pkg.name}
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
{/* 价格 */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: '4px' }}>
|
||||
<span style={{
|
||||
fontSize: '20px',
|
||||
color: '#666',
|
||||
transition: 'color 0.3s ease'
|
||||
}}>
|
||||
{pkg.currency}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '56px',
|
||||
fontWeight: 'bold',
|
||||
color: pkg.recommended ? '#1890ff' : '#333',
|
||||
lineHeight: 1,
|
||||
transition: 'color 0.3s ease'
|
||||
}}>
|
||||
{pkg.price === 0 ? '免费' : pkg.price}
|
||||
</span>
|
||||
{pkg.price > 0 && (
|
||||
<span style={{
|
||||
fontSize: '18px',
|
||||
color: '#666',
|
||||
transition: 'color 0.3s ease'
|
||||
}}>
|
||||
/{pkg.period}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 核心信息 */}
|
||||
<div style={{ marginBottom: '32px' }}>
|
||||
<Row gutter={[0, 6]}>
|
||||
<Col span={24}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
className="info-row"
|
||||
>
|
||||
<Text style={{ fontSize: '14px', color: '#666' }}>每日额度</Text>
|
||||
<Text strong style={{
|
||||
fontSize: '18px',
|
||||
color: pkg.recommended ? '#1890ff' : '#333',
|
||||
transition: 'color 0.3s ease'
|
||||
}}>
|
||||
{pkg.dailyQuota === -1 ? '∞' : pkg.dailyQuota}
|
||||
</Text>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
className="info-row"
|
||||
>
|
||||
<Text style={{ fontSize: '14px', color: '#666' }}>并发数</Text>
|
||||
<Text strong style={{
|
||||
fontSize: '18px',
|
||||
color: pkg.recommended ? '#1890ff' : '#333',
|
||||
transition: 'color 0.3s ease'
|
||||
}}>
|
||||
{pkg.concurrent}
|
||||
</Text>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
className="info-row"
|
||||
>
|
||||
<Text style={{ fontSize: '14px', color: '#666' }}>有效天数</Text>
|
||||
<Text strong style={{
|
||||
fontSize: '18px',
|
||||
color: pkg.recommended ? '#1890ff' : '#333',
|
||||
transition: 'color 0.3s ease'
|
||||
}}>
|
||||
{pkg.validDays}
|
||||
</Text>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
className="info-row"
|
||||
>
|
||||
<Text style={{ fontSize: '14px', color: '#666' }}>单图价格</Text>
|
||||
<Text strong style={{
|
||||
fontSize: '18px',
|
||||
color: pkg.recommended ? '#1890ff' : '#333',
|
||||
transition: 'color 0.3s ease'
|
||||
}}>
|
||||
{pkg.unitPrice}
|
||||
</Text>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '0 24px',
|
||||
backgroundColor: '#fafafa',
|
||||
minHeight: '100vh'
|
||||
}}>
|
||||
{/* 添加 CSS 样式 */}
|
||||
<style>
|
||||
{`
|
||||
.info-row:hover {
|
||||
background-color: #e6f4ff !important;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.ant-card:hover .info-row {
|
||||
background-color: #e6f4ff;
|
||||
}
|
||||
|
||||
.ant-card:hover .ant-typography {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
|
||||
.ant-card:hover .ant-btn-default {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
{messageHolder}
|
||||
<Layout className="task-layout">
|
||||
<Content>
|
||||
{/* 页面标题 */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '60px' }}>
|
||||
<Title level={1} style={{
|
||||
color: '#1890ff',
|
||||
marginBottom: '16px',
|
||||
fontSize: '42px'
|
||||
}}>
|
||||
选择您的套餐
|
||||
</Title>
|
||||
<Text style={{
|
||||
fontSize: '18px',
|
||||
color: '#666'
|
||||
}}>
|
||||
简单透明的价格,强大的AI绘图服务
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 套餐网格 */}
|
||||
<div style={{ maxWidth: '1400px', margin: '0 auto' }}>
|
||||
<Row gutter={[24, 24]} justify="center">
|
||||
{packages.map(pkg => (
|
||||
<Col
|
||||
key={pkg.id}
|
||||
xs={24}
|
||||
sm={12}
|
||||
md={12}
|
||||
lg={6}
|
||||
xl={6}
|
||||
>
|
||||
{renderPackageCard(pkg)}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 底部说明 */}
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '80px',
|
||||
padding: '40px',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.08)'
|
||||
}}>
|
||||
<Title level={3} style={{ marginBottom: '16px', color: '#333' }}>
|
||||
需要帮助?
|
||||
</Title>
|
||||
<Text style={{ color: '#666', fontSize: '16px', marginBottom: '24px', display: 'block' }}>
|
||||
我们的团队随时为您提供支持
|
||||
</Text>
|
||||
<Space size="large">
|
||||
<Button size="large" style={{ borderRadius: '8px' }}>
|
||||
常见问题
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<CustomerServiceOutlined />}
|
||||
onClick={showContactModal}
|
||||
style={{ borderRadius: '8px' }}
|
||||
>
|
||||
联系客服
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
{/* 修复 Footer 样式 */}
|
||||
<Footer style={{
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '24px 0',
|
||||
backgroundColor: 'transparent'
|
||||
}}>
|
||||
<CustomFooter />
|
||||
</Footer>
|
||||
</Layout>
|
||||
|
||||
{/* 联系客服模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<CustomerServiceOutlined style={{ color: '#1890ff', fontSize: '18px' }} />
|
||||
<span style={{ fontSize: '18px', fontWeight: 'bold' }}>联系客服</span>
|
||||
</div>
|
||||
}
|
||||
open={isContactModalVisible}
|
||||
onCancel={handleContactModalClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
centered
|
||||
closeIcon={<CloseOutlined style={{ color: '#666', fontSize: '16px' }} />}
|
||||
styles={{
|
||||
body: {
|
||||
padding: '24px',
|
||||
textAlign: 'center'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<Text style={{ fontSize: '16px', color: '#666', display: 'block', marginBottom: '16px' }}>
|
||||
扫描下方二维码,添加客服微信获取专业帮助
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 客服二维码图片 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '24px',
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '12px'
|
||||
}}>
|
||||
<Image
|
||||
width={280}
|
||||
src={systemConfig.system.kefu}
|
||||
alt="客服微信二维码"
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
border: '2px solid #e8e8e8'
|
||||
}}
|
||||
placeholder={
|
||||
<div style={{
|
||||
width: 280,
|
||||
backgroundColor: '#f5f5f5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
<Text style={{ color: '#999' }}>加载中...</Text>
|
||||
</div>
|
||||
}
|
||||
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3Ik1RnG4W+FgYxNLuVFV+D2CgzY+RKOHZBdO/YSjJ1gO3YMhgQGBiSwCxshOzAEtjOArtTlFQjdF77Y0/I"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 其他联系方式 */}
|
||||
{/* <div style={{
|
||||
backgroundColor: '#fff7e6',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ffd591'
|
||||
}}>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<Text strong style={{ color: '#d48806', fontSize: '14px' }}>
|
||||
📱 客服热线:400-123-4567
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<Text strong style={{ color: '#d48806', fontSize: '14px' }}>
|
||||
⏰ 服务时间:9:00-18:00 (周一至周五)
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ color: '#d48806', fontSize: '14px' }}>
|
||||
📧 邮箱:support@example.com
|
||||
</Text>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* 底部按钮 */}
|
||||
{/* <div style={{ marginTop: '24px' }}>
|
||||
<Space size="middle">
|
||||
<Button onClick={handleContactModalClose}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
messageApi.success('已为您复制客服微信号');
|
||||
// 这里可以添加复制到剪贴板的逻辑
|
||||
navigator.clipboard.writeText('客服微信号');
|
||||
}}
|
||||
>
|
||||
复制微信号
|
||||
</Button>
|
||||
</Space>
|
||||
</div> */}
|
||||
</Modal>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default MJPriceInfo;
|
||||
@@ -0,0 +1,617 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Space,
|
||||
message,
|
||||
Tag,
|
||||
Row,
|
||||
Col,
|
||||
Form,
|
||||
Modal,
|
||||
Select,
|
||||
Tooltip
|
||||
} from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
CloseOutlined,
|
||||
ReloadOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||
import { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface';
|
||||
import { FormatDate } from '@/util/time';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { useGlassButtonStyles } from '@/hooks/useGlassButtonStyles';
|
||||
import { adminGetDayTaskStatistics, adminQueryTaskCollection } from '@/services/services/mjp';
|
||||
import { isEmpty } from 'lodash';
|
||||
import TaskInfo from './TokenManagement/TaskInfo';
|
||||
import { getStatusTag } from './TaskMessageInfo/TaskTable';
|
||||
|
||||
export interface QueryTaskParams {
|
||||
thirdPartyTaskId?: string;
|
||||
token?: string;
|
||||
tokenId?: string;
|
||||
}
|
||||
|
||||
const TaskManagement: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [statisticsLoading, setStatisticsLoading] = useState(false);
|
||||
const [dataSource, setDataSource] = useState<Array<MJP.MJApiTasks>>([]);
|
||||
const [simpleData, setSimpleData] = useState<BasicModel.QueryCollection<Array<MJP.MJApiTasks>>>();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// const [taskStats, setTaskStats] = useState<MJP.TaskStatsResponse>();
|
||||
const { getButtonStyle } = useGlassButtonStyles();
|
||||
|
||||
const [tableParams, setTableParams] = useState<TableModel.TableParams>({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
showQuickJumper: true,
|
||||
totalBoundaryShowSizeChanger: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
const [modalApi, modalHolder] = Modal.useModal();
|
||||
|
||||
const [viewModalVisible, setViewModalVisible] = useState(false);
|
||||
const [currentViewTask, setCurrentViewTask] = useState<MJP.MJApiTasks>({} as MJP.MJApiTasks);
|
||||
const [modalWidth, setModalWidth] = useState(800);
|
||||
|
||||
const [taskStatisticsData, setTaskStatisticsData] = useState<MJP.TaskStatistics>();
|
||||
|
||||
|
||||
// 统计数据
|
||||
const stats = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: "今日总任务数",
|
||||
value: taskStatisticsData?.totalTasks ?? 0,
|
||||
iconText: "📋"
|
||||
},
|
||||
{
|
||||
title: "今日处理中任务",
|
||||
value: taskStatisticsData?.inProgressTasks ?? 0,
|
||||
iconText: "⚡"
|
||||
},
|
||||
{
|
||||
title: "今日已完成任务",
|
||||
value: taskStatisticsData?.completedTasks ?? 0,
|
||||
iconText: "✅"
|
||||
},
|
||||
{
|
||||
title: "今日失败任务",
|
||||
value: taskStatisticsData?.failedTasks ?? 0,
|
||||
iconText: "❌"
|
||||
}
|
||||
];
|
||||
}, [simpleData, taskStatisticsData]);
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType<MJP.MJApiTasks> = [
|
||||
{
|
||||
title: '任务ID',
|
||||
dataIndex: 'taskId',
|
||||
key: 'taskId',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
render: (text: string) => (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '4px' }}>
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
wordBreak: 'break-all',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: '1.4',
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
{text}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Token ID',
|
||||
dataIndex: 'tokenId',
|
||||
key: 'tokenId',
|
||||
width: 80,
|
||||
render: (tokenId: number) => (
|
||||
<Tag color="blue">
|
||||
{tokenId}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'MJ任务ID',
|
||||
dataIndex: 'thirdPartyTaskId',
|
||||
key: 'thirdPartyTaskId',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '生图机器人',
|
||||
dataIndex: 'botType',
|
||||
key: 'botType',
|
||||
width: 120,
|
||||
render: (status: string, record: MJP.MJApiTasks) => {
|
||||
return record.propertieJson && record.propertieJson.botType ?
|
||||
<Tag color="purple">{record.propertieJson.botType}</Tag> : "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => (
|
||||
getStatusTag(status)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '提示词',
|
||||
dataIndex: 'prompt',
|
||||
key: 'prompt',
|
||||
width: 200,
|
||||
render: (_, record: MJP.MJApiTasks) => {
|
||||
return record.propertieJson && record.propertieJson.prompt ? (
|
||||
<Tooltip title={record.propertieJson.prompt} placement="topLeft">
|
||||
<div style={{
|
||||
maxWidth: '180px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{record.propertieJson.prompt}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span style={{ color: '#999' }}>暂无提示词</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
width: 100,
|
||||
render: (status: string, record: MJP.MJApiTasks) => {
|
||||
return record.propertieJson && record.propertieJson.progress ?
|
||||
<Tag color="purple">{record.propertieJson?.progress}</Tag> : "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
width: 150,
|
||||
render: (time: Date) => (
|
||||
<span >
|
||||
{FormatDate(time)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '完成时间',
|
||||
dataIndex: 'endTime',
|
||||
key: 'endTime',
|
||||
width: 150,
|
||||
render: (time: Date | null) => (
|
||||
<span >
|
||||
{time ? FormatDate(time) : '-'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '耗时',
|
||||
key: 'duration',
|
||||
width: 80,
|
||||
render: (_, record: MJP.MJApiTasks) => {
|
||||
if (!record.endTime || !record.startTime) {
|
||||
return <span style={{ color: '#ccc' }}>-</span>;
|
||||
}
|
||||
const duration = new Date(record.endTime).getTime() - new Date(record.startTime).getTime();
|
||||
const seconds = Math.floor(duration / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
|
||||
if (minutes > 0) {
|
||||
return <span >{minutes}分{seconds % 60}秒</span>;
|
||||
}
|
||||
return <span>{seconds}秒</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "失败原因",
|
||||
dataIndex: 'completeTime',
|
||||
key: 'completeTime',
|
||||
width: 150,
|
||||
render: (text: string, record: MJP.MJApiTasks) => {
|
||||
const promptText = record.propertieJson && record.propertieJson.failReason
|
||||
? record.propertieJson.failReason
|
||||
: '-';
|
||||
if (promptText === '-') {
|
||||
return <span style={{ color: '#999' }}>-</span>;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={promptText} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}>
|
||||
<div
|
||||
className="prompt-cell"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 4, // 显示2行
|
||||
lineHeight: '20px',
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{promptText}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
render: (_, record: MJP.MJApiTasks) => (
|
||||
<Space size={4}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => handleView(record)}
|
||||
style={{ ...getButtonStyle('primary').getStyle(), fontSize: '12px' }}
|
||||
onMouseEnter={(e) => getButtonStyle('primary').getMouseEnterStyle(e)}
|
||||
onMouseLeave={(e) => getButtonStyle('primary').getMouseLeaveStyle(e)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
onClick={() => handleDelete(record.taskId)}
|
||||
style={{ ...getButtonStyle('danger').getStyle(), fontSize: '12px' }}
|
||||
onMouseEnter={(e) => getButtonStyle('danger').getMouseEnterStyle(e)}
|
||||
onMouseLeave={(e) => getButtonStyle('danger').getMouseLeaveStyle(e)}
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
// 查询任务数据的函数
|
||||
async function QueryTaskBasic(params: QueryTaskParams | null, pagination: TablePaginationConfig | null): Promise<void> {
|
||||
setLoading(true);
|
||||
try {
|
||||
let tableParamsParams = pagination ? { pagination } : tableParams;
|
||||
|
||||
let res = await adminQueryTaskCollection(tableParamsParams, params ?? form.getFieldsValue());
|
||||
|
||||
console.log("QueryTaskBasic", '查询任务数据:', res);
|
||||
setSimpleData(res);
|
||||
// 处理任务数据
|
||||
|
||||
let tasks = [] as Array<MJP.MJApiTasks>;
|
||||
|
||||
for (let i = 0; res.collection && i < res.collection.length; i++) {
|
||||
let tempTask = res.collection[i];
|
||||
if (isEmpty(tempTask.properties) || tempTask.properties == null) {
|
||||
tasks.push(tempTask);
|
||||
continue;
|
||||
}
|
||||
let tempProperties: Record<string, any> = {};
|
||||
try {
|
||||
tempProperties = JSON.parse(tempTask.properties);
|
||||
// 处理属性的JSON数据
|
||||
tempTask.propertieJson = tempProperties;
|
||||
tasks.push(tempTask);
|
||||
} catch (error) {
|
||||
// 报错 就是 JSON解析错误 直接添加就行
|
||||
tasks.push(tempTask);
|
||||
}
|
||||
}
|
||||
setDataSource(tasks);
|
||||
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
total: res.total
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
async function handleTableChange(
|
||||
pagination: TablePaginationConfig,
|
||||
filters: Record<string, FilterValue | null>,
|
||||
sorter: SorterResult<MJP.MJApiTasks> | SorterResult<MJP.MJApiTasks>[],
|
||||
extra: TableCurrentDataSource<MJP.MJApiTasks>
|
||||
): Promise<void> {
|
||||
setLoading(true);
|
||||
try {
|
||||
await QueryTaskBasic(form.getFieldsValue(), pagination);
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...pagination,
|
||||
total: simpleData?.total || 0,
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = async (value: QueryTaskParams) => {
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: 1
|
||||
}
|
||||
});
|
||||
await QueryTaskBasic(value, null);
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = async () => {
|
||||
form.resetFields();
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: 1
|
||||
}
|
||||
});
|
||||
await QueryTaskBasic(null, null);
|
||||
};
|
||||
|
||||
// 操作处理函数
|
||||
const handleView = (record: MJP.MJApiTasks) => {
|
||||
setCurrentViewTask(record);
|
||||
setViewModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (taskId: string) => {
|
||||
messageApi.warning('未实现删除功能,请稍后再试。');
|
||||
return;
|
||||
const confirm = await modalApi.confirm({
|
||||
title: '确认删除',
|
||||
content: '您确定要删除这个任务吗?删除后将无法恢复。',
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消'
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
messageApi.info('已取消删除操作');
|
||||
return;
|
||||
}
|
||||
|
||||
messageApi.loading('正在删除任务,请稍候...');
|
||||
// 这里添加删除任务的API调用
|
||||
// await adminDeleteTask(taskId);
|
||||
messageApi.success('任务已删除');
|
||||
await QueryTaskBasic(form.getFieldsValue(), null);
|
||||
};
|
||||
|
||||
// 添加窗口大小监听
|
||||
useEffect(() => {
|
||||
const updateModalWidth = () => {
|
||||
let w = window.innerWidth * 0.8 || 900;
|
||||
if (w < 900) {
|
||||
w = 900;
|
||||
}
|
||||
setModalWidth(w);
|
||||
};
|
||||
|
||||
updateModalWidth();
|
||||
window.addEventListener('resize', updateModalWidth);
|
||||
QueryTaskBasic(null, null);
|
||||
getDayTaskStatistics();
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateModalWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 获取统计数据
|
||||
async function getDayTaskStatistics() {
|
||||
setStatisticsLoading(true);
|
||||
try {
|
||||
setTaskStatisticsData({
|
||||
totalTasks: 0,
|
||||
inProgressTasks: 0,
|
||||
completedTasks: 0,
|
||||
failedTasks: 0
|
||||
})
|
||||
let res = await adminGetDayTaskStatistics();
|
||||
setTaskStatisticsData(res);
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message);
|
||||
} finally {
|
||||
setStatisticsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer title={false} ghost>
|
||||
<div style={{ padding: '24px' }}>
|
||||
{messageHolder}
|
||||
{modalHolder}
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
{stats.map((stat, index) => (
|
||||
<Col xs={24} sm={12} md={12} lg={6} xl={6} key={index}>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e8e8e8',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||
padding: 0
|
||||
}}
|
||||
styles={{
|
||||
body: { padding: '10px 16px' }
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '13px', color: '#666', marginBottom: '4px' }}>
|
||||
{stat.title}
|
||||
</div>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#1890ff' }}>
|
||||
{stat.value}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
backgroundColor: '#e6f4ff',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
{stat.iconText}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 主表格卡片 */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<span>任务管理</span>
|
||||
<Tag color="blue">共 {simpleData?.total} 条记录</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
loading={statisticsLoading}
|
||||
type='primary'
|
||||
style={{ ...getButtonStyle('primary').getStyle() }}
|
||||
onMouseEnter={(e) => getButtonStyle('primary').getMouseEnterStyle(e)}
|
||||
onMouseLeave={(e) => getButtonStyle('primary').getMouseLeaveStyle(e)}
|
||||
onClick={getDayTaskStatistics}
|
||||
>刷新统计</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{/* 搜索表单区域 */}
|
||||
<Form
|
||||
form={form}
|
||||
layout="inline"
|
||||
onFinish={handleSearch}
|
||||
>
|
||||
<Form.Item name="thirdPartyTaskId" label="MJ任务ID" style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="请输入MJ任务ID"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="token" label="Token" style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="请输入Token"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="tokenId" label="Token ID" style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="请输入TokenID"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SearchOutlined />}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={loading}
|
||||
pagination={tableParams.pagination}
|
||||
onChange={handleTableChange}
|
||||
scroll={{ x: 1600 }}
|
||||
rowKey="taskId"
|
||||
size="middle"
|
||||
bordered
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 查看任务详情的Modal */}
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span>📋</span>
|
||||
<span>任务详情</span>
|
||||
{currentViewTask && (
|
||||
<Tag color="blue" style={{ marginLeft: '8px' }}>
|
||||
{currentViewTask.taskId}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width={modalWidth}
|
||||
open={viewModalVisible}
|
||||
footer={null}
|
||||
closable={true}
|
||||
maskClosable={false}
|
||||
closeIcon={<CloseOutlined style={{ color: '#333', fontSize: '14px' }} />}
|
||||
onCancel={() => setViewModalVisible(false)}
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: '75vh',
|
||||
paddingRight: '16px',
|
||||
overflowY: 'auto'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TaskInfo taskData={currentViewTask} isAdmin={true}>
|
||||
</TaskInfo>
|
||||
|
||||
</Modal>
|
||||
</PageContainer >
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskManagement;
|
||||
@@ -0,0 +1,320 @@
|
||||
.task-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 48px; /* 从 64px 减少到 48px */
|
||||
padding: 0 16px; /* 从 24px 减少到 16px */
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left h3 {
|
||||
margin: 0 !important;
|
||||
font-size: 16px !important; /* 从 18px 减少到 16px */
|
||||
}
|
||||
|
||||
.header-left span {
|
||||
margin-left: 8px; /* 从 12px 减少到 8px */
|
||||
font-size: 11px; /* 从 12px 减少到 11px */
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-right span {
|
||||
font-size: 13px; /* 从 14px 减少到 13px */
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.user-dropdown:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.task-content {
|
||||
flex: 1;
|
||||
min-height: 0; /* 重要:允许flex子项收缩 */
|
||||
padding: 24px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 24px; /* 底部留出空间 */
|
||||
}
|
||||
|
||||
.cards-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
/* 移除固定高度和flex设置,让其自然增长 */
|
||||
}
|
||||
|
||||
/* 页脚样式 - 固定在底部 */
|
||||
.task-footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
padding: 16px 24px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 信息卡片样式 - 自适应宽度 */
|
||||
.info-cards {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-cards .stat-card {
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-cards .stat-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.info-cards .stat-card .ant-card-body {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
margin-right: 12px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.stat-info .ant-statistic-title {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-cards .ant-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-cards .ant-card-head {
|
||||
min-height: 40px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.info-cards .ant-card-head-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 表格样式 - 自适应高度 */
|
||||
.image-table-card {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-table-card .ant-card-head {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.image-table-card .ant-card-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.image-cell {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table-row-light {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.table-row-dark {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* 表格滚动样式优化 */
|
||||
.ant-table-tbody > tr > td {
|
||||
vertical-align: middle; /* 确保内容垂直居中 */
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d9d9d9 transparent;
|
||||
}
|
||||
|
||||
.ant-table-tbody::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.ant-table-tbody::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ant-table-tbody::-webkit-scrollbar-thumb {
|
||||
background-color: #d9d9d9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ant-table-tbody::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bfbfbf;
|
||||
}
|
||||
|
||||
/* 图片预览样式 */
|
||||
.ant-image {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
.ant-image-img {
|
||||
object-fit: cover !important;
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
/* 按钮组样式 */
|
||||
.image-cell .ant-space-vertical {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-cell .ant-space-horizontal {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.content-container {
|
||||
max-width: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-header {
|
||||
height: 42px; /* 进一步减小 */
|
||||
padding: 6px 12px; /* 进一步减小 */
|
||||
}
|
||||
|
||||
.header-left h3 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.header-left span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-right span {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.cards-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.task-footer {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.task-header {
|
||||
height: 38px; /* 进一步减小 */
|
||||
padding: 4px 8px; /* 进一步减小 */
|
||||
}
|
||||
|
||||
.header-left h3 {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.info-cards .stat-card .ant-card-body {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-info .ant-statistic-content {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Layout, Space, Typography, Button, Spin, message } from 'antd';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import InfoCards from './TaskMessageInfo/InfoCards';
|
||||
import TaskTable from './TaskMessageInfo/TaskTable';
|
||||
import './TaskMessageInfo.css';
|
||||
import CustomFooter from "@/components/Footer/index"
|
||||
import { getTokenCacheIten } from '@/services/services/mjp';
|
||||
import { TimeDelay } from '@/util/time';
|
||||
import { useMJPStore } from '@/store/mjp';
|
||||
import { formatTokenDisplay } from '@/util/text';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
const { Header, Content, Footer } = Layout;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const TaskMessageInfo: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tip, setTip] = useState("加载中,请稍等...");
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
|
||||
// 或者使用选择器
|
||||
const setTokenCacheItem = useMJPStore((state) => state.setTokenCacheItem);
|
||||
const tokenCacheItem = useMJPStore((state) => state.tokenCacheItem);
|
||||
|
||||
useEffect(() => {
|
||||
getToken();
|
||||
}, []);
|
||||
|
||||
|
||||
// 从 localstorage 中加载token 没有找到Token的话 跳转到 /mjp/login
|
||||
async function getToken() {
|
||||
try {
|
||||
let expires_at = localStorage.getItem("expires_at");
|
||||
if (!expires_at || isEmpty(expires_at)) {
|
||||
window.location.href = '/mjp/login';
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断是否过期
|
||||
const currentTime = new Date().getTime();
|
||||
const expiresTime = new Date(expires_at).getTime();
|
||||
console.log("当前时间:", currentTime, "过期时间:", expiresTime);
|
||||
if (currentTime > expiresTime) {
|
||||
// 如果过期了,跳转到登录页面
|
||||
window.location.href = '/mjp/login';
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setTip("正在获取Token信息,请稍等...");
|
||||
const token = localStorage.getItem('mjp_token');
|
||||
if (!token) {
|
||||
window.location.href = '/mjp/login';
|
||||
return;
|
||||
}
|
||||
// 存在 获取数据 要是不能获取 还是跳转到登录界面
|
||||
var tokenItem = await getTokenCacheIten(token);
|
||||
await TimeDelay(1000);
|
||||
localStorage.setItem('mjp_token_info', JSON.stringify(tokenItem));
|
||||
localStorage.setItem('mjp_token', tokenItem.token);
|
||||
setTokenCacheItem(tokenItem);
|
||||
} catch (error) {
|
||||
messageApi.error('获取Token信息失败,请重新登录');
|
||||
window.location.href = '/mjp/login';
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setTip("加载中,请稍等...");
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
async function TokenLogout() {
|
||||
localStorage.removeItem('mjp_token_info');
|
||||
setTokenCacheItem(null);
|
||||
messageApi.success('已成功退出登录,正在跳转到登录页面...');
|
||||
// 等待1秒后跳转到登录页面
|
||||
await TimeDelay(1000);
|
||||
window.location.href = '/mjp/login';
|
||||
}
|
||||
|
||||
return (
|
||||
<Spin tip={tip} spinning={loading} >
|
||||
<Layout className="task-layout">
|
||||
<Header className="task-header">
|
||||
<div className="header-left">
|
||||
<Title level={3} style={{ color: 'white', margin: 0 }}>
|
||||
LaiTool MJ生图管理系统
|
||||
</Title>
|
||||
<Text style={{ color: 'rgba(255,255,255,0.8)', marginLeft: 16 }}>
|
||||
AI 图像生成任务管理平台
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="header-right">
|
||||
<Space size="middle">
|
||||
<Text style={{ color: 'white' }}>
|
||||
欢迎回来,{formatTokenDisplay(tokenCacheItem?.token, 10)}
|
||||
</Text>
|
||||
<Button type="text" icon={<LogoutOutlined />} danger onClick={TokenLogout}>
|
||||
退出登录
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
<Content className="task-content">
|
||||
<div className="content-container">
|
||||
{/* 信息卡片区域 */}
|
||||
<div className="cards-section">
|
||||
<InfoCards />
|
||||
</div>
|
||||
|
||||
{/* 表格区域 */}
|
||||
<div className="table-section">
|
||||
<TaskTable />
|
||||
</div>
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
<Footer className="task-footer">
|
||||
<CustomFooter />
|
||||
</Footer>
|
||||
</Layout>
|
||||
{messageHolder}
|
||||
</Spin >
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskMessageInfo;
|
||||
@@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
import { Card, Row, Col, Statistic, Alert, Button } from 'antd';
|
||||
import {
|
||||
CloudUploadOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
RocketOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { FormatDate } from '@/util/time';
|
||||
import { useMJPStore } from '@/store/mjp';
|
||||
import { systemConfig } from '../../../../config/systemConfig';
|
||||
|
||||
|
||||
|
||||
const InfoCards: React.FC = () => {
|
||||
const tokenCacheItem = useMJPStore((state) => state.tokenCacheItem);
|
||||
|
||||
const cardData = [
|
||||
{
|
||||
title: '用户套餐',
|
||||
value: '高级套餐',
|
||||
icon: <CloudUploadOutlined style={{ color: '#1890ff' }} />,
|
||||
color: '#1890ff',
|
||||
suffix: ''
|
||||
},
|
||||
{
|
||||
title: '今日绘图限制',
|
||||
value: `${tokenCacheItem?.dailyUsage} / ${tokenCacheItem?.dailyLimit}`,
|
||||
icon: <CheckCircleOutlined style={{ color: '#52c41a' }} />,
|
||||
color: '#52c41a',
|
||||
suffix: ''
|
||||
},
|
||||
{
|
||||
title: '并发限制',
|
||||
value: `${tokenCacheItem?.currentlyExecuting} / ${tokenCacheItem?.concurrencyLimit}`,
|
||||
icon: <ClockCircleOutlined style={{ color: '#faad14' }} />,
|
||||
color: '#faad14',
|
||||
suffix: ''
|
||||
},
|
||||
{
|
||||
title: 'Token 到期时间',
|
||||
value: `${tokenCacheItem?.expiresAt ? FormatDate(tokenCacheItem?.expiresAt) : '无限制'}`,
|
||||
icon: <ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />,
|
||||
color: '#ff4d4f',
|
||||
suffix: '',
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="info-cards">
|
||||
|
||||
<Alert
|
||||
message={
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{
|
||||
background: 'linear-gradient(45deg, #1890ff, #40a9ff)',
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
animation: 'gentle-float 4s ease-in-out infinite'
|
||||
}}>
|
||||
<RocketOutlined style={{ color: 'white', fontSize: '14px' }} />
|
||||
</div>
|
||||
<span style={{ fontSize: '14px', color: '#262626' }}>
|
||||
<strong>欢迎使用 MJ 绘图系统</strong> - 请关注使用配额和到期时间
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
size="middle"
|
||||
icon={<span style={{ marginRight: '4px' }}>📚</span>}
|
||||
onClick={() => window.open(systemConfig.mjPackage.doc, '_blank')}
|
||||
style={{
|
||||
background: '#fff7e6',
|
||||
borderColor: '#ffd591',
|
||||
color: '#fa8c16',
|
||||
fontWeight: 500,
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
配置教程
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
closable
|
||||
showIcon={false}
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #e6f7ff'
|
||||
}}
|
||||
/>
|
||||
|
||||
<Alert
|
||||
message="生成的图片只会保留三天,请及时下载保存。图片包含违规检测,部分出图成功但不显示的图片可能是因为检测到违规内容。图片被过滤!"
|
||||
type="warning"
|
||||
closable
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Row gutter={[24, 24]}>
|
||||
{cardData.map((item, index) => (
|
||||
<Col xs={24} sm={12} lg={6} key={index}>
|
||||
<Card
|
||||
hoverable
|
||||
className="stat-card"
|
||||
>
|
||||
<div className="stat-content">
|
||||
<div className="stat-icon">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="stat-text">
|
||||
<Statistic
|
||||
title={item.title}
|
||||
value={item.value}
|
||||
suffix={item.suffix}
|
||||
valueStyle={{
|
||||
color: item.color,
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoCards;
|
||||
@@ -0,0 +1,493 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Table, Card, Image, Tag, Button, Space, Input, Select, message, Tooltip, Modal } from 'antd';
|
||||
import { SearchOutlined, EyeOutlined, DownloadOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||
import { queryTaskList } from '@/services/services/mjp';
|
||||
import { useMJPStore } from '@/store/mjp';
|
||||
import { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface';
|
||||
import { FormatDate, TimeDelay } from '@/util/time';
|
||||
import { isEmpty } from 'lodash';
|
||||
import TaskInfo from '../TokenManagement/TaskInfo';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
// 状态映射显示
|
||||
export const getStatusTag = (status: string) => {
|
||||
const statusMap = {
|
||||
NOT_START: {
|
||||
color: 'default',
|
||||
text: '未开始',
|
||||
icon: '⏸️'
|
||||
},
|
||||
SUBMITTED: {
|
||||
color: 'blue',
|
||||
text: '已提交',
|
||||
icon: '📤'
|
||||
},
|
||||
IN_PROGRESS: {
|
||||
color: 'processing',
|
||||
text: '进行中',
|
||||
icon: '🔄'
|
||||
},
|
||||
FAILURE: {
|
||||
color: 'red',
|
||||
text: '失败',
|
||||
icon: '❌'
|
||||
},
|
||||
SUCCESS: {
|
||||
color: 'green',
|
||||
text: '成功',
|
||||
icon: '✅'
|
||||
},
|
||||
MODAL: {
|
||||
color: 'purple',
|
||||
text: '模态框',
|
||||
icon: '🖼️'
|
||||
},
|
||||
CANCEL: {
|
||||
color: 'orange',
|
||||
text: '已取消',
|
||||
icon: '🚫'
|
||||
}
|
||||
};
|
||||
|
||||
const config = statusMap[status as keyof typeof statusMap] || {
|
||||
color: 'default',
|
||||
text: status,
|
||||
icon: '❓'
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag color={config.color}>
|
||||
<span style={{ marginRight: '4px' }}>{config.icon}</span>
|
||||
{config.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const TaskTable: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [taskCollection, setTaskCollection] = useState<Array<MJP.MJApiTasks>>([]);
|
||||
|
||||
const [taskData, setTaskData] = useState<MJP.QueryTaskData>();
|
||||
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
const { setTokenCacheItem } = useMJPStore((state) => state);
|
||||
|
||||
const [tableParams, setTableParams] = useState<TableModel.TableParams>({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
showQuickJumper: true,
|
||||
totalBoundaryShowSizeChanger: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 添加弹窗相关状态
|
||||
const [taskDetailVisible, setTaskDetailVisible] = useState(false);
|
||||
const [currentTaskData, setCurrentTaskData] = useState<MJP.MJApiTasks | null>(null);
|
||||
const [modalWidth, setModalWidth] = useState(900);
|
||||
|
||||
// 处理任务详情查看
|
||||
const handleViewTaskDetail = (record: MJP.MJApiTasks) => {
|
||||
setCurrentTaskData(record);
|
||||
// 根据任务状态调整弹窗宽度
|
||||
let w = window.innerWidth * 0.8 || 900;
|
||||
if (w < 900) {
|
||||
w = 900;
|
||||
}
|
||||
setModalWidth(w);
|
||||
setTaskDetailVisible(true);
|
||||
};
|
||||
|
||||
// 计算表格高度
|
||||
useEffect(() => {
|
||||
QueryTaskBasic(undefined, null);
|
||||
}, []);
|
||||
|
||||
const columns: ColumnsType<MJP.MJApiTasks> = [
|
||||
{
|
||||
title: '任务ID',
|
||||
dataIndex: 'taskId',
|
||||
key: 'taskId',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
render: (text: string, record: MJP.MJApiTasks) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handleViewTaskDetail(record)}
|
||||
style={{
|
||||
padding: 0,
|
||||
height: 'auto',
|
||||
fontFamily: 'monospace',
|
||||
color: '#1890ff',
|
||||
wordBreak: 'break-all',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
title="点击查看任务详情"
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '提示词',
|
||||
dataIndex: 'prompt',
|
||||
key: 'prompt',
|
||||
width: 300,
|
||||
render: (text: string, record: MJP.MJApiTasks) => {
|
||||
const promptText = record.propertieJson && record.propertieJson.prompt
|
||||
? record.propertieJson.prompt
|
||||
: '-';
|
||||
if (promptText === '-') {
|
||||
return <span style={{ color: '#999' }}>暂无提示词</span>;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={promptText} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}>
|
||||
<div
|
||||
className="prompt-cell"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 4, // 显示2行
|
||||
lineHeight: '20px',
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: '13px'
|
||||
}}
|
||||
>
|
||||
{promptText}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'MJ任务ID',
|
||||
dataIndex: 'thirdPartyTaskId',
|
||||
key: 'thirdPartyTaskId',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '生图机器人',
|
||||
dataIndex: 'botType',
|
||||
key: 'botType',
|
||||
width: 120,
|
||||
render: (status: string, record: MJP.MJApiTasks) => {
|
||||
return record.propertieJson && record.propertieJson.botType ?
|
||||
<Tag color="purple">{record.propertieJson.botType}</Tag> : "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
width: 100,
|
||||
render: (status: string, record: MJP.MJApiTasks) => {
|
||||
return record.propertieJson && record.propertieJson.progress ?
|
||||
<Tag color="purple">{record.propertieJson?.progress}</Tag> : "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
width: 150,
|
||||
render: (text: string, record: MJP.MJApiTasks) => (
|
||||
<div title={text}>
|
||||
{FormatDate(record.startTime)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '完成时间',
|
||||
dataIndex: 'completeTime',
|
||||
key: 'completeTime',
|
||||
width: 150,
|
||||
render: (text: string, record: MJP.MJApiTasks) => (
|
||||
<div title={text}>
|
||||
{record.endTime ? FormatDate(record.endTime) : "-"}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "失败原因",
|
||||
dataIndex: 'completeTime',
|
||||
key: 'completeTime',
|
||||
width: 120,
|
||||
render: (text: string, record: MJP.MJApiTasks) => {
|
||||
const promptText = record.propertieJson && record.propertieJson.failReason
|
||||
? record.propertieJson.failReason
|
||||
: '-';
|
||||
if (promptText === '-') {
|
||||
return <span style={{ color: '#999' }}>-</span>;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={promptText} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}>
|
||||
<div
|
||||
className="prompt-cell"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 4, // 显示2行
|
||||
lineHeight: '20px',
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: '13px'
|
||||
}}
|
||||
>
|
||||
{promptText}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '生成图片',
|
||||
dataIndex: 'image',
|
||||
key: 'image',
|
||||
width: 140, // 增加宽度以容纳更大的图片
|
||||
fixed: 'right',
|
||||
render: (image: string, record: MJP.MJApiTasks) => {
|
||||
let hasImage = record.propertieJson && record.propertieJson.imageUrl && !isEmpty(record.propertieJson.imageUrl);
|
||||
return (
|
||||
<div className="image-cell">
|
||||
{hasImage ? (
|
||||
<Space direction="vertical" size="small" align="center">
|
||||
<Image
|
||||
width={120} // 从60增加到100
|
||||
height={120} // 从60增加到100
|
||||
alt='1111'
|
||||
src={record.propertieJson?.imageUrl}
|
||||
style={{ borderRadius: 6, objectFit: 'cover' }}
|
||||
placeholder={
|
||||
<div style={{
|
||||
width: 120,
|
||||
height: 120,
|
||||
background: '#f5f5f5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 6
|
||||
}}>
|
||||
<span style={{ fontSize: '12px', color: '#999' }}>加载中...</span>
|
||||
</div>
|
||||
}
|
||||
loading='lazy'
|
||||
fallback='https://lms.laitool.cn/im/empty_image.png'
|
||||
/>
|
||||
</Space>
|
||||
) : (
|
||||
<div style={{
|
||||
width: 120,
|
||||
height: 120,
|
||||
background: '#f5f5f5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 6,
|
||||
color: '#999',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
暂无图片
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 基础的查询任务
|
||||
async function QueryTaskBasic(thirdPartyTaskId: string | undefined, pagination: TablePaginationConfig | null): Promise<void> {
|
||||
setLoading(true);
|
||||
try {
|
||||
let tableParamsParams = pagination ? { pagination } : tableParams;
|
||||
let tokenString = localStorage.getItem('mjp_token_info');
|
||||
if (!tokenString) {
|
||||
messageApi.error("请先登录获取Token信息");
|
||||
await TimeDelay(1000);
|
||||
window.location.href = '/mjp/login';
|
||||
return;
|
||||
} let tokenItem: MJP.TokenCacheItem
|
||||
try {
|
||||
tokenItem = JSON.parse(tokenString);
|
||||
} catch (error) {
|
||||
throw new Error("Token信息格式错误,请重新登录");
|
||||
}
|
||||
let res = await queryTaskList(tokenItem?.token, tableParamsParams, thirdPartyTaskId);
|
||||
setTaskData(res);
|
||||
if (res.collection != null && res.collection.length > 0) {
|
||||
let taskInfo = res.collection[0];
|
||||
// 设置Token缓存项
|
||||
setTokenCacheItem({
|
||||
id: taskInfo.id,
|
||||
token: tokenItem.token,
|
||||
useToken: tokenItem.useToken,
|
||||
dailyLimit: taskInfo.dailyLimit,
|
||||
totalLimit: taskInfo.totalLimit,
|
||||
concurrencyLimit: taskInfo.concurrencyLimit,
|
||||
createdAt: taskInfo.createdAt,
|
||||
expiresAt: taskInfo.expiresAt,
|
||||
dailyUsage: taskInfo.dailyUsage,
|
||||
totalUsage: taskInfo.totalUsage,
|
||||
lastActivityTime: taskInfo.lastActivityTime,
|
||||
historyUse: taskInfo.historyUse,
|
||||
currentlyExecuting: taskInfo.currentlyExecuting
|
||||
});
|
||||
|
||||
let tasks = [] as Array<MJP.MJApiTasks>;
|
||||
// 初始 任务数据
|
||||
for (let i = 0; i < taskInfo.taskCollections.length; i++) {
|
||||
let tempTask = taskInfo.taskCollections[i];
|
||||
if (isEmpty(tempTask.properties) || tempTask.properties == null) {
|
||||
tasks.push(tempTask);
|
||||
continue;
|
||||
}
|
||||
let tempProperties: Record<string, any> = {};
|
||||
try {
|
||||
tempProperties = JSON.parse(tempTask.properties);
|
||||
// 处理属性的JSON数据
|
||||
tempTask.propertieJson = tempProperties;
|
||||
tasks.push(tempTask);
|
||||
} catch (error) {
|
||||
// 报错 就是 JSON解析错误 直接添加就行
|
||||
tasks.push(tempTask);
|
||||
}
|
||||
}
|
||||
// 设置task
|
||||
setTaskCollection(tasks);
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
total: res.total,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
messageApi.warning("没有找到Token相关任务信息");
|
||||
}
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTableChange(pagination: TablePaginationConfig, filters: Record<string, FilterValue | null>, sorter: SorterResult<MJP.MJApiTasks> | SorterResult<MJP.MJApiTasks>[], extra: TableCurrentDataSource<MJP.MJApiTasks>): Promise<void> {
|
||||
setLoading(true);
|
||||
try {
|
||||
await QueryTaskBasic(undefined, pagination);
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...pagination,
|
||||
total: taskData?.total
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleSearch = async (value: string) => {
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: 1 // 重置到第一页
|
||||
}
|
||||
})
|
||||
await QueryTaskBasic(value, null)
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={tableRef}
|
||||
title={
|
||||
<Space>
|
||||
<span>任务列表</span>
|
||||
<Tag color="blue">总计 {taskData?.total} 条记录</Tag>
|
||||
</Space>
|
||||
}
|
||||
className="image-table-card"
|
||||
extra={
|
||||
<Space>
|
||||
<Search
|
||||
placeholder="搜索MJ任务ID"
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
size="middle"
|
||||
style={{ width: 280 }}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={taskCollection}
|
||||
loading={loading}
|
||||
scroll={{
|
||||
x: 1200
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
pagination={tableParams.pagination}
|
||||
/>
|
||||
{messageHolder}
|
||||
{/* 任务详情弹窗 */}
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span>📋</span>
|
||||
<span>任务详情</span>
|
||||
{currentTaskData && (
|
||||
<Tag color="blue" style={{ marginLeft: '8px' }}>
|
||||
{currentTaskData.taskId}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width={modalWidth}
|
||||
open={taskDetailVisible}
|
||||
footer={null}
|
||||
closable={true}
|
||||
closeIcon={<CloseOutlined style={{ color: '#333', fontSize: '14px' }} />}
|
||||
onCancel={() => {
|
||||
setTaskDetailVisible(false);
|
||||
setCurrentTaskData(null);
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: '75vh',
|
||||
paddingRight: '16px',
|
||||
overflowY: 'auto'
|
||||
}
|
||||
}}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
{currentTaskData && (
|
||||
<TaskInfo
|
||||
taskData={currentTaskData}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskTable;
|
||||
@@ -0,0 +1,44 @@
|
||||
/* 重置页面样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.token-login-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: none;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.login-card .ant-card-head {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.login-card .ant-card-head-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Input, Button, Form, message } from 'antd';
|
||||
import { LockOutlined } from '@ant-design/icons';
|
||||
import './TokenLogin.css';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { TimeDelay } from '@/util/time';
|
||||
import { getTokenCacheIten } from '@/services/services/mjp';
|
||||
import { useMJPStore } from '@/store/mjp';
|
||||
|
||||
const TokenLogin: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { setTokenCacheItem } = useMJPStore();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// 检查是否有Token,如果没有则跳转到登录页面
|
||||
const token = localStorage.getItem('mjp_token');
|
||||
if (token) {
|
||||
form.setFieldsValue({ token });
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 登录
|
||||
const onFinish = async (values: { token: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isEmpty(values.token)) {
|
||||
messageApi.error('Token不能为空');
|
||||
return;
|
||||
}
|
||||
// 开始调用请求token信息接口
|
||||
const res = await getTokenCacheIten(values.token);
|
||||
localStorage.setItem('mjp_token_info', JSON.stringify(res));
|
||||
localStorage.setItem('mjp_token', values.token);
|
||||
const expiresAt = new Date(Date.now() + 8 * 60 * 60 * 1000);
|
||||
localStorage.setItem("expires_at", expiresAt.toLocaleString());
|
||||
|
||||
// 存储
|
||||
setTokenCacheItem(res);
|
||||
// 登录成功 等待1秒
|
||||
messageApi.success('登录成功,正在跳转到任务列表页面...');
|
||||
await TimeDelay(1000);
|
||||
window.location.href = '/mjp/task';
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="token-login-container">
|
||||
<Card
|
||||
title="Token 登录"
|
||||
className="login-card"
|
||||
style={{
|
||||
width: 400,
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
name="tokenLogin"
|
||||
onFinish={onFinish}
|
||||
layout="vertical"
|
||||
size="large"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item
|
||||
name="token"
|
||||
label="访问令牌"
|
||||
rules={[
|
||||
{ required: true, message: '请输入您的Token!' },
|
||||
{ min: 10, message: 'Token长度至少10位!' }
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="请输入您的Token"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
style={{ height: '40px' }}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
{messageHolder}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenLogin;
|
||||
@@ -0,0 +1,630 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Space,
|
||||
message,
|
||||
Tag,
|
||||
Row,
|
||||
Col,
|
||||
Form,
|
||||
Modal
|
||||
} from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
CopyOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||
import { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface';
|
||||
import { adminGetHealthAndCacheTokenData, adminQueryTokenBasic } from '@/services/services/mjp';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { formatTokenDisplay } from '@/util/text';
|
||||
import { FormatDate } from '@/util/time';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { useFormReset } from '@/hooks/useFormReset';
|
||||
import AddToken from './TokenManagement/AddToken';
|
||||
import ModifyToken from './TokenManagement/ModifyToken';
|
||||
import { useGlassButtonStyles } from '@/hooks/useGlassButtonStyles';
|
||||
import TokenInfo from './TokenManagement/TokenInfo';
|
||||
|
||||
export interface QueryTokenParams {
|
||||
token?: string;
|
||||
tokenId?: number
|
||||
}
|
||||
|
||||
const TokenManagement: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dataSource, setDataSource] = useState<Array<MJP.TokenCacheItem>>([]);
|
||||
const [simpleData, setSimpleData] = useState<BasicModel.QueryCollection<Array<MJP.TokenCacheItem>>>();
|
||||
const [form] = Form.useForm(); // 添加 form 实例
|
||||
|
||||
const [healthAndCache, setHealthAndCache] = useState<MJP.MJPHealthAndCacheResponse>();
|
||||
|
||||
const [title, setTitle] = useState<string>("新增Token");
|
||||
|
||||
|
||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||
const [tokenId, setTokenId] = useState<number>(0);
|
||||
const { setFormRef, resetForm } = useFormReset();
|
||||
|
||||
const { getButtonStyle } = useGlassButtonStyles();
|
||||
|
||||
const [tableParams, setTableParams] = useState<TableModel.TableParams>({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
showQuickJumper: true,
|
||||
totalBoundaryShowSizeChanger: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
const [modalApi, modalHolder] = Modal.useModal();
|
||||
|
||||
const [viewModalVisible, setViewModalVisible] = useState(false);
|
||||
const [currentViewToken, setCurrentViewToken] = useState<MJP.TokenCacheItem | null>(null);
|
||||
const [modalWidth, setModalWidth] = useState(800);
|
||||
|
||||
useEffect(() => {
|
||||
QueryTokenBasic(form.getFieldsValue(), tableParams.pagination)
|
||||
}, []);
|
||||
|
||||
// 使用 useMemo 替代
|
||||
const stats = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: "总计Token数",
|
||||
value: simpleData?.total || 0,
|
||||
iconText: "📊"
|
||||
},
|
||||
{
|
||||
title: "活跃Token数(5分钟)",
|
||||
value: healthAndCache?.cacheStats?.activeTokens || 0,
|
||||
iconText: "🟢"
|
||||
},
|
||||
{
|
||||
title: "缓存中的token数",
|
||||
value: healthAndCache?.cacheStats?.totalTokens || 0,
|
||||
iconText: "💾"
|
||||
},
|
||||
{
|
||||
title: "缓存Token出图数",
|
||||
value: healthAndCache?.cacheStats?.totalDailyUsage || 0,
|
||||
iconText: "🖼️"
|
||||
}
|
||||
];
|
||||
}, [simpleData, healthAndCache]);
|
||||
|
||||
|
||||
// 状态 显示剩余天数的版本
|
||||
const getStatusTag = (expiresAt: Date | null | undefined) => {
|
||||
if (!expiresAt) {
|
||||
return <Tag color="blue">无时间限制</Tag>;
|
||||
}
|
||||
|
||||
const expireDate = new Date(expiresAt);
|
||||
const currentDate = new Date();
|
||||
const timeDiff = expireDate.getTime() - currentDate.getTime();
|
||||
|
||||
if (timeDiff > 0) {
|
||||
const daysLeft = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
|
||||
|
||||
let color = 'green';
|
||||
let text = '使用中';
|
||||
|
||||
if (daysLeft <= 1) {
|
||||
color = 'red';
|
||||
text = `今日过期`;
|
||||
} else if (daysLeft <= 3) {
|
||||
color = 'orange';
|
||||
text = `${daysLeft}天后过期`;
|
||||
} else if (daysLeft <= 7) {
|
||||
color = 'yellow';
|
||||
text = `${daysLeft}天后过期`;
|
||||
} else if (daysLeft <= 30) {
|
||||
text = `${daysLeft}天后过期`;
|
||||
} else {
|
||||
text = `剩余 ${daysLeft} 天`;
|
||||
}
|
||||
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
}
|
||||
|
||||
return <Tag color="red">已到期</Tag>;
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType<MJP.TokenCacheItem> = [
|
||||
{
|
||||
title: 'Token ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: 'Token',
|
||||
dataIndex: 'token',
|
||||
key: 'token',
|
||||
width: 200,
|
||||
render: (text: string) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span >
|
||||
{formatTokenDisplay(text, 20)}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(text);
|
||||
messageApi.success('Token 已复制到剪贴板');
|
||||
}}
|
||||
style={{
|
||||
padding: '0 4px',
|
||||
height: '20px',
|
||||
color: '#1890ff'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '每日限制',
|
||||
dataIndex: 'dailyLimit',
|
||||
key: 'dailyLimit',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 700 }}>{record.dailyLimit > 0 ? record.dailyLimit : '不限制'}</div>
|
||||
<div style={{ color: '#666' }}>
|
||||
已用: {record.dailyUsage}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '总限制',
|
||||
dataIndex: 'totalLimit',
|
||||
key: 'totalLimit',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{record.totalLimit > 0 ? record.totalLimit : '不限制'}</div>
|
||||
<div style={{ color: '#666' }}>
|
||||
已用: {record.totalUsage}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '并发限制',
|
||||
dataIndex: 'concurrencyLimit',
|
||||
key: 'concurrencyLimit',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{record.concurrencyLimit}</div>
|
||||
<div style={{ color: '#666' }}>
|
||||
执行中: {record.currentlyExecuting}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (_, record) => getStatusTag(record.expiresAt),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 150,
|
||||
render: (time: Date) => (
|
||||
<span>
|
||||
{FormatDate(time)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '过期时间',
|
||||
dataIndex: 'expiresAt',
|
||||
key: 'expiresAt',
|
||||
width: 150,
|
||||
render: (time: Date | null) => (
|
||||
<span >
|
||||
{time ? FormatDate(time) : '无时间限制'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '最后活动',
|
||||
dataIndex: 'lastActivityTime',
|
||||
key: 'lastActivityTime',
|
||||
width: 150,
|
||||
render: (time: Date | null, record) => (
|
||||
<span >
|
||||
<span >
|
||||
{time && time != record.createdAt ? FormatDate(time) : '-'}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 170,
|
||||
render: (_, record: MJP.TokenCacheItem) => (
|
||||
<Space size={6}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => handleView(record)}
|
||||
style={{ ...getButtonStyle('primary').getStyle() }}
|
||||
onMouseEnter={(e) => {
|
||||
getButtonStyle('primary').getMouseEnterStyle(e);
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
getButtonStyle('primary').getMouseLeaveStyle(e);
|
||||
}}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => handleEdit(record)}
|
||||
style={{ ...getButtonStyle('primary').getStyle() }}
|
||||
onMouseEnter={(e) => {
|
||||
getButtonStyle('primary').getMouseEnterStyle(e);
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
getButtonStyle('primary').getMouseLeaveStyle(e);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
onClick={() => handleDelete(record.id)}
|
||||
style={{ ...getButtonStyle("danger").getStyle() }}
|
||||
onMouseEnter={(e) => {
|
||||
getButtonStyle('danger').getMouseEnterStyle(e);
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
getButtonStyle('danger').getMouseLeaveStyle(e);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
async function QueryTokenBasic(params: QueryTokenParams | null, pagination: TablePaginationConfig | null): Promise<void> {
|
||||
setLoading(true);
|
||||
try {
|
||||
let tableParamsParams = pagination ? { pagination } : tableParams;
|
||||
|
||||
// 加载缓存中的数据
|
||||
let cacheRes = await adminGetHealthAndCacheTokenData();
|
||||
setHealthAndCache(cacheRes);
|
||||
|
||||
// 加载表格中的数据
|
||||
let res = await adminQueryTokenBasic(tableParamsParams, params ?? form.getFieldsValue());
|
||||
|
||||
setSimpleData(res);
|
||||
|
||||
// 处理历史数据
|
||||
let tokens: Array<MJP.TokenCacheItem> = []
|
||||
|
||||
// 初始 任务数据
|
||||
for (let i = 0; res.collection && i < res.collection.length; i++) {
|
||||
let tempToken = res.collection[i];
|
||||
if (isEmpty(tempToken.historyUse) || tempToken.historyUse == null) {
|
||||
tokens.push(tempToken);
|
||||
continue;
|
||||
}
|
||||
// 如果有历史数据 就进行JSON解析
|
||||
let tempHistoryUseJson: Record<string, any> = {};
|
||||
try {
|
||||
tempHistoryUseJson = JSON.parse(tempToken.historyUse ?? "{}");
|
||||
// 处理属性的JSON数据
|
||||
tempToken.historyUseJson = tempHistoryUseJson;
|
||||
tokens.push(tempToken);
|
||||
} catch (error) {
|
||||
// 报错 就是 JSON解析错误 直接添加就行
|
||||
tokens.push(tempToken);
|
||||
}
|
||||
}
|
||||
setDataSource(tokens);
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
total: res.total
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function handleTableChange(pagination: TablePaginationConfig, filters: Record<string, FilterValue | null>, sorter: SorterResult<MJP.TokenCacheItem> | SorterResult<MJP.TokenCacheItem>[], extra: TableCurrentDataSource<MJP.TokenCacheItem>): Promise<void> {
|
||||
setLoading(true);
|
||||
try {
|
||||
await QueryTokenBasic(form.getFieldsValue(), pagination);
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...pagination,
|
||||
total: simpleData?.total || 0,
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = async (value: QueryTokenParams) => {
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: 1 // 重置到第一页
|
||||
}
|
||||
})
|
||||
await QueryTokenBasic(value, null)
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = async () => {
|
||||
form.resetFields();
|
||||
setTableParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: 1 // 重置到第一页
|
||||
}
|
||||
});
|
||||
await QueryTokenBasic(null, null);
|
||||
};
|
||||
|
||||
|
||||
// 关闭新增编辑token
|
||||
async function modalCancel(): Promise<void> {
|
||||
setModalVisible(false);
|
||||
resetForm();
|
||||
setTokenId(0);
|
||||
// 这边调用加载数据的方法
|
||||
await QueryTokenBasic(form.getFieldsValue(), null);
|
||||
}
|
||||
|
||||
// 操作处理函数
|
||||
const handleView = (record: MJP.TokenCacheItem) => {
|
||||
setCurrentViewToken(record);
|
||||
setViewModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: MJP.TokenCacheItem) => {
|
||||
form.resetFields();
|
||||
setTitle("编辑 Token");
|
||||
setTokenId(record.id);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// 删除方法
|
||||
const handleDelete = async (id: number) => {
|
||||
let confirm = await modalApi.confirm({
|
||||
title: '确认删除',
|
||||
content: '您确定要删除这个 Token 吗?删除之后该 Token 将无法使用。',
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消'
|
||||
})
|
||||
if (!confirm) {
|
||||
messageApi.info('已取消删除操作');
|
||||
return;
|
||||
}
|
||||
messageApi.loading('正在删除 Token,请稍候...');
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
form.resetFields();
|
||||
setTitle("新增 Token");
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// 添加窗口大小监听
|
||||
useEffect(() => {
|
||||
const updateModalWidth = () => {
|
||||
let w = window.innerWidth * 0.7 || 800;
|
||||
if (w < 800) {
|
||||
w = 800;
|
||||
}
|
||||
setModalWidth(w);
|
||||
};
|
||||
|
||||
updateModalWidth();
|
||||
window.addEventListener('resize', updateModalWidth);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateModalWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer title={false} ghost>
|
||||
<div style={{ padding: '24px' }}>
|
||||
{messageHolder}
|
||||
{modalHolder}
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
{
|
||||
stats.map((stat, index) => (<Col xs={24} sm={12} md={12} lg={6} xl={6}>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e8e8e8',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||
padding: 0
|
||||
}}
|
||||
styles={{
|
||||
body: { padding: '10px 16px' } // 新的方式设置body样式
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '13px', color: '#666', marginBottom: '4px' }}>
|
||||
{stat.title}
|
||||
</div>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#ff4d4f' }}>
|
||||
{stat.value}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
backgroundColor: '#fff2f0',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
{stat.iconText}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>))
|
||||
|
||||
}
|
||||
</Row>
|
||||
|
||||
{/* 主表格卡片 */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<span>Token 管理</span>
|
||||
<Tag color="blue">共 {simpleData?.total} 条记录</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
新增 Token
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{/* 搜索表单区域 */}
|
||||
<Form
|
||||
form={form}
|
||||
layout="inline"
|
||||
onFinish={handleSearch}
|
||||
>
|
||||
<Form.Item name="token" label="Token" style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="请输入 Token"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="tokenId" label="Token ID" style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="请输入 TokenId"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SearchOutlined />}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={loading}
|
||||
pagination={tableParams.pagination}
|
||||
onChange={handleTableChange}
|
||||
scroll={{ x: 1500 }}
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
bordered
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Modal width={600} title={title} maskClosable={false} open={modalVisible} footer={null} onCancel={modalCancel}>
|
||||
{
|
||||
tokenId == 0 ? <AddToken setFormRef={setFormRef} /> :
|
||||
<ModifyToken setFormRef={setFormRef} tokenId={tokenId} />
|
||||
}
|
||||
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span>🔍</span>
|
||||
<span>Token 使用记录</span>
|
||||
</div>
|
||||
}
|
||||
width={modalWidth}
|
||||
open={viewModalVisible}
|
||||
footer={null}
|
||||
closable={true}
|
||||
closeIcon={<CloseOutlined style={{ color: '#333', fontSize: '14px' }} />}
|
||||
onCancel={() => setViewModalVisible(false)}
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: '75vh',
|
||||
paddingRight: '16px',
|
||||
overflowY: 'auto'
|
||||
}, content: {
|
||||
paddingRight: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentViewToken && (
|
||||
<TokenInfo
|
||||
tokenData={currentViewToken}
|
||||
onCopyToken={(token) => {
|
||||
navigator.clipboard.writeText(token);
|
||||
messageApi.success('Token 已复制到剪贴板');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
</PageContainer >
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenManagement;
|
||||
@@ -0,0 +1,182 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Form, Input, InputNumber, Button, message, FormInstance } from 'antd';
|
||||
import { adminAddToken } from '@/services/services/mjp';
|
||||
|
||||
interface AddTokenProps {
|
||||
setFormRef: (form: FormInstance) => void;
|
||||
}
|
||||
|
||||
export const defaultTokenValues: MJP.AddAndModifyTokenParams = {
|
||||
token: "",
|
||||
useToken: "",
|
||||
dailyLimit: 200,
|
||||
totalLimit: 6000,
|
||||
concurrencyLimit: 5,
|
||||
useDayCount: 30
|
||||
}
|
||||
|
||||
const AddToken: React.FC<AddTokenProps> = ({ setFormRef }) => {
|
||||
const [form] = Form.useForm<MJP.AddAndModifyTokenParams>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
|
||||
useEffect(() => {
|
||||
setFormRef(form);
|
||||
// 设置默认值
|
||||
form.setFieldsValue({ ...defaultTokenValues });
|
||||
}, [form, setFormRef]);
|
||||
|
||||
// 提交添加 Token
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
let res = await adminAddToken(form.getFieldsValue())
|
||||
messageApi.success(res);
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ ...defaultTokenValues });
|
||||
}
|
||||
|
||||
// 生成一个随机的 Token
|
||||
const generateToken = () => {
|
||||
const randomToken = crypto.randomUUID().replace(/-/g, '').substring(0, 32);
|
||||
form.setFieldsValue({ token: randomToken });
|
||||
messageApi.success('已生成新的 Token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Token
|
||||
*/
|
||||
const formatString = () => {
|
||||
let useToken = form.getFieldValue('useToken');
|
||||
if (useToken && useToken.startsWith('sk-')) {
|
||||
// 移除 前面三个字符
|
||||
useToken = useToken.substring(3);
|
||||
}
|
||||
form.setFieldsValue({ useToken });
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
name="token"
|
||||
label="用户Token"
|
||||
rules={[{ required: true, message: '请输入或生成 Token' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入 Token 或点击生成"
|
||||
addonAfter={
|
||||
<Button type="link" onClick={generateToken} style={{ padding: 0 }}>
|
||||
生成
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="useToken"
|
||||
label="使用的Token"
|
||||
rules={[{ required: true, message: '请输入或生成 实际Token' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入 Token 或点击生成"
|
||||
addonAfter={
|
||||
<Button type="link" onClick={formatString} style={{ padding: 0 }}>
|
||||
标准化
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="dailyLimit"
|
||||
label="每日限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入每日限制' },
|
||||
{ type: 'number', min: 1, message: '每日限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入每日使用限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="totalLimit"
|
||||
label="总限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入总限制' },
|
||||
{ type: 'number', min: 1, message: '总限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入总使用限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="concurrencyLimit"
|
||||
label="并发限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入并发限制' },
|
||||
{ type: 'number', min: 1, message: '并发限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入并发限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="useDayCount"
|
||||
label="使用天数"
|
||||
rules={[
|
||||
{ required: true, message: '请输入使用天数' },
|
||||
{ type: 'number', min: 1, message: '使用天数必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入可使用天数"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
addonAfter="天"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ textAlign: 'right', marginTop: 24 }}>
|
||||
<Button
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
保存
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
{messageHolder}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddToken;
|
||||
@@ -0,0 +1,234 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Form, Input, InputNumber, Button, message, FormInstance, Spin } from 'antd';
|
||||
import { adminGetTokenById, adminModifyToken } from '@/services/services/mjp';
|
||||
import { FormatDate } from '@/util/time';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
interface ModifyTokenProps {
|
||||
setFormRef: (form: FormInstance) => void;
|
||||
tokenId: number; // Token ID,用于编辑时获取数据
|
||||
}
|
||||
|
||||
const ModifyToken: React.FC<ModifyTokenProps> = ({ setFormRef, tokenId }) => {
|
||||
const [form] = Form.useForm<MJP.AddAndModifyTokenParams & MJP.MJAPITokens>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
|
||||
useEffect(() => {
|
||||
setFormRef(form);
|
||||
// 加载Token详情
|
||||
loadTokenDetail();
|
||||
}, [form, setFormRef, tokenId]);
|
||||
|
||||
// 加载Token详情数据
|
||||
const loadTokenDetail = async () => {
|
||||
if (!tokenId || tokenId <= 0) {
|
||||
messageApi.error('无效的Token ID');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoadingData(true);
|
||||
let res = await adminGetTokenById(tokenId);
|
||||
|
||||
form.setFieldsValue({
|
||||
...res,
|
||||
createdAt: res.createdAt ? FormatDate(res.createdAt) : '-',
|
||||
expiresAt: res.expiresAt ? FormatDate(res.expiresAt) : '-',
|
||||
useDayCount: -1 // 默认值为 -1,表示不修改
|
||||
|
||||
});
|
||||
messageApi.success('获取Token详情成功');
|
||||
} catch (error: any) {
|
||||
messageApi.error(`获取Token详情失败: ${error.message}`);
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交修改 Token
|
||||
const handleSubmit = async (values: MJP.AddAndModifyTokenParams) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (!tokenId || tokenId <= 0) {
|
||||
messageApi.error('无效的Token ID');
|
||||
return;
|
||||
}
|
||||
if (values.token == null || isEmpty(values.token)) {
|
||||
messageApi.error('Token 不能为空');
|
||||
return;
|
||||
}
|
||||
// 开始调用修改方法
|
||||
let res = await adminModifyToken(tokenId, values);
|
||||
messageApi.success(res);
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置表单信息
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
// 重置为初始加载的数据,而不是默认值
|
||||
loadTokenDetail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化Token格式
|
||||
*/
|
||||
const formatString = () => {
|
||||
let token = form.getFieldValue('token');
|
||||
if (token && token.startsWith('sk-')) {
|
||||
// 移除 前面三个字符
|
||||
token = token.substring(3);
|
||||
}
|
||||
form.setFieldsValue({ token });
|
||||
};
|
||||
|
||||
return (
|
||||
<Spin spinning={loadingData} tip="正在加载 Token 数据...">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
|
||||
<Form.Item
|
||||
name="token"
|
||||
label="Token"
|
||||
rules={[{ required: true, message: '请输入或生成 Token' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入 Token"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="useToken"
|
||||
label="实际TOKEN"
|
||||
rules={[{ required: true, message: '请输入或生成实际 Token' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入 Token"
|
||||
addonAfter={
|
||||
<Button type="link" onClick={formatString} style={{ padding: 0, height: 20 }}>
|
||||
标准化
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="dailyLimit"
|
||||
label="每日限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入每日限制' },
|
||||
{ type: 'number', min: 1, message: '每日限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入每日使用限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="totalLimit"
|
||||
label="总限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入总限制' },
|
||||
{ type: 'number', min: 1, message: '总限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入总使用限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="concurrencyLimit"
|
||||
label="并发限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入并发限制' },
|
||||
{ type: 'number', min: 1, message: '并发限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入并发限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="createdAt"
|
||||
label="创建时间"
|
||||
>
|
||||
<Input
|
||||
placeholder="创建时间"
|
||||
readOnly
|
||||
disabled
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="expiresAt"
|
||||
label="停用时间"
|
||||
>
|
||||
<Input
|
||||
placeholder="停用时间"
|
||||
readOnly
|
||||
disabled
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="useDayCount"
|
||||
label="使用天数(-1 和 0 不修改)"
|
||||
rules={[
|
||||
{ required: true, message: '请输入使用天数' },
|
||||
{ type: 'number', min: -1, message: '使用天数必须大于 -1' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入可使用天数"
|
||||
style={{ width: '100%' }}
|
||||
min={-1}
|
||||
addonAfter="天"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ textAlign: 'right', marginTop: 24 }}>
|
||||
<Button
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={handleReset}
|
||||
disabled={loadingData}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
disabled={loadingData}
|
||||
>
|
||||
保存修改
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
{messageHolder}
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModifyToken;
|
||||
@@ -0,0 +1,564 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { Row, Col, Tag, Button, Space, Descriptions, Typography, message, Image, Card, Divider } from 'antd';
|
||||
import { CopyOutlined, LinkOutlined, DownloadOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { formatTokenDisplay } from '@/util/text';
|
||||
import { FormatDate } from '@/util/time';
|
||||
import { useGlassButtonStyles } from '@/hooks/useGlassButtonStyles';
|
||||
import { getStatusTag } from '../TaskMessageInfo/TaskTable';
|
||||
|
||||
const { Text, Title, Paragraph } = Typography;
|
||||
|
||||
interface TaskDetailProps {
|
||||
taskData: MJP.MJApiTasks;
|
||||
isAdmin?: boolean
|
||||
}
|
||||
|
||||
|
||||
const TaskInfo: React.FC<TaskDetailProps> = ({ taskData, isAdmin }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerWidth, setContainerWidth] = useState(800);
|
||||
const { getButtonStyle } = useGlassButtonStyles();
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
|
||||
// 监听容器宽度变化
|
||||
useEffect(() => {
|
||||
const updateWidth = () => {
|
||||
if (containerRef.current) {
|
||||
const width = containerRef.current.offsetWidth;
|
||||
setContainerWidth(width);
|
||||
}
|
||||
};
|
||||
|
||||
updateWidth();
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { width } = entry.contentRect;
|
||||
setContainerWidth(width);
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', updateWidth);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', updateWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 响应式配置
|
||||
const getResponsiveConfig = () => {
|
||||
if (containerWidth < 500) {
|
||||
return {
|
||||
descriptionColumns: 1,
|
||||
statisticColumns: { xs: 24, sm: 24, md: 24, lg: 24, xl: 24 },
|
||||
summaryColumns: { xs: 24, sm: 24, md: 12, lg: 12, xl: 6 },
|
||||
tokenDisplayLength: 8
|
||||
};
|
||||
} else if (containerWidth < 700) {
|
||||
return {
|
||||
descriptionColumns: 2,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 12, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 24, sm: 12, md: 12, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 12
|
||||
};
|
||||
} else if (containerWidth < 900) {
|
||||
return {
|
||||
descriptionColumns: 2,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 12, sm: 12, md: 6, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 15
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
descriptionColumns: 3,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 12, sm: 6, md: 6, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 20
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getResponsiveConfig();
|
||||
|
||||
// 解析属性JSON
|
||||
const properties = taskData.propertieJson || {};
|
||||
|
||||
// 计算耗时
|
||||
const getDuration = () => {
|
||||
if (!taskData.endTime || !taskData.startTime) return '-';
|
||||
|
||||
const duration = new Date(taskData.endTime).getTime() - new Date(taskData.startTime).getTime();
|
||||
const seconds = Math.floor(duration / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes}分${seconds % 60}秒`;
|
||||
}
|
||||
return `${seconds}秒`;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* 任务基本信息 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#1890ff',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
📋 任务基本信息
|
||||
</Title>
|
||||
|
||||
<Descriptions
|
||||
column={config.descriptionColumns}
|
||||
size="small"
|
||||
labelStyle={{
|
||||
width: 'auto',
|
||||
minWidth: containerWidth < 500 ? '60px' : '80px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}
|
||||
contentStyle={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item label="任务ID">
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: taskData.taskId || '',
|
||||
onCopy: () => messageApi.success('任务ID已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{taskData.taskId || '无'}
|
||||
</Paragraph>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="第三方任务ID">
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: taskData.thirdPartyTaskId || '',
|
||||
onCopy: () => messageApi.success('第三方任务ID已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{taskData.thirdPartyTaskId || '无'}
|
||||
</Paragraph>
|
||||
</Descriptions.Item>
|
||||
|
||||
{isAdmin ? <Descriptions.Item label="Token ID">
|
||||
<Tag color="blue">{taskData.tokenId}</Tag>
|
||||
</Descriptions.Item> : null}
|
||||
|
||||
<Descriptions.Item label="状态">
|
||||
{getStatusTag(taskData.status || '')}
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="动作类型">
|
||||
<Tag color="purple">{properties.action || '-'}</Tag>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="进度">
|
||||
<Tag color="purple">{properties.progress || "-"}</Tag>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="开始时间">
|
||||
<span>{FormatDate(taskData.startTime)}</span>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="结束时间">
|
||||
<span>{taskData.endTime ? FormatDate(taskData.endTime) : '-'}</span>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="耗时">
|
||||
<span>{getDuration()}</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
{/* 提示词信息 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#52c41a',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
💭 提示词信息
|
||||
</Title>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Card size="small" style={{ marginBottom: '8px' }}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong style={{ fontSize: '12px', color: '#666' }}>原始提示词:</Text>
|
||||
</div>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.prompt || '',
|
||||
onCopy: () => messageApi.success('原始提示词已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '8px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{properties.prompt || '无'}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
|
||||
<Card size="small" style={{ marginBottom: '8px' }}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong style={{ fontSize: '12px', color: '#666' }}>英文提示词:</Text>
|
||||
</div>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.promptEn || '',
|
||||
onCopy: () => messageApi.success('英文提示词已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '8px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{properties.promptEn || '无'}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
|
||||
{properties.properties?.finalPrompt && (
|
||||
<Card size="small">
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong style={{ fontSize: '12px', color: '#666' }}>最终提示词:</Text>
|
||||
</div>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.properties.finalPrompt || '',
|
||||
onCopy: () => messageApi.success('最终提示词已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '8px',
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{properties.properties.finalPrompt || '无'}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 生成结果 */}
|
||||
{properties.imageUrl && (
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#faad14',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
🖼️ 生成结果
|
||||
</Title>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Card
|
||||
size="small"
|
||||
title="结果图片"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<LinkOutlined />}
|
||||
onClick={() => window.open(properties.imageUrl, '_blank')}
|
||||
title="在新窗口打开"
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={properties.imageUrl}
|
||||
alt="生成结果"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
preview={{
|
||||
mask: <EyeOutlined style={{ fontSize: '20px' }} />,
|
||||
maskClassName: 'custom-mask'
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
|
||||
尺寸: {properties.imageWidth} × {properties.imageHeight}
|
||||
</div>
|
||||
|
||||
{/* 添加图片地址复制功能 */}
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong style={{ fontSize: '11px', color: '#666' }}>图片地址:</Text>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.imageUrl || '',
|
||||
onCopy: () => messageApi.success('图片地址已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '4px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
fontSize: '10px',
|
||||
wordBreak: 'break-all',
|
||||
marginTop: '4px'
|
||||
}}
|
||||
>
|
||||
{formatTokenDisplay(properties.imageUrl, 30)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={16}>
|
||||
<Card size="small" title="技术详情">
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#666'
|
||||
}}>提交时间</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{properties.submitTime ? FormatDate(new Date(properties.submitTime)) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#666'
|
||||
}}>结束时间</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{properties.finishTime ? FormatDate(new Date(properties.submitTime)) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#666'
|
||||
}}>机器人类型</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{properties.botType || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#666'
|
||||
}}>Discord实例</div>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.properties?.discordInstanceId || '',
|
||||
onCopy: () => messageApi.success('Discord实例ID已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '2px',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
wordBreak: 'break-word',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{properties.properties?.discordInstanceId ?
|
||||
properties.properties.discordInstanceId : '-'}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 只保留失败原因 */}
|
||||
{properties.failReason && (
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#ff4d4f',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
❌ 失败原因
|
||||
</Title>
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
borderColor: '#ffccc7',
|
||||
backgroundColor: '#fff2f0'
|
||||
}}
|
||||
styles={{
|
||||
body: { padding: '16px' }
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
backgroundColor: '#ff4d4f',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
<span style={{
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold'
|
||||
}}>!</span>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
marginBottom: '8px',
|
||||
fontSize: '13px',
|
||||
color: '#666',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
任务执行失败,详细信息如下:
|
||||
</div>
|
||||
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.failReason || '',
|
||||
onCopy: () => messageApi.success('失败原因已复制'),
|
||||
tooltips: ['复制失败原因', '已复制']
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '12px',
|
||||
backgroundColor: '#ffffff',
|
||||
border: '1px solid #ffccc7',
|
||||
borderRadius: '6px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '13px',
|
||||
color: '#d4380d',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: '1.6',
|
||||
boxShadow: '0 2px 4px rgba(255, 77, 79, 0.1)'
|
||||
}}
|
||||
>
|
||||
{properties.failReason}
|
||||
</Paragraph>
|
||||
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#fff7e6',
|
||||
border: '1px solid #ffd591',
|
||||
borderRadius: '4px',
|
||||
fontSize: '11px',
|
||||
color: '#ad6800'
|
||||
}}>
|
||||
💡 <strong>提示:</strong>如果问题持续存在,请检查提示词格式或联系技术支持
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messageHolder}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskInfo;
|
||||
@@ -0,0 +1,696 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { Row, Col, Tag, Button, Space, Statistic, Descriptions, Typography, message } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { formatTokenDisplay } from '@/util/text';
|
||||
import { FormatDate } from '@/util/time';
|
||||
import Table, { ColumnsType } from 'antd/es/table';
|
||||
import { useGlassButtonStyles } from '@/hooks/useGlassButtonStyles';
|
||||
import { systemConfig } from '../../../../config/systemConfig';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
interface TokenDetailProps {
|
||||
tokenData: MJP.TokenCacheItem;
|
||||
onCopyToken: (token: string) => void;
|
||||
}
|
||||
|
||||
const TokenInfo: React.FC<TokenDetailProps> = ({ tokenData, onCopyToken }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerWidth, setContainerWidth] = useState(800);
|
||||
const { getButtonStyle } = useGlassButtonStyles();
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
|
||||
// 监听容器宽度变化
|
||||
useEffect(() => {
|
||||
const updateWidth = () => {
|
||||
if (containerRef.current) {
|
||||
const width = containerRef.current.offsetWidth;
|
||||
setContainerWidth(width);
|
||||
console.log('Container width updated:', width); // 调试用
|
||||
}
|
||||
};
|
||||
|
||||
// 初始设置
|
||||
updateWidth();
|
||||
|
||||
// 使用 ResizeObserver 监听容器大小变化
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { width } = entry.contentRect;
|
||||
setContainerWidth(width);
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
// 也监听窗口大小变化作为后备
|
||||
window.addEventListener('resize', updateWidth);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', updateWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 根据容器宽度动态计算响应式配置
|
||||
const getResponsiveConfig = () => {
|
||||
console.log('Current container width:', containerWidth); // 调试用
|
||||
|
||||
if (containerWidth < 500) {
|
||||
return {
|
||||
descriptionColumns: 1,
|
||||
statisticColumns: { xs: 24, sm: 24, md: 24, lg: 24, xl: 24 },
|
||||
summaryColumns: { xs: 24, sm: 24, md: 12, lg: 12, xl: 6 },
|
||||
tokenDisplayLength: 8,
|
||||
showSimplePagination: true
|
||||
};
|
||||
} else if (containerWidth < 700) {
|
||||
return {
|
||||
descriptionColumns: 2,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 12, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 24, sm: 12, md: 12, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 12,
|
||||
showSimplePagination: true
|
||||
};
|
||||
} else if (containerWidth < 900) {
|
||||
return {
|
||||
descriptionColumns: 2,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 12, sm: 12, md: 6, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 15,
|
||||
showSimplePagination: false
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
descriptionColumns: 3,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 12, sm: 6, md: 6, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 20,
|
||||
showSimplePagination: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getResponsiveConfig();
|
||||
|
||||
// 状态标签
|
||||
const getStatusTag = (expiresAt: Date | null | undefined) => {
|
||||
if (!expiresAt) {
|
||||
return <Tag color="blue">无时间限制</Tag>;
|
||||
}
|
||||
|
||||
const expireDate = new Date(expiresAt);
|
||||
const currentDate = new Date();
|
||||
const timeDiff = expireDate.getTime() - currentDate.getTime();
|
||||
|
||||
if (timeDiff > 0) {
|
||||
const daysLeft = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
|
||||
|
||||
let color = 'green';
|
||||
let text = '使用中';
|
||||
|
||||
if (daysLeft <= 1) {
|
||||
color = 'red';
|
||||
text = `今日过期`;
|
||||
} else if (daysLeft <= 3) {
|
||||
color = 'orange';
|
||||
text = `${daysLeft}天后过期`;
|
||||
} else if (daysLeft <= 7) {
|
||||
color = 'yellow';
|
||||
text = `${daysLeft}天后过期`;
|
||||
} else if (daysLeft <= 30) {
|
||||
text = `${daysLeft}天后过期`;
|
||||
}
|
||||
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
}
|
||||
|
||||
return <Tag color="red">已到期</Tag>;
|
||||
};
|
||||
|
||||
// 添加历史记录的类型定义
|
||||
interface HistoryRecord {
|
||||
TokenId: number;
|
||||
Date: string;
|
||||
DailyUsage: number;
|
||||
TotalUsage: number;
|
||||
LastActivityAt: string;
|
||||
HistoryUse: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
// 处理历史数据的函数
|
||||
const processHistoryData = (historyUseJson: HistoryRecord[]): HistoryRecord[] => {
|
||||
if (!Array.isArray(historyUseJson)) {
|
||||
return [];
|
||||
}
|
||||
console.log(historyUseJson)
|
||||
|
||||
return historyUseJson
|
||||
.map((record, index) => ({
|
||||
...record,
|
||||
key: `${record.TokenId}_${record.Date}_${index}`
|
||||
}))
|
||||
.sort((a, b) => new Date(b.Date).getTime() - new Date(a.Date).getTime());
|
||||
};
|
||||
|
||||
// 根据容器宽度动态调整表格列
|
||||
const getTableColumns = (): ColumnsType<HistoryRecord> => {
|
||||
const baseColumns: ColumnsType<HistoryRecord> = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: containerWidth < 500 ? 35 : containerWidth < 700 ? 40 : 50,
|
||||
align: 'center',
|
||||
render: (_, __, index) => (
|
||||
<span style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#8c8c8c',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
{index + 1}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '日期',
|
||||
dataIndex: 'Date',
|
||||
key: 'Date',
|
||||
width: containerWidth < 500 ? 70 : containerWidth < 700 ? 80 : 100,
|
||||
render: (date: Date) => {
|
||||
const dateObj = new Date(date);
|
||||
const today = new Date();
|
||||
const diffTime = today.getTime() - dateObj.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) - 1;
|
||||
|
||||
let dateLabel = '';
|
||||
let labelColor = '#bfbfbf';
|
||||
|
||||
if (diffDays === 1) {
|
||||
dateLabel = '昨天';
|
||||
labelColor = '#73d13d';
|
||||
} else if (diffDays === 0) {
|
||||
dateLabel = '今天';
|
||||
labelColor = '#40a9ff';
|
||||
} else if (diffDays <= 7) {
|
||||
dateLabel = `${diffDays}天前`;
|
||||
labelColor = '#ffa940';
|
||||
} else {
|
||||
dateLabel = `${diffDays}天前`;
|
||||
labelColor = '#ffa940';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#262626',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
{FormatDate(date, true)}
|
||||
</div>
|
||||
{dateLabel && containerWidth >= 500 && (
|
||||
<div style={{
|
||||
fontSize: '9px',
|
||||
color: labelColor,
|
||||
marginTop: '1px'
|
||||
}}>
|
||||
{dateLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sorter: (a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime(),
|
||||
},
|
||||
{
|
||||
title: '当日',
|
||||
dataIndex: 'DailyUsage',
|
||||
key: 'DailyUsage',
|
||||
width: containerWidth < 500 ? 50 : containerWidth < 700 ? 60 : 80,
|
||||
align: 'center',
|
||||
render: (usage: number) => {
|
||||
return (
|
||||
<span style={{
|
||||
fontWeight: '600',
|
||||
color: '#1677ff',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}>
|
||||
{usage}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
sorter: (a, b) => a.DailyUsage - b.DailyUsage,
|
||||
},
|
||||
{
|
||||
title: '累计',
|
||||
dataIndex: 'TotalUsage',
|
||||
key: 'TotalUsage',
|
||||
width: containerWidth < 500 ? 50 : containerWidth < 700 ? 60 : 80,
|
||||
align: 'center',
|
||||
render: (usage: number) => (
|
||||
<span style={{
|
||||
fontWeight: '600',
|
||||
color: '#1677ff',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}>
|
||||
{usage >= 1000 ? `${(usage / 1000).toFixed(1)}k` : usage}
|
||||
</span>
|
||||
),
|
||||
sorter: (a, b) => a.TotalUsage - b.TotalUsage,
|
||||
}, {
|
||||
title: '最后活跃时间',
|
||||
dataIndex: 'LastActivityAt',
|
||||
key: 'LastActivityAt',
|
||||
width: containerWidth < 500 ? 50 : containerWidth < 700 ? 60 : 80,
|
||||
align: 'center',
|
||||
render: (date: Date) => {
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#262626',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
{FormatDate(date)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
];
|
||||
return baseColumns;
|
||||
};
|
||||
|
||||
// 复制使用信息
|
||||
function copyTaskTokenToUser(token: MJP.TokenCacheItem) {
|
||||
let dateString =
|
||||
`Token: ${token.token}
|
||||
创建时间: ${FormatDate(token.createdAt)}
|
||||
过期时间: ${token.expiresAt ? FormatDate(token.expiresAt) : '无时间限制'}
|
||||
每日使用限制: ${token.dailyLimit > 0 ? token.dailyLimit : '无限制'}
|
||||
总使用限制: ${token.totalLimit > 0 ? token.totalLimit : '无限制'}
|
||||
并发限制: ${token.concurrencyLimit > 0 ? token.concurrencyLimit : '无限制'}
|
||||
LaiTool设置文档:${systemConfig.mjPackage.laitoolDoc}
|
||||
API调用使用文档:${systemConfig.mjPackage.doc}
|
||||
查询网址:https://lms.laitool.cn/mjp/task
|
||||
⚠️ 重要提示:Token 为敏感凭证,请妥善保管,避免泄露。如因保管不当造成损失,后果自负。
|
||||
`
|
||||
// 写入到剪贴板
|
||||
navigator.clipboard.writeText(dateString).then(() => {
|
||||
messageApi.success('Token 信息已复制到剪贴板!', 3);
|
||||
}).catch(err => {
|
||||
// 复制失败 ,弹出上面的文本 ,自行复制
|
||||
// 显示复制失败的提示,并提供手动复制选项
|
||||
messageApi.error({
|
||||
content: (
|
||||
<div>
|
||||
<div style={{ marginBottom: '8px' }}>📋 复制失败,请手动复制以下信息:</div>
|
||||
<div style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{dateString}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
duration: 10
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* Token 基本信息 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#1890ff',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
📋 基本信息
|
||||
</Title>
|
||||
|
||||
<Descriptions
|
||||
column={config.descriptionColumns}
|
||||
size="small"
|
||||
labelStyle={{
|
||||
width: 'auto',
|
||||
minWidth: containerWidth < 500 ? '60px' : '80px',
|
||||
fontSize: containerWidth < 500 ? '11px' : '12px'
|
||||
}}
|
||||
contentStyle={{
|
||||
fontSize: containerWidth < 500 ? '11px' : '12px'
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item label="Token ID">
|
||||
<Text strong style={{ fontSize: containerWidth < 500 ? '11px' : '12px' }}>{tokenData.id}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Token">
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
maxWidth: containerWidth < 500 ? '150px' : '200px'
|
||||
}}>
|
||||
<Text
|
||||
code
|
||||
style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
fontFamily: 'monospace',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{formatTokenDisplay(tokenData.token, config.tokenDisplayLength)}
|
||||
</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined style={{ fontSize: '12px' }} />}
|
||||
onClick={() => onCopyToken(tokenData.token)}
|
||||
title="复制Token"
|
||||
style={{ minWidth: 'auto', padding: '2px' }}
|
||||
/>
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="UseToken">
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
maxWidth: containerWidth < 500 ? '150px' : '200px'
|
||||
}}>
|
||||
<Text
|
||||
code
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
fontFamily: 'monospace',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{formatTokenDisplay(tokenData.useToken, config.tokenDisplayLength)}
|
||||
</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined style={{ fontSize: '12px' }} />}
|
||||
onClick={() => onCopyToken(tokenData.useToken)}
|
||||
title="复制Token"
|
||||
style={{ minWidth: 'auto', padding: '2px' }}
|
||||
/>
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{getStatusTag(tokenData.expiresAt)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
<span style={{ fontSize: containerWidth < 500 ? '11px' : '12px' }}>{FormatDate(tokenData.createdAt)}</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="过期时间">
|
||||
<span style={{ fontSize: containerWidth < 500 ? '11px' : '12px' }}>
|
||||
{tokenData.expiresAt ? FormatDate(tokenData.expiresAt) : '无时间限制'}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后活动">
|
||||
<span style={{ fontSize: containerWidth < 500 ? '11px' : '12px' }}>
|
||||
{tokenData.lastActivityTime ? FormatDate(tokenData.lastActivityTime) : '-'}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item >
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => copyTaskTokenToUser(tokenData)}
|
||||
style={{ ...getButtonStyle('primary').getStyle() }}
|
||||
onMouseEnter={(e) => getButtonStyle('primary').getMouseEnterStyle(e)}
|
||||
onMouseLeave={(e) => getButtonStyle('primary').getMouseLeaveStyle(e)}
|
||||
>复制Token信息-给用户</Button>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
{/* 使用统计 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#52c41a',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
📊 使用统计
|
||||
</Title>
|
||||
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col {...config.statisticColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '8px 4px' : '12px 8px',
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderRadius: '8px',
|
||||
minHeight: containerWidth < 500 ? '60px' : '80px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '14px' : '16px',
|
||||
fontWeight: 'bold',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
{tokenData.dailyUsage || 0}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
color: '#666',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
每日使用 / {tokenData.dailyLimit > 0 ? tokenData.dailyLimit : '∞'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.statisticColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '8px 4px' : '12px 8px',
|
||||
backgroundColor: '#f6ffed',
|
||||
borderRadius: '8px',
|
||||
minHeight: containerWidth < 500 ? '60px' : '80px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '14px' : '16px',
|
||||
fontWeight: 'bold',
|
||||
color: '#52c41a'
|
||||
}}>
|
||||
{tokenData.totalUsage || 0}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
color: '#666',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
总使用量 / {tokenData.totalLimit > 0 ? tokenData.totalLimit : '∞'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.statisticColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '8px 4px' : '12px 8px',
|
||||
backgroundColor: '#fff2f0',
|
||||
borderRadius: '8px',
|
||||
minHeight: containerWidth < 500 ? '60px' : '80px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '14px' : '16px',
|
||||
fontWeight: 'bold',
|
||||
color: '#ff4d4f'
|
||||
}}>
|
||||
{tokenData.currentlyExecuting || 0}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
color: '#666',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
并发执行 / {tokenData.concurrencyLimit}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 历史使用数据 */}
|
||||
{
|
||||
tokenData.historyUseJson && Array.isArray(tokenData.historyUseJson) && tokenData.historyUseJson.length > 0 ? (
|
||||
<div>
|
||||
<Title level={5} style={{
|
||||
color: '#faad14',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
📈 历史使用记录
|
||||
</Title>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{tokenData.historyUseJson.length}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#666'
|
||||
}}>记录天数</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{tokenData.historyUseJson.reduce((sum, record) => sum + record.DailyUsage, 0)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#666'
|
||||
}}>总使用量</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{(tokenData.historyUseJson.reduce((sum, record) => sum + record.DailyUsage, 0) / tokenData.historyUseJson.length).toFixed(1)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#666'
|
||||
}}>平均每日</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{Math.max(...tokenData.historyUseJson.map(record => record.DailyUsage))}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#666'
|
||||
}}>最高单日</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 历史记录表格 */}
|
||||
<div style={{
|
||||
width: '100%',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<Table
|
||||
columns={getTableColumns()}
|
||||
dataSource={processHistoryData(tokenData.historyUseJson)}
|
||||
size="small"
|
||||
scroll={{
|
||||
x: 'max-content',
|
||||
y: containerWidth < 500 ? 200 : 250
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: '8px',
|
||||
padding: containerWidth < 500 ? '6px' : '8px'
|
||||
}}
|
||||
pagination={false} // 完全禁用分页
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
backgroundColor: '#fff7e6',
|
||||
borderRadius: '8px',
|
||||
textAlign: 'center',
|
||||
color: '#d46b08',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}>
|
||||
<Text strong>暂无历史使用记录</Text>
|
||||
</div>
|
||||
)}
|
||||
{messageHolder}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenInfo;
|
||||
Reference in New Issue
Block a user