v 1.1.2 生图包管理界面

This commit is contained in:
lq1405 2025-06-14 22:14:20 +08:00
parent 6e52f5ce9a
commit 5c5d79d126
40 changed files with 5781 additions and 97 deletions

View File

@ -76,7 +76,7 @@ export default defineConfig({
* @name layout * @name layout
* @doc https://umijs.org/docs/max/layout-menu * @doc https://umijs.org/docs/max/layout-menu
*/ */
title: 'Ant Design Pro', title: 'LaiTool Management System',
layout: { layout: {
locale: true, locale: true,
...defaultSettings, ...defaultSettings,

16
config/filingConfig.ts Normal file
View File

@ -0,0 +1,16 @@
export const filingConfig = {
copyright: "2025 LaiTool Management System",
gonxin: {
title: '蜀ICP备2024079688号-1',
href: 'https://beian.miit.gov.cn/',
show: true,
},
gongan: {
title: '蜀公网安备51010402012345号',
href: 'https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=51010402012345',
show: false,
},
}

View File

@ -1,7 +1,7 @@
{ {
"openapi": "3.0.1", "openapi": "3.0.1",
"info": { "info": {
"title": "Ant Design Pro", "title": "LaiTool Management System",
"version": "1.0.0" "version": "1.0.0"
}, },
"servers": [{ "servers": [{

View File

@ -1,6 +1,4 @@
import { access } from "fs"; /**
/**
* @name umi * @name umi
* @description path,component,routes,redirect,wrappers,name,icon * @description path,component,routes,redirect,wrappers,name,icon
* @param path path 第一种是动态参数 :id * * @param path path 第一种是动态参数 :id *
@ -17,6 +15,10 @@ export default [
path: '/user', path: '/user',
layout: false, layout: false,
routes: [ routes: [
{
path: '/user',
redirect: '/user/login',
},
{ {
name: 'login', name: 'login',
path: '/user/login', path: '/user/login',
@ -28,8 +30,33 @@ export default [
path: '/user/register', path: '/user/register',
component: './User/Register/index', component: './User/Register/index',
}
]
},
{
path: '/mjp',
layout: false,
routes: [
{
path: '/mjp',
redirect: '/mjp/task',
}, },
], {
name: 'login',
path: '/mjp/login',
component: './MJPackage/TokenLogin',
},
{
name: 'task',
path: '/mjp/task',
component: './MJPackage/TaskMessageInfo',
},
{
name: 'task',
path: '/mjp/price',
component: './MJPackage/MJPriceInfo',
}
]
}, },
{ {
path: '/welcome', path: '/welcome',
@ -138,6 +165,27 @@ export default [
} }
] ]
}, },
{
name: 'mjpackage',
path: '/mjpackage',
icon: 'Discord',
access: 'canSystemOptions',
routes: [
{
name: 'token-management',
path: '/mjpackage/token-management',
component: './MJPackage/TokenManagement',
access: 'canSystemOptions',
},
{
name: 'task-management',
path: '/mjpackage/task-management',
component: './MJPackage/TaskManagement',
access: 'canSystemOptions',
}
]
},
{ {
path: '/', path: '/',
redirect: '/welcome', redirect: '/welcome',

11
config/systemConfig.ts Normal file
View File

@ -0,0 +1,11 @@
export const systemConfig = {
mjPackage: {
doc: "https://rvgyir5wk1c.feishu.cn/wiki/QFZGwx2vti5AN9ku71BcZIRLnDh",
laitoolDoc: "https://rvgyir5wk1c.feishu.cn/wiki/NtYCwgVmgiFaQ6k6K5rcmlKZndb"
},
system: {
kefu: "https://lms.laitool.cn/im/xiangbei.jpg",
}
}

View File

@ -47,7 +47,8 @@
], ],
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.1", "@ant-design/icons": "^4.8.1",
"@ant-design/pro-components": "^2.7.19", "@ant-design/pro-components": "^2.8.9",
"@ant-design/pro-layout": "^7.22.6",
"@uiw/react-json-view": "^2.0.0-alpha.30", "@uiw/react-json-view": "^2.0.0-alpha.30",
"@umijs/route-utils": "^2.2.2", "@umijs/route-utils": "^2.2.2",
"ahooks": "^3.8.4", "ahooks": "^3.8.4",

View File

@ -40,6 +40,7 @@ export default function access(initialState: { currentUser?: API.CurrentUser } |
canAddForeverSoftwareControl: false, canAddForeverSoftwareControl: false,
canDeleteSoftwareControl: false, canDeleteSoftwareControl: false,
canManagementMJPackage: false
} as AccessType.AccessType; } as AccessType.AccessType;
@ -88,7 +89,7 @@ export default function access(initialState: { currentUser?: API.CurrentUser } |
access = { access = {
...access, ...access,
canPrompt: true, canPrompt: true,
canOptionManagement : true, canOptionManagement: true,
canUserManagement: true, canUserManagement: true,
canEditUser: true, canEditUser: true,
@ -123,7 +124,7 @@ export default function access(initialState: { currentUser?: API.CurrentUser } |
...access, ...access,
canPrompt: true, canPrompt: true,
canRoleManagement: true, canRoleManagement: true,
canOptionManagement : true, canOptionManagement: true,
canUserManagement: true, canUserManagement: true,
canEditUser: true, canEditUser: true,
@ -151,6 +152,8 @@ export default function access(initialState: { currentUser?: API.CurrentUser } |
canAddYearSoftwareControl: true, canAddYearSoftwareControl: true,
canAddForeverSoftwareControl: true, canAddForeverSoftwareControl: true,
canDeleteSoftwareControl: true, canDeleteSoftwareControl: true,
canManagementMJPackage: true
}; };
} }
console.log("accsee", access); console.log("accsee", access);

View File

@ -1,13 +1,12 @@
import { Footer, Question, SelectLang, AvatarDropdown, AvatarName } from '@/components'; import { Footer, Question, SelectLang, AvatarDropdown, AvatarName } from '@/components';
import { LinkOutlined } from '@ant-design/icons';
import type { Settings as LayoutSettings } from '@ant-design/pro-components'; import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import { SettingDrawer } from '@ant-design/pro-components'; import { SettingDrawer } from '@ant-design/pro-components';
import type { RunTimeLayoutConfig } from '@umijs/max'; import type { RunTimeLayoutConfig } from '@umijs/max';
import { history, Link } from '@umijs/max'; import { history } from '@umijs/max';
import defaultSettings from '../config/defaultSettings'; import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig'; import { errorConfig } from './requestErrorConfig';
import { UserInfo, getCurrentUser as queryCurrentUser } from './services/services/user'; import { UserInfo, getCurrentUser as queryCurrentUser } from './services/services/user';
import React, { useEffect, useState } from 'react'; import React, { } from 'react';
import { TokenStorage } from './services/define/tokenStorage'; import { TokenStorage } from './services/define/tokenStorage';
import { App, ConfigProvider } from 'antd'; import { App, ConfigProvider } from 'antd';
import cusRequest from './request'; import cusRequest from './request';
@ -57,7 +56,21 @@ export async function getInitialState(): Promise<{
// 如果不是登录页面,执行 // 如果不是登录页面,执行
const { location } = history; const { location } = history;
if (location.pathname !== loginPath && !location.pathname.startsWith('/user/register')) {
// 定义不需要登录检查的路径
const noAuthPaths = [
'/user/login',
'/user/register',
'/mjp/login',
'/mjp',
'/mjp/task' // 如果MJP有自己的认证系统
];
const needAuthCheck = !noAuthPaths.some(path =>
location.pathname === path
);
// 全局设置哪些不用跳转到登录
if (needAuthCheck) {
let currentUserString = localStorage.getItem('userInfo'); let currentUserString = localStorage.getItem('userInfo');
let currentUser = currentUserString ? JSON.parse(currentUserString) : null; let currentUser = currentUserString ? JSON.parse(currentUserString) : null;
let token = localStorage.getItem('token') ?? null; let token = localStorage.getItem('token') ?? null;
@ -111,8 +124,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
}, },
footerRender: () => ( footerRender: () => (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}> <div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<span style={{ marginRight: "15px" }}>Copyright 2024 LaiTool Admins</span> <Footer />
<a style={{ color: "#333" }} href="https://beian.miit.gov.cn/">ICP备2024079688号-1</a>
</div> </div>
), ),
onPageChange: () => { onPageChange: () => {

View File

@ -0,0 +1,21 @@
/* 页脚样式 - 固定在底部 */
.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%;
padding: 10 auto;
}

View File

@ -1,26 +1,53 @@
import { GithubOutlined } from '@ant-design/icons';
import { DefaultFooter } from '@ant-design/pro-components';
import React from 'react'; import React from 'react';
import './index.css';
import { Space, Typography } from 'antd';
import { filingConfig } from '../../../config/filingConfig';
const { Title, Text } = Typography;
const Footer: React.FC = () => { const Footer: React.FC = () => {
return ( return (
<DefaultFooter <div className="footer-content">
style={{ <Space direction="vertical" size={4} style={{ textAlign: 'center', width: '100%' }}>
margin: '0px', <Text type="secondary" style={{ fontSize: '12px' }}>
background: 'none', © {filingConfig.copyright}. All rights reserved.
}} </Text>
links={[ <Space split={<span style={{ color: '#d9d9d9' }}>|</span>} size={16}>
{ {
key: '蜀ICP备2024079688号-1', filingConfig.gonxin.show ?
title: '蜀ICP备2024079688号-1', <Text type="secondary" style={{ fontSize: '12px' }}>
href: 'https://beian.miit.gov.cn/', {filingConfig.gonxin.title}
blankTarget: false, </Text> : null
} }
]} {
copyright="2024 LaiTool Admins" filingConfig.gongan.show ?
<Text type="secondary" style={{ fontSize: '12px' }}>
/> {filingConfig.gongan.title}
</Text> : null
}
<Text type="secondary" style={{ fontSize: '12px' }}>
v1.0.0
</Text>
</Space>
</Space>
</div>
); );
}; };
export default Footer; export default Footer;
// <DefaultFooter
// style={{
// margin: '0px',
// background: 'none',
// }}
// links={[
// {
// key: '蜀ICP备2024079688号-1',
// title: '蜀ICP备2024079688号-1',
// href: 'https://beian.miit.gov.cn/',
// blankTarget: false,
// }
// ]}
// copyright="2024 LaiTool Admins"
// />

View File

@ -0,0 +1,264 @@
export const useGlassButtonStyles = () => {
// 主要按钮(蓝色)
const buttonPrimary = {
getStyle: () => ({
color: '#1890ff',
backgroundColor: 'rgba(24, 144, 255, 0.08)',
border: '1px solid rgba(24, 144, 255, 0.2)',
borderRadius: '8px',
padding: '4px 12px',
backdropFilter: 'blur(4px)',
transition: 'all 0.2s ease',
fontWeight: '500'
}),
getMouseEnterStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(24, 144, 255, 0.15)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 8px rgba(24, 144, 255, 0.2)';
e.currentTarget.style.borderColor = 'rgba(24, 144, 255, 0.4)';
},
getMouseLeaveStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(24, 144, 255, 0.08)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.borderColor = 'rgba(24, 144, 255, 0.2)';
}
};
// 危险按钮(红色)
const buttonDanger = {
getStyle: () => ({
color: '#ff4d4f',
backgroundColor: 'rgba(255, 77, 79, 0.08)',
border: '1px solid rgba(255, 77, 79, 0.2)',
borderRadius: '8px',
padding: '4px 12px',
backdropFilter: 'blur(4px)',
transition: 'all 0.2s ease',
fontWeight: '500'
}),
getMouseEnterStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(255, 77, 79, 0.15)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 8px rgba(255, 77, 79, 0.2)';
e.currentTarget.style.borderColor = 'rgba(255, 77, 79, 0.4)';
},
getMouseLeaveStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(255, 77, 79, 0.08)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.borderColor = 'rgba(255, 77, 79, 0.2)';
}
};
// 成功按钮(绿色)
const buttonSuccess = {
getStyle: () => ({
color: '#52c41a',
backgroundColor: 'rgba(82, 196, 26, 0.08)',
border: '1px solid rgba(82, 196, 26, 0.2)',
borderRadius: '8px',
padding: '4px 12px',
backdropFilter: 'blur(4px)',
transition: 'all 0.2s ease',
fontWeight: '500'
}),
getMouseEnterStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(82, 196, 26, 0.15)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 8px rgba(82, 196, 26, 0.2)';
e.currentTarget.style.borderColor = 'rgba(82, 196, 26, 0.4)';
},
getMouseLeaveStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(82, 196, 26, 0.08)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.borderColor = 'rgba(82, 196, 26, 0.2)';
}
};
// 警告按钮(橙色)
const buttonWarning = {
getStyle: () => ({
color: '#faad14',
backgroundColor: 'rgba(250, 173, 20, 0.08)',
border: '1px solid rgba(250, 173, 20, 0.2)',
borderRadius: '8px',
padding: '4px 12px',
backdropFilter: 'blur(4px)',
transition: 'all 0.2s ease',
fontWeight: '500'
}),
getMouseEnterStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(250, 173, 20, 0.15)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 8px rgba(250, 173, 20, 0.2)';
e.currentTarget.style.borderColor = 'rgba(250, 173, 20, 0.4)';
},
getMouseLeaveStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(250, 173, 20, 0.08)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.borderColor = 'rgba(250, 173, 20, 0.2)';
}
};
// 信息按钮(青色)
const buttonInfo = {
getStyle: () => ({
color: '#13c2c2',
backgroundColor: 'rgba(19, 194, 194, 0.08)',
border: '1px solid rgba(19, 194, 194, 0.2)',
borderRadius: '8px',
padding: '4px 12px',
backdropFilter: 'blur(4px)',
transition: 'all 0.2s ease',
fontWeight: '500'
}),
getMouseEnterStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(19, 194, 194, 0.15)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 8px rgba(19, 194, 194, 0.2)';
e.currentTarget.style.borderColor = 'rgba(19, 194, 194, 0.4)';
},
getMouseLeaveStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(19, 194, 194, 0.08)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.borderColor = 'rgba(19, 194, 194, 0.2)';
}
};
// 默认按钮(灰色)
const buttonDefault = {
getStyle: () => ({
color: '#595959',
backgroundColor: 'rgba(89, 89, 89, 0.08)',
border: '1px solid rgba(89, 89, 89, 0.2)',
borderRadius: '8px',
padding: '4px 12px',
backdropFilter: 'blur(4px)',
transition: 'all 0.2s ease',
fontWeight: '500'
}),
getMouseEnterStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(89, 89, 89, 0.15)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 8px rgba(89, 89, 89, 0.2)';
e.currentTarget.style.borderColor = 'rgba(89, 89, 89, 0.4)';
},
getMouseLeaveStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(89, 89, 89, 0.08)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.borderColor = 'rgba(89, 89, 89, 0.2)';
}
};
// 紫色按钮(自定义)
const buttonPurple = {
getStyle: () => ({
color: '#722ed1',
backgroundColor: 'rgba(114, 46, 209, 0.08)',
border: '1px solid rgba(114, 46, 209, 0.2)',
borderRadius: '8px',
padding: '4px 12px',
backdropFilter: 'blur(4px)',
transition: 'all 0.2s ease',
fontWeight: '500'
}),
getMouseEnterStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(114, 46, 209, 0.15)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 8px rgba(114, 46, 209, 0.2)';
e.currentTarget.style.borderColor = 'rgba(114, 46, 209, 0.4)';
},
getMouseLeaveStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(114, 46, 209, 0.08)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.borderColor = 'rgba(114, 46, 209, 0.2)';
}
};
// 粉色按钮(自定义)
const buttonPink = {
getStyle: () => ({
color: '#eb2f96',
backgroundColor: 'rgba(235, 47, 150, 0.08)',
border: '1px solid rgba(235, 47, 150, 0.2)',
borderRadius: '8px',
padding: '4px 12px',
backdropFilter: 'blur(4px)',
transition: 'all 0.2s ease',
fontWeight: '500'
}),
getMouseEnterStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(235, 47, 150, 0.15)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 8px rgba(235, 47, 150, 0.2)';
e.currentTarget.style.borderColor = 'rgba(235, 47, 150, 0.4)';
},
getMouseLeaveStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.backgroundColor = 'rgba(235, 47, 150, 0.08)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.borderColor = 'rgba(235, 47, 150, 0.2)';
}
};
// 渐变按钮(特殊效果)
const buttonGradient = {
getStyle: () => ({
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
border: 'none',
borderRadius: '8px',
padding: '4px 12px',
backdropFilter: 'blur(4px)',
transition: 'all 0.2s ease',
fontWeight: '500',
boxShadow: '0 2px 4px rgba(102, 126, 234, 0.3)'
}),
getMouseEnterStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)';
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 6px 12px rgba(102, 126, 234, 0.4)';
},
getMouseLeaveStyle: (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 4px rgba(102, 126, 234, 0.3)';
}
};
// 工具函数:根据类型获取对应的按钮样式
const getButtonStyle = (type: 'primary' | 'danger' | 'success' | 'warning' | 'info' | 'default' | 'purple' | 'pink' | 'gradient') => {
const styleMap = {
primary: buttonPrimary,
danger: buttonDanger,
success: buttonSuccess,
warning: buttonWarning,
info: buttonInfo,
default: buttonDefault,
purple: buttonPurple,
pink: buttonPink,
gradient: buttonGradient
};
return styleMap[type];
};
return {
buttonPrimary,
buttonDanger,
buttonSuccess,
buttonWarning,
buttonInfo,
buttonDefault,
buttonPurple,
buttonPink,
buttonGradient,
getButtonStyle
};
};

View File

@ -25,6 +25,12 @@ export default {
'menu.other.machine-id-authorization': '机器码授权', 'menu.other.machine-id-authorization': '机器码授权',
'menu.other.data-info': '数据信息', 'menu.other.data-info': '数据信息',
'menu.mjpackage': '生图包管理',
'menu.mjpackage.token-management': 'Token管理',
'menu.mjpackage.task-management': '任务管理',
'menu.more-blocks': '更多区块', 'menu.more-blocks': '更多区块',
'menu.home': '首页', 'menu.home': '首页',
'menu.admin': '管理页', 'menu.admin': '管理页',

View File

@ -1,6 +1,6 @@
{ {
"name": "Ant Design Pro", "name": "LaiTool Management System",
"short_name": "Ant Design Pro", "short_name": "LaiTool Management System",
"display": "standalone", "display": "standalone",
"start_url": "./?utm_source=homescreen", "start_url": "./?utm_source=homescreen",
"theme_color": "#002140", "theme_color": "#002140",

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -4,13 +4,12 @@ import TemplateContainer from "@/pages/TemplateContainer";
import { DeactivationMachine, MachinePermanent, QueryMachineList } from "@/services/services/machine"; import { DeactivationMachine, MachinePermanent, QueryMachineList } from "@/services/services/machine";
import { FormatDate } from "@/util/time"; import { FormatDate } from "@/util/time";
import { useAccess, useModel } from "@umijs/max"; import { useAccess, useModel } from "@umijs/max";
import { Button, Dropdown, Form, Input, Menu, message, Modal, Select, SelectProps, Spin, Table, Tag } from "antd"; import { Button, Dropdown, Form, Input, message, Modal, Select, Spin, Table, Tag } from "antd";
import { ColumnsType, TablePaginationConfig } from "antd/es/table"; import { ColumnsType, TablePaginationConfig } from "antd/es/table";
import { FilterValue, SorterResult, TableCurrentDataSource } from "antd/es/table/interface"; import { FilterValue, SorterResult, TableCurrentDataSource } from "antd/es/table/interface";
import { delay, set } from "lodash";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ModifyMachine from "../ModifyMachine"; import ModifyMachine from "../ModifyMachine";
import { DownOutlined, EditOutlined, MenuOutlined, MoreOutlined, PlusOutlined, SafetyCertificateOutlined, StopOutlined } from "@ant-design/icons"; import { EditOutlined, MenuOutlined, PlusOutlined, SafetyCertificateOutlined, StopOutlined } from "@ant-design/icons";
import AddMachineForm from "../AddMachineForm"; import AddMachineForm from "../AddMachineForm";
const MachineManagement: React.FC = () => { const MachineManagement: React.FC = () => {

View File

@ -1,5 +1,6 @@
import { AllOptionKeyName } from '@/services/enum/optionEnum'; import { AllOptionKeyName } from '@/services/enum/optionEnum';
import { GetOptions, getOptionsStringValue, SaveOptions } from '@/services/services/options/optionsTool'; import { GetOptions, getOptionsStringValue, SaveOptions } from '@/services/services/options/optionsTool';
import { OptionModel } from '@/services/typing/options/option';
import { useOptionsStore } from '@/store/options'; import { useOptionsStore } from '@/store/options';
import { useSoftStore } from '@/store/software'; import { useSoftStore } from '@/store/software';
import { Button, Card, Form, Input } from 'antd'; import { Button, Card, Form, Input } from 'antd';

View File

@ -1,5 +1,6 @@
import { AllOptionKeyName } from '@/services/enum/optionEnum'; import { AllOptionKeyName } from '@/services/enum/optionEnum';
import { GetOptions, getOptionsStringValue, SaveOptions } from '@/services/services/options/optionsTool'; import { GetOptions, getOptionsStringValue, SaveOptions } from '@/services/services/options/optionsTool';
import { OptionModel } from '@/services/typing/options/option';
import { useOptionsStore } from '@/store/options'; import { useOptionsStore } from '@/store/options';
import { useSoftStore } from '@/store/software'; import { useSoftStore } from '@/store/software';
import { Button, Card, Col, Form, Input, message, Row } from 'antd'; import { Button, Card, Col, Form, Input, message, Row } from 'antd';

View File

@ -1,12 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Form, Input, DatePicker, Select, Button, message, FormInstance, Space } from 'antd'; import { Form, Input, DatePicker, Select, Button, message, FormInstance } from 'antd';
import moment from 'moment';
import { useNavigate } from 'react-router-dom';
import { GetMachineAuthorizationTypeOptions } from '@/services/enum/machineAuthorizationEnum'; import { GetMachineAuthorizationTypeOptions } from '@/services/enum/machineAuthorizationEnum';
const { TextArea } = Input;
import CryptoJS from 'crypto-js'; // 添加这一行导入 import CryptoJS from 'crypto-js'; // 添加这一行导入
import * as LZString from 'lz-string'; import * as LZString from 'lz-string';
import cusRequest from '@/request';
import { AddMachineIdAuthorizationFunc } from '@/services/services/other'; import { AddMachineIdAuthorizationFunc } from '@/services/services/other';
@ -136,49 +132,6 @@ const AddMachineIdAuthorization: React.FC<AddMachineIdAuthorizationProps> = ({ s
// } // }
} }
function DecryptAuthorizationCode(authCode: string, machineId: string) {
try {
// 解压缩
const originalAuthCode = LZString.decompressFromEncodedURIComponent(authCode);
// 拆分授权码获取IV和加密数据
const [ivBase64, encryptedBase64] = originalAuthCode.split(':');
if (!ivBase64 || !encryptedBase64) {
throw new Error('无效的授权码格式');
}
// 从Base64转换回IV
const iv = CryptoJS.enc.Base64.parse(ivBase64);
// 使用相同的方法生成密钥
const secretKey = machineId;
const key = CryptoJS.enc.Utf8.parse(CryptoJS.SHA256(secretKey).toString());
// 解密数据
const decrypted = CryptoJS.AES.decrypt(encryptedBase64, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
// 将解密后的数据转换为字符串
const decryptedData = decrypted.toString(CryptoJS.enc.Utf8);
// 将JSON字符串解析为对象
const decodedObject = JSON.parse(decryptedData);
return {
success: true,
data: decodedObject
};
} catch (error) {
console.error('解密授权码时出错:', error);
return {
success: false,
error: error instanceof Error ? error.message : '未知错误'
};
}
}
return ( return (

View File

@ -4,12 +4,10 @@ import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
import TemplateContainer from '@/pages/TemplateContainer'; import TemplateContainer from '@/pages/TemplateContainer';
import { useModel } from '@umijs/max'; import { useModel } from '@umijs/max';
import { ColumnsType, FilterValue, SorterResult, TableCurrentDataSource, TablePaginationConfig } from 'antd/es/table/interface'; import { ColumnsType, FilterValue, SorterResult, TableCurrentDataSource, TablePaginationConfig } from 'antd/es/table/interface';
import { objectToQueryString } from '@/services/services/common';
import { FormatDate } from '@/util/time'; import { FormatDate } from '@/util/time';
import { GetMachineAuthorizationTypeOption, GetMachineAuthorizationTypeOptions } from '@/services/enum/machineAuthorizationEnum'; import { GetMachineAuthorizationTypeOption, GetMachineAuthorizationTypeOptions } from '@/services/enum/machineAuthorizationEnum';
import { useFormReset } from '@/hooks/useFormReset'; import { useFormReset } from '@/hooks/useFormReset';
import AddMachineIdAuthorization from './AddMachineIdAuthorization'; import AddMachineIdAuthorization from './AddMachineIdAuthorization';
import cusRequest from '@/request';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import ModifyMachineIdAuthorization from './ModifyMachineIdAuthorization'; import ModifyMachineIdAuthorization from './ModifyMachineIdAuthorization';
import { BatchDeleteMachine, DeleteMachineAuthorization, QueryMachineAuthorization } from '@/services/services/other'; import { BatchDeleteMachine, DeleteMachineAuthorization, QueryMachineAuthorization } from '@/services/services/other';

View File

@ -4,16 +4,13 @@ import { QueryRoleOption } from "@/services/services/role";
import { UserInfo } from "@/services/services/user"; import { UserInfo } from "@/services/services/user";
import { FormatDate } from "@/util/time"; import { FormatDate } from "@/util/time";
import { useAccess, useModel } from "@umijs/max"; import { useAccess, useModel } from "@umijs/max";
import { Button, Dropdown, Form, Input, InputNumber, message, Modal, Select, SelectProps, Table, Tag, Tooltip } from "antd"; import { Button, Dropdown, Form, Input, message, Modal, Select, SelectProps, Table, Tag, Tooltip } from "antd";
import { ColumnsType, TablePaginationConfig } from "antd/es/table"; import { ColumnsType, TablePaginationConfig } from "antd/es/table";
import { FilterValue, SorterResult, TableCurrentDataSource } from "antd/es/table/interface"; import { FilterValue, SorterResult, TableCurrentDataSource } from "antd/es/table/interface";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ModifyUser from "../ModifyUser"; import ModifyUser from "../ModifyUser";
import Icon, { DeleteOutlined, EditOutlined, KeyOutlined, MenuOutlined, SyncOutlined, ToolOutlined } from "@ant-design/icons"; import { DeleteOutlined, EditOutlined, KeyOutlined, MenuOutlined, SyncOutlined, ToolOutlined } from "@ant-design/icons";
import { SoftwareControl } from "@/services/services/software"; import { SoftwareControl } from "@/services/services/software";
import { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon";
import DiceIcon from "@/components/Icon/DiceIcon";
import { generateRandomPassword } from "@/util/password";
import ResetUserPassword from "./ResetUserPassword"; import ResetUserPassword from "./ResetUserPassword";
import SofrwareControlManagement from "@/pages/Software/SofrwareControl/SofrwareControlManagement"; import SofrwareControlManagement from "@/pages/Software/SofrwareControl/SofrwareControlManagement";

View File

@ -6,10 +6,12 @@ import * as api from './api';
import * as login from './login'; import * as login from './login';
import * as role from './role'; import * as role from './role';
import * as user from './user'; import * as user from './user';
import * as mjp from './mjp';
export default { export default {
api, api,
login, login,
role, role,
user user,
mjp
}; };

View File

@ -0,0 +1,370 @@
import cusRequest from "@/request";
import { isEmpty } from "lodash";
import { objectToQueryString } from "./common";
import { QueryTokenParams } from "@/pages/MJPackage/TokenManagement";
import { QueryTaskParams } from "@/pages/MJPackage/TaskManagement";
/**
* Token缓存信息
* @description Token字符串获取对应的Token缓存项信息使使
* @param token - Token字符串
* @returns Promise<MJP.TokenCacheItem> - Token缓存项信息的Promise对象
* @throws {Error} Token为空时抛出错误
* @throws {Error} API请求失败时抛出错误
* @example
* ```typescript
* try {
* const tokenInfo = await getTokenCacheIten('your-token-here');
* console.log('Token信息:', tokenInfo);
* console.log('每日限制:', tokenInfo.dailyLimit);
* console.log('已使用次数:', tokenInfo.dailyUsage);
* } catch (error) {
* console.error('获取Token信息失败:', error.message);
* }
* ```
*/
export async function getTokenCacheIten(token: string): Promise<MJP.TokenCacheItem> {
if (isEmpty(token)) {
throw new Error("Token不能为空");
}
// 开始调用请求token信息接口
const res = await cusRequest<MJP.TokenCacheItem>(`/api/TokenManagement/GetTokenItem/${token}`, {
method: 'GET',
});
if (res.code != 1) {
throw new Error(res.message || '获取Token信息失败');
}
return res.data as MJP.TokenCacheItem;
}
/**
*
* @description Token和分页参数查询MJ任务集合ID筛选
* @param token - Token字符串
* @param tableParams -
* @param thirdPartyTaskId - ID
* @returns Promise<MJP.QueryTaskData> - Promise对象
* @throws {Error} API请求失败时抛出错误
* @example
* ```typescript
* try {
* const tableParams = {
* pagination: {
* current: 1,
* pageSize: 10
* }
* };
* const taskData = await queryTaskList('your-token', tableParams, 'task-id-123');
* console.log('任务列表:', taskData.list);
* console.log('总数:', taskData.total);
* } catch (error) {
* console.error('查询任务列表失败:', error.message);
* }
* ```
*/
export async function queryTaskList(token: string, tableParams: TableModel.TableParams, thirdPartyTaskId?: string): Promise<MJP.QueryTaskData> {
let data = {
thirdPartyTaskId,
page: tableParams.pagination?.current,
pageSize: tableParams.pagination?.pageSize,
}
let query = objectToQueryString(data)
let res = await cusRequest<MJP.QueryTaskData>(`/api/TokenManagement/QueryTokenTaskCollection/${token}?${query}`, {
method: 'GET',
});
if (res.code != 1) {
throw new Error(res.message);
}
return res.data as MJP.QueryTaskData;
}
/**
* Token基础信息
* @description Token集合的基础信息Token管理页面的数据展示
* @param tableParams -
* @param tableParams.pagination -
* @param tableParams.pagination.current - 1
* @param tableParams.pagination.pageSize -
* @param tokenParams - Token查询参数对象
* @param tokenParams.token - Token字符串
* @param tokenParams.tokenId - Token的数字IDID查询
* @returns Promise<BasicModel.QueryCollection<MJP.TokenCacheItem>> - Token集合查询结果的Promise对象
* @returns {Promise<{collection: MJP.TokenCacheItem[], total: number, page: number, pageSize: number}>} Token列表
* @throws {Error} API请求失败时抛出错误
* @example
* ```typescript
* // 基础分页查询所有Token
* const tableParams = {
* pagination: {
* current: 1,
* pageSize: 10
* }
* };
* const tokenParams = {};
*
* try {
* const result = await adminQueryTokenBasic(tableParams, tokenParams);
* console.log('Token列表:', result.collection);
* console.log('总数:', result.total);
* console.log('当前页:', result.page);
* } catch (error) {
* console.error('查询Token列表失败:', error.message);
* }
* ```
* @since 1.0.0
* @author AI Assistant
* @access admin -
*/
export async function adminQueryTokenBasic(tableParams: TableModel.TableParams, tokenParams: QueryTokenParams) {
let data = {
...tokenParams,
page: tableParams.pagination?.current,
pageSize: tableParams.pagination?.pageSize,
}
let query = objectToQueryString(data)
let res = await cusRequest<BasicModel.QueryCollection<MJP.TokenCacheItem[]>>(`/api/TokenManagement/QueryTokenCollection?${query}`, {
method: 'GET',
});
if (res.code != 1) {
throw new Error(res.message);
}
return res.data as BasicModel.QueryCollection<MJP.TokenCacheItem[]>;
}
/**
* Token缓存统计信息
* @description Token缓存统计
* @returns Promise<MJP.MJPHealthAndCacheResponse> - Promise对象
* @returns {Promise<{status: string, timestamp: string, cacheStats: object, uptime: string}>}
* @throws {Error} API请求失败时抛出错误
* @example
* ```typescript
* try {
* const healthData = await adminGetCacheTokenData();
* console.log('系统状态:', healthData.status); // 'Healthy'
* console.log('时间戳:', healthData.timestamp); // '2025-06-10T15:58:35.0185517'
* console.log('总Token数:', healthData.cacheStats.totalTokens); // 0
* console.log('活跃Token数:', healthData.cacheStats.activeTokens); // 0
* console.log('系统运行时间:', healthData.uptime); // '08:04:23.4814517'
* } catch (error) {
* console.error('获取系统健康状态失败:', error.message);
* }
* ```
* @access admin -
*/
export async function adminGetHealthAndCacheTokenData() {
let res = await cusRequest<MJP.MJPHealthAndCacheResponse>(`/api/TokenManagement/GetHealth`, {
method: 'GET',
});
if (res.code != 1) {
throw new Error(res.message);
}
return res.data as MJP.MJPHealthAndCacheResponse;
}
/**
* Token
* @description Token到系统中使
* @param tokenParams - Token参数对象Token字符串和各种限制配置
* @param tokenParams.token - Token字符串
* @param tokenParams.dailyLimit - 使0
* @param tokenParams.totalLimit - 使0
* @param tokenParams.concurrencyLimit - 0
* @param tokenParams.useDayCount - 使0
* @returns Promise<string> - Promise对象
* @throws {Error} Token为空时抛出"Token不能为空"
* @throws {Error} API请求失败时抛出错误
* @example
* ```typescript
* try {
* const tokenParams = {
* token: 'sk-1234567890abcdef',
* dailyLimit: 100,
* totalLimit: 1000,
* concurrencyLimit: 5,
* useDayCount: 30
* };
* const result = await adminAddToken(tokenParams);
* console.log('添加Token成功:', result);
* } catch (error) {
* console.error('添加Token失败:', error.message);
* }
* ```
* @access admin -
*/
export async function adminAddToken(tokenParams: MJP.AddAndModifyTokenParams): Promise<string> {
// 验证Token参数不能为空
if (isEmpty(tokenParams.token)) {
throw new Error("Token不能为空");
}
// 发起POST请求添加新Token
let res = await cusRequest<string>(`/api/TokenManagement/AddToken`, {
method: 'POST',
data: tokenParams,
});
// 检查响应状态,如果不是成功状态则抛出错误
if (res.code != 1) {
throw new Error(res.message);
}
// 返回操作结果消息
return res.data as string;
}
/**
* ID获取Token详情
* @description Token ID获取Token的详细信息Token时加载数据
* @param tokenId - Token的唯一标识ID0
* @returns Promise<MJP.TokenDetailInfo> - Token详情信息的Promise对象
* @throws {Error} Token ID无效时抛出错误
* @throws {Error} API请求失败时抛出错误
* @example
* ```typescript
* try {
* const tokenDetail = await adminGetTokenById(3);
* console.log('Token详情:', tokenDetail);
* console.log('Token字符串:', tokenDetail.token);
* console.log('每日限制:', tokenDetail.dailyLimit);
* console.log('创建时间:', tokenDetail.createdAt);
* } catch (error) {
* console.error('获取Token详情失败:', error.message);
* }
* ```
* @since 1.0.0
* @author AI Assistant
* @access admin -
*/
export async function adminGetTokenById(tokenId: number): Promise<MJP.MJAPITokens> {
// 验证Token ID参数
if (!tokenId || tokenId <= 0) {
throw new Error("Token ID无效");
}
// 发起GET请求获取Token详情
let res = await cusRequest<MJP.MJAPITokens>(`/api/TokenManagement/QueryTokenById/${tokenId}`, {
method: 'GET',
});
// 检查响应状态,如果不是成功状态则抛出错误
if (res.code != 1) {
throw new Error(res.message);
}
// 返回Token详情信息
return res.data as MJP.MJAPITokens;
}
/**
* Token信息
* @description Token的配置信息使
* @param tokenParams - Token修改参数对象ID和要修改的字段
* @param tokenParams.id - Token的唯一标识IDToken
* @param tokenParams.token - Token字符串
* @param tokenParams.dailyLimit - 使0
* @param tokenParams.totalLimit - 使0
* @param tokenParams.concurrencyLimit - 0
* @param tokenParams.useDayCount - 使0
* @returns Promise<string> - Promise对象
* @throws {Error} Token ID无效时抛出错误
* @throws {Error} Token为空时抛出"Token不能为空"
* @throws {Error} API请求失败时抛出错误
* @example
* ```typescript
* try {
* const tokenParams = {
* id: 3,
* token: 'sk-1234567890abcdef',
* dailyLimit: 200,
* totalLimit: 2000,
* concurrencyLimit: 3,
* useDayCount: 60
* };
* const result = await adminModifyToken(tokenParams);
* console.log('修改Token成功:', result);
* } catch (error) {
* console.error('修改Token失败:', error.message);
* }
* ```
* @since 1.0.0
* @author AI Assistant
* @access admin -
*/
export async function adminModifyToken(tokenId: number, tokenParams: MJP.AddAndModifyTokenParams): Promise<string> {
// 验证Token ID参数
if (!tokenId || tokenId <= 0) {
throw new Error("Token ID无效");
}
// 验证Token参数不能为空
if (isEmpty(tokenParams.token)) {
throw new Error("Token不能为空");
}
// 发起PUT请求修改Token
let res = await cusRequest<string>(`/api/TokenManagement/ModifyToken/${tokenId}`, {
method: 'POST',
data: tokenParams,
});
// 检查响应状态,如果不是成功状态则抛出错误
if (res.code != 1) {
throw new Error(res.message);
}
// 返回操作结果消息
return res.data as string;
}
/**
*
* @description Task
* @param tableParams -
* @param tableParams.pagination -
* @param tableParams.pagination.current - 1
* @param tableParams.pagination.pageSize -
* @param taskParams -
* @param taskParams.thirdPartyTaskId - ID
* @param taskParams.token - Token
* @param taskParams.tokenId -
* @returns Promise<BasicModel.QueryCollection<MJP.MJApiTasks[]>> - Promise对象
* @returns {Promise<{collection: MJP.MJApiTasks[], total: number, page: number, pageSize: number}>}
* @throws {Error} API请求失败时抛出错误
*/
export async function adminQueryTaskCollection(tableParams: TableModel.TableParams, taskParams: QueryTaskParams) {
let data = {
...taskParams,
page: tableParams.pagination?.current,
pageSize: tableParams.pagination?.pageSize,
}
let query = objectToQueryString(data)
let res = await cusRequest<BasicModel.QueryCollection<MJP.MJApiTasks[]>>(`/api/TokenManagement/QueryTaskCollection?${query}`, {
method: 'GET',
});
if (res.code != 1) {
throw new Error(res.message);
}
return res.data as BasicModel.QueryCollection<MJP.MJApiTasks[]>;
}
/**
*
* @description
* @returns Promise<MJP.TaskStatistics> - Promise对象
* @returns {Promise<{totalTasks: number, completedTasks: number, failedTasks: number, inProgressTasks: number}>}
* @throws {Error} API请求失败时抛出错误
* @example
* @access admin -
* @see MJP.TaskStatistics
*/
export async function adminGetDayTaskStatistics() {
let res = await cusRequest<MJP.TaskStatistics>(`/api/TokenManagement/GetDayTaskStatistics`, {
method: 'GET',
});
if (res.code != 1) {
throw new Error(res.message);
}
return res.data as MJP.TaskStatistics;
}

View File

@ -3,7 +3,7 @@ declare namespace AccessType {
canPrompt: boolean; canPrompt: boolean;
canRoleManagement: boolean; canRoleManagement: boolean;
/** 是不是显示数据管理 */ /** 是不是显示数据管理 */
canOptionManagement : boolean; canOptionManagement: boolean;
//#region 用户权限 //#region 用户权限
/** 是不是显示用户管理的菜单 */ /** 是不是显示用户管理的菜单 */
@ -72,6 +72,14 @@ declare namespace AccessType {
canAddForeverSoftwareControl: boolean; canAddForeverSoftwareControl: boolean;
/** 是否可以删除软件控制权限 */ /** 是否可以删除软件控制权限 */
canDeleteSoftwareControl: boolean; canDeleteSoftwareControl: boolean;
//#endregion
//#region 生图包权限
/** 是不是可以管理生图包 */
canManagementMJPackage: boolean;
//#endregion //#endregion
} }
} }

186
src/services/typing/mjp.d.ts vendored Normal file
View File

@ -0,0 +1,186 @@
declare namespace MJP {
/**
* MJ API
*/
interface MJApiTasks {
/** 任务ID (主键) */
taskId: string;
/** Token */
token: string;
/** TokenId */
tokenId: number;
/** 开始时间 */
startTime: Date;
/** 结束时间 */
endTime?: Date | null;
/** 状态 */
status: string;
/** 第三方任务ID */
thirdPartyTaskId: string;
/** 属性 */
properties?: string | null;
/** 属性的JSON数据 */
propertieJson?: Record<string, any> | null;
}
/**
* Token详情信息接口
* @description Token的完整信息结构IDToken字符串
*/
interface MJAPITokens {
/** Token的唯一标识ID */
id: number;
/** Token字符串用于API认证 */
token: string;
/** 实际使用的TOKEN */
useToken: string;
/** 每日使用限制必须大于0 */
dailyLimit: number;
/** 总使用限制必须大于0 */
totalLimit: number;
/** 并发请求限制必须大于0 */
concurrencyLimit: number;
/** Token创建时间ISO格式的日期时间字符串 */
createdAt: Date;
/** Token过期时间ISO格式的日期时间字符串 */
expiresAt?: Date | null;
}
/**
* Token
*/
interface TokenCacheItem extends MJAPITokens {
/** 每日使用量 */
dailyUsage: number;
/** 总使用量 */
totalUsage: number;
/** 最后活动时间 */
lastActivityTime: Date;
/** 历史使用记录 */
historyUse?: string | null;
historyUseJson?: Record<string, any> | null;
/** 占用并发 */
currentlyExecuting: number;
}
/**
* Token
*/
interface TokenAndTaskCollection extends TokenCacheItem {
/** 任务集合 */
taskCollections: Array<MJApiTasks>
}
/**
*
*/
type QueryTaskData = {
/** 任务集合信息 */
collection: TokenAndTaskCollection[];
/** 当前页 */
current: number;
/** 总数 */
total: number;
}
/**
* Token参数接口
* @description Token时的参数结构
*/
interface AddAndModifyTokenParams {
/** Token字符串用于API认证 */
token: string;
/** 实际使用得TOKEN */
useToken: string;
/** 每日使用限制必须大于0 */
dailyLimit: number;
/** 总使用限制必须大于0 */
totalLimit: number;
/** 并发请求限制必须大于0 */
concurrencyLimit: number;
/** 可使用天数大于0 会生效 小于零 不会生效 不生效传 -1 */
useDayCount: number;
}
/**
* Token缓存统计信息接口
* @description Token缓存系统的统计数据结构
*/
interface TokenCacheStats {
/** 总Token数量 */
totalTokens: number;
/** 活跃Token数量 */
activeTokens: number;
/** 非活跃Token数量 */
inactiveTokens: number;
/** 每日总使用量 */
totalDailyUsage: number;
/** 总使用量 */
totalUsage: number;
}
/**
*
* @description
*/
interface MJPHealthAndCacheResponse {
/** 系统状态,如 'Healthy'、'Unhealthy' 等 */
status: string;
/** 时间戳ISO格式的日期时间字符串 */
timestamp: string;
/** Token缓存统计信息 */
cacheStats: TokenCacheStats;
/** 系统运行时间,格式如 "08:04:23.4814517" */
uptime: string;
}
/**
*
* @description
*/
interface TaskStatistics {
/** 总任务数量 */
totalTasks: number;
/** 已完成任务数量 */
completedTasks: number;
/** 失败任务数量 */
failedTasks: number;
/** 进行中任务数量 */
inProgressTasks: number;
}
}

22
src/store/mjp.ts Normal file
View File

@ -0,0 +1,22 @@
import { create } from 'zustand';
interface MJPState {
/** Token 缓存项信息 */
tokenCacheItem: MJP.TokenCacheItem | null;
/** 设置 Token 缓存项 */
setTokenCacheItem: (tokenCacheItem: MJP.TokenCacheItem | null) => void;
}
export const useMJPStore = create<MJPState>((set, get) => ({
/** Token 缓存项信息 */
tokenCacheItem: null,
/** 设置 Token 缓存项 */
setTokenCacheItem: (tokenCacheItem: MJP.TokenCacheItem | null) => {
console.log('Store收到数据:', tokenCacheItem);
set({ tokenCacheItem: tokenCacheItem });
},
}));

View File

@ -1,3 +1,4 @@
import { OptionModel } from '@/services/typing/options/option';
import { create } from 'zustand'; import { create } from 'zustand';
export const useOptionsStore = create((set: (arg0: any) => any) => ({ export const useOptionsStore = create((set: (arg0: any) => any) => ({

8
src/util/text.ts Normal file
View File

@ -0,0 +1,8 @@
// 在组件顶部定义脱敏函数
export const formatTokenDisplay = (token: string | undefined, showLength: number = 8) => {
if (!token) return '';
if (token.length <= showLength) {
return token.substring(0, 2) + '*'.repeat(Math.max(0, token.length - 2));
}
return token.substring(0, showLength) + '*'.repeat(4);
};

View File

@ -1,4 +1,4 @@
export function FormatDate(date: Date): string { export function FormatDate(date: Date, onlyDay: boolean = false): string {
// 如果传入的是字符串,尝试将其转换为 Date 对象 // 如果传入的是字符串,尝试将其转换为 Date 对象
if (typeof date === 'string') { if (typeof date === 'string') {
date = new Date(date); date = new Date(date);
@ -6,6 +6,11 @@ export function FormatDate(date: Date): string {
if (!(date instanceof Date) || isNaN(date.getTime())) { if (!(date instanceof Date) || isNaN(date.getTime())) {
return ''; return '';
} }
if (onlyDay) {
return date.getFullYear() + '-' +
pad(date.getMonth() + 1) + '-' +
pad(date.getDate());
}
return date.getFullYear() + '-' + return date.getFullYear() + '-' +
pad(date.getMonth() + 1) + '-' + pad(date.getMonth() + 1) + '-' +
pad(date.getDate()) + ' ' + pad(date.getDate()) + ' ' +
@ -19,3 +24,12 @@ function pad(number: number) {
return (number < 10 ? '0' : '') + number; return (number < 10 ? '0' : '') + number;
} }
/**
* Promise
* @param time
* @returns viod
*/
export async function TimeDelay(time: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, time));
}