From 5c5d79d126302584ce6861da53d3da7adac53ec0 Mon Sep 17 00:00:00 2001
From: lq1405 <2769838458@qq.com>
Date: Sat, 14 Jun 2025 22:14:20 +0800
Subject: [PATCH] =?UTF-8?q?v=201.1.2=20=E7=94=9F=E5=9B=BE=E5=8C=85?=
=?UTF-8?q?=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
config/config.ts | 2 +-
config/filingConfig.ts | 16 +
config/oneapi.json | 2 +-
config/routes.ts | 56 +-
config/systemConfig.ts | 11 +
package.json | 3 +-
src/access.ts | 9 +-
src/app.tsx | 24 +-
src/components/Footer/index.css | 21 +
src/components/Footer/index.tsx | 63 +-
src/hooks/useGlassButtonStyles.ts | 264 +++++++
src/locales/zh-CN/menu.ts | 6 +
src/manifest.json | 4 +-
src/pages/MJPackage/MJPriceInfo.tsx | 557 ++++++++++++++
src/pages/MJPackage/TaskManagement.tsx | 617 ++++++++++++++++
src/pages/MJPackage/TaskMessageInfo.css | 320 ++++++++
src/pages/MJPackage/TaskMessageInfo.tsx | 130 ++++
.../MJPackage/TaskMessageInfo/InfoCards.tsx | 144 ++++
.../MJPackage/TaskMessageInfo/TaskTable.tsx | 493 +++++++++++++
src/pages/MJPackage/TokenLogin.css | 44 ++
src/pages/MJPackage/TokenLogin.tsx | 104 +++
src/pages/MJPackage/TokenManagement.tsx | 630 ++++++++++++++++
.../MJPackage/TokenManagement/AddToken.tsx | 182 +++++
.../MJPackage/TokenManagement/ModifyToken.tsx | 234 ++++++
.../MJPackage/TokenManagement/TaskInfo.tsx | 564 ++++++++++++++
.../MJPackage/TokenManagement/TokenInfo.tsx | 696 ++++++++++++++++++
src/pages/Machine/MachineManagement/index.tsx | 5 +-
.../BasicOptions/SimpleOptions/index.tsx | 1 +
.../DubSetting/DubSettingTTsOptions/index.tsx | 1 +
.../AddMachineIdAuthorization.tsx | 49 +-
.../Other/MachineIdAuthorization/index.tsx | 2 -
.../User/UserManage/UserManagement/index.tsx | 7 +-
src/services/services/index.ts | 4 +-
src/services/services/mjp.ts | 370 ++++++++++
src/services/typing/access.d.ts | 10 +-
src/services/typing/mjp.d.ts | 186 +++++
src/store/mjp.ts | 22 +
src/store/options.ts | 1 +
src/util/text.ts | 8 +
src/util/time.ts | 16 +-
40 files changed, 5781 insertions(+), 97 deletions(-)
create mode 100644 config/filingConfig.ts
create mode 100644 config/systemConfig.ts
create mode 100644 src/components/Footer/index.css
create mode 100644 src/hooks/useGlassButtonStyles.ts
create mode 100644 src/pages/MJPackage/MJPriceInfo.tsx
create mode 100644 src/pages/MJPackage/TaskManagement.tsx
create mode 100644 src/pages/MJPackage/TaskMessageInfo.css
create mode 100644 src/pages/MJPackage/TaskMessageInfo.tsx
create mode 100644 src/pages/MJPackage/TaskMessageInfo/InfoCards.tsx
create mode 100644 src/pages/MJPackage/TaskMessageInfo/TaskTable.tsx
create mode 100644 src/pages/MJPackage/TokenLogin.css
create mode 100644 src/pages/MJPackage/TokenLogin.tsx
create mode 100644 src/pages/MJPackage/TokenManagement.tsx
create mode 100644 src/pages/MJPackage/TokenManagement/AddToken.tsx
create mode 100644 src/pages/MJPackage/TokenManagement/ModifyToken.tsx
create mode 100644 src/pages/MJPackage/TokenManagement/TaskInfo.tsx
create mode 100644 src/pages/MJPackage/TokenManagement/TokenInfo.tsx
create mode 100644 src/services/services/mjp.ts
create mode 100644 src/services/typing/mjp.d.ts
create mode 100644 src/store/mjp.ts
create mode 100644 src/util/text.ts
diff --git a/config/config.ts b/config/config.ts
index f0004a8..faf350d 100644
--- a/config/config.ts
+++ b/config/config.ts
@@ -76,7 +76,7 @@ export default defineConfig({
* @name layout 插件
* @doc https://umijs.org/docs/max/layout-menu
*/
- title: 'Ant Design Pro',
+ title: 'LaiTool Management System',
layout: {
locale: true,
...defaultSettings,
diff --git a/config/filingConfig.ts b/config/filingConfig.ts
new file mode 100644
index 0000000..e0b2520
--- /dev/null
+++ b/config/filingConfig.ts
@@ -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,
+ },
+}
\ No newline at end of file
diff --git a/config/oneapi.json b/config/oneapi.json
index f8b31e8..d3c2068 100644
--- a/config/oneapi.json
+++ b/config/oneapi.json
@@ -1,7 +1,7 @@
{
"openapi": "3.0.1",
"info": {
- "title": "Ant Design Pro",
+ "title": "LaiTool Management System",
"version": "1.0.0"
},
"servers": [{
diff --git a/config/routes.ts b/config/routes.ts
index c4de3af..dcbd556 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -1,6 +1,4 @@
-import { access } from "fs";
-
-/**
+/**
* @name umi 的路由配置
* @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置
* @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。
@@ -17,6 +15,10 @@ export default [
path: '/user',
layout: false,
routes: [
+ {
+ path: '/user',
+ redirect: '/user/login',
+ },
{
name: 'login',
path: '/user/login',
@@ -28,8 +30,33 @@ export default [
path: '/user/register',
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',
@@ -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: '/',
redirect: '/welcome',
diff --git a/config/systemConfig.ts b/config/systemConfig.ts
new file mode 100644
index 0000000..7c901b8
--- /dev/null
+++ b/config/systemConfig.ts
@@ -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",
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 5ef4d39..efbe862 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,8 @@
],
"dependencies": {
"@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",
"@umijs/route-utils": "^2.2.2",
"ahooks": "^3.8.4",
diff --git a/src/access.ts b/src/access.ts
index 66e0a39..4fbd787 100644
--- a/src/access.ts
+++ b/src/access.ts
@@ -40,6 +40,7 @@ export default function access(initialState: { currentUser?: API.CurrentUser } |
canAddForeverSoftwareControl: false,
canDeleteSoftwareControl: false,
+ canManagementMJPackage: false
} as AccessType.AccessType;
@@ -88,7 +89,7 @@ export default function access(initialState: { currentUser?: API.CurrentUser } |
access = {
...access,
canPrompt: true,
- canOptionManagement : true,
+ canOptionManagement: true,
canUserManagement: true,
canEditUser: true,
@@ -123,8 +124,8 @@ export default function access(initialState: { currentUser?: API.CurrentUser } |
...access,
canPrompt: true,
canRoleManagement: true,
- canOptionManagement : true,
-
+ canOptionManagement: true,
+
canUserManagement: true,
canEditUser: true,
canDeleteUser: true,
@@ -151,6 +152,8 @@ export default function access(initialState: { currentUser?: API.CurrentUser } |
canAddYearSoftwareControl: true,
canAddForeverSoftwareControl: true,
canDeleteSoftwareControl: true,
+
+ canManagementMJPackage: true
};
}
console.log("accsee", access);
diff --git a/src/app.tsx b/src/app.tsx
index 1662a8e..845535c 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,13 +1,12 @@
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 { SettingDrawer } from '@ant-design/pro-components';
import type { RunTimeLayoutConfig } from '@umijs/max';
-import { history, Link } from '@umijs/max';
+import { history } from '@umijs/max';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
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 { App, ConfigProvider } from 'antd';
import cusRequest from './request';
@@ -57,7 +56,21 @@ export async function getInitialState(): Promise<{
// 如果不是登录页面,执行
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 currentUser = currentUserString ? JSON.parse(currentUserString) : null;
let token = localStorage.getItem('token') ?? null;
@@ -111,8 +124,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
},
footerRender: () => (
),
onPageChange: () => {
diff --git a/src/components/Footer/index.css b/src/components/Footer/index.css
new file mode 100644
index 0000000..682cea9
--- /dev/null
+++ b/src/components/Footer/index.css
@@ -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;
+}
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
index a7ab9f5..b79618c 100644
--- a/src/components/Footer/index.tsx
+++ b/src/components/Footer/index.tsx
@@ -1,26 +1,53 @@
-import { GithubOutlined } from '@ant-design/icons';
-import { DefaultFooter } from '@ant-design/pro-components';
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 = () => {
return (
-
+
+
+
+ © {filingConfig.copyright}. All rights reserved.
+
+ |} size={16}>
+ {
+ filingConfig.gonxin.show ?
+
+ {filingConfig.gonxin.title}
+ : null
+ }
+ {
+ filingConfig.gongan.show ?
+
+ {filingConfig.gongan.title}
+ : null
+ }
+
+ 版本:v1.0.0
+
+
+
+
);
};
export default Footer;
+
+//
diff --git a/src/hooks/useGlassButtonStyles.ts b/src/hooks/useGlassButtonStyles.ts
new file mode 100644
index 0000000..ef555b7
--- /dev/null
+++ b/src/hooks/useGlassButtonStyles.ts
@@ -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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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
+ };
+};
\ No newline at end of file
diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts
index 3d9c920..af60ac8 100644
--- a/src/locales/zh-CN/menu.ts
+++ b/src/locales/zh-CN/menu.ts
@@ -25,6 +25,12 @@ export default {
'menu.other.machine-id-authorization': '机器码授权',
'menu.other.data-info': '数据信息',
+ 'menu.mjpackage': '生图包管理',
+ 'menu.mjpackage.token-management': 'Token管理',
+ 'menu.mjpackage.task-management': '任务管理',
+
+
+
'menu.more-blocks': '更多区块',
'menu.home': '首页',
'menu.admin': '管理页',
diff --git a/src/manifest.json b/src/manifest.json
index 839bc5b..4ab20c7 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -1,6 +1,6 @@
{
- "name": "Ant Design Pro",
- "short_name": "Ant Design Pro",
+ "name": "LaiTool Management System",
+ "short_name": "LaiTool Management System",
"display": "standalone",
"start_url": "./?utm_source=homescreen",
"theme_color": "#002140",
diff --git a/src/pages/MJPackage/MJPriceInfo.tsx b/src/pages/MJPackage/MJPriceInfo.tsx
new file mode 100644
index 0000000..4c560f1
--- /dev/null
+++ b/src/pages/MJPackage/MJPriceInfo.tsx
@@ -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 (
+
+ {
+ 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 && (
+
+ 推荐
+
+ )}
+
+
+ {/* 套餐名称 */}
+
+
+ {pkg.name}
+
+
+
+ {/* 价格 */}
+
+
+
+ {pkg.currency}
+
+
+ {pkg.price === 0 ? '免费' : pkg.price}
+
+ {pkg.price > 0 && (
+
+ /{pkg.period}
+
+ )}
+
+
+
+ {/* 核心信息 */}
+
+
+
+
+ 每日额度
+
+ {pkg.dailyQuota === -1 ? '∞' : pkg.dailyQuota}
+
+
+
+
+
+ 并发数
+
+ {pkg.concurrent}
+
+
+
+
+
+ 有效天数
+
+ {pkg.validDays}
+
+
+
+
+
+ 单图价格
+
+ {pkg.unitPrice}
+
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+ {/* 添加 CSS 样式 */}
+
+
+ {messageHolder}
+
+
+ {/* 页面标题 */}
+
+
+ 选择您的套餐
+
+
+ 简单透明的价格,强大的AI绘图服务
+
+
+
+ {/* 套餐网格 */}
+
+
+ {packages.map(pkg => (
+
+ {renderPackageCard(pkg)}
+
+ ))}
+
+
+
+ {/* 底部说明 */}
+
+
+ 需要帮助?
+
+
+ 我们的团队随时为您提供支持
+
+
+
+ }
+ onClick={showContactModal}
+ style={{ borderRadius: '8px' }}
+ >
+ 联系客服
+
+
+
+
+
+ {/* 修复 Footer 样式 */}
+
+
+
+ {/* 联系客服模态框 */}
+
+
+ 联系客服
+
+ }
+ open={isContactModalVisible}
+ onCancel={handleContactModalClose}
+ footer={null}
+ width={600}
+ centered
+ closeIcon={}
+ styles={{
+ body: {
+ padding: '24px',
+ textAlign: 'center'
+ }
+ }}
+ >
+
+
+ 扫描下方二维码,添加客服微信获取专业帮助
+
+
+
+ {/* 客服二维码图片 */}
+
+
+ 加载中...
+
+ }
+ fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3Ik1RnG4W+FgYxNLuVFV+D2CgzY+RKOHZBdO/YSjJ1gO3YMhgQGBiSwCxshOzAEtjOArtTlFQjdF77Y0/I"
+ />
+
+
+ {/* 其他联系方式 */}
+ {/*
+
+
+ 📱 客服热线:400-123-4567
+
+
+
+
+ ⏰ 服务时间:9:00-18:00 (周一至周五)
+
+
+
+
+ 📧 邮箱:support@example.com
+
+
+
*/}
+
+ {/* 底部按钮 */}
+ {/*
+
+
+
+
+
*/}
+
+
+ );
+};
+
+export default MJPriceInfo;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TaskManagement.tsx b/src/pages/MJPackage/TaskManagement.tsx
new file mode 100644
index 0000000..16d9e55
--- /dev/null
+++ b/src/pages/MJPackage/TaskManagement.tsx
@@ -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>([]);
+ const [simpleData, setSimpleData] = useState>>();
+ const [form] = Form.useForm();
+
+ // const [taskStats, setTaskStats] = useState();
+ const { getButtonStyle } = useGlassButtonStyles();
+
+ const [tableParams, setTableParams] = useState({
+ 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({} as MJP.MJApiTasks);
+ const [modalWidth, setModalWidth] = useState(800);
+
+ const [taskStatisticsData, setTaskStatisticsData] = useState();
+
+
+ // 统计数据
+ 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 = [
+ {
+ title: '任务ID',
+ dataIndex: 'taskId',
+ key: 'taskId',
+ width: 120,
+ fixed: 'left',
+ render: (text: string) => (
+
+
+
+ {text}
+
+
+
+ )
+ },
+ {
+ title: 'Token ID',
+ dataIndex: 'tokenId',
+ key: 'tokenId',
+ width: 80,
+ render: (tokenId: number) => (
+
+ {tokenId}
+
+ )
+ },
+ {
+ 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 ?
+ {record.propertieJson.botType} : "-"
+ },
+ },
+ {
+ 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 ? (
+
+
+ {record.propertieJson.prompt}
+
+
+ ) : (
+ 暂无提示词
+ )
+ }
+ },
+ {
+ title: '进度',
+ dataIndex: 'progress',
+ key: 'progress',
+ width: 100,
+ render: (status: string, record: MJP.MJApiTasks) => {
+ return record.propertieJson && record.propertieJson.progress ?
+ {record.propertieJson?.progress} : "-"
+ },
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'startTime',
+ key: 'startTime',
+ width: 150,
+ render: (time: Date) => (
+
+ {FormatDate(time)}
+
+ ),
+ },
+ {
+ title: '完成时间',
+ dataIndex: 'endTime',
+ key: 'endTime',
+ width: 150,
+ render: (time: Date | null) => (
+
+ {time ? FormatDate(time) : '-'}
+
+ )
+ },
+ {
+ title: '耗时',
+ key: 'duration',
+ width: 80,
+ render: (_, record: MJP.MJApiTasks) => {
+ if (!record.endTime || !record.startTime) {
+ return -;
+ }
+ 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 {minutes}分{seconds % 60}秒;
+ }
+ return {seconds}秒;
+ }
+ },
+ {
+ 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 -;
+ }
+ return (
+
+
+ {promptText}
+
+
+ );
+ }
+ },
+ {
+ title: '操作',
+ key: 'action',
+ fixed: 'right',
+ width: 150,
+ render: (_, record: MJP.MJApiTasks) => (
+
+
+
+
+ )
+ }
+ ];
+
+ // 查询任务数据的函数
+ async function QueryTaskBasic(params: QueryTaskParams | null, pagination: TablePaginationConfig | null): Promise {
+ 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;
+
+ 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 = {};
+ 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,
+ sorter: SorterResult | SorterResult[],
+ extra: TableCurrentDataSource
+ ): Promise {
+ 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 (
+
+
+ {messageHolder}
+ {modalHolder}
+
+ {/* 统计卡片 */}
+
+ {stats.map((stat, index) => (
+
+
+
+
+
+ {stat.title}
+
+
+ {stat.value}
+
+
+
+ {stat.iconText}
+
+
+
+
+ ))}
+
+
+ {/* 主表格卡片 */}
+
+ 任务管理
+ 共 {simpleData?.total} 条记录
+
+ }
+ extra={
+
+
+
+ }
+ >
+ {/* 搜索表单区域 */}
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ style={{ borderRadius: '6px' }}
+ >
+ 搜索
+
+
+
+
+
+
+ {/* 数据表格 */}
+
+
+
+
+ {/* 查看任务详情的Modal */}
+
+ 📋
+ 任务详情
+ {currentViewTask && (
+
+ {currentViewTask.taskId}
+
+ )}
+
+ }
+ width={modalWidth}
+ open={viewModalVisible}
+ footer={null}
+ closable={true}
+ maskClosable={false}
+ closeIcon={}
+ onCancel={() => setViewModalVisible(false)}
+ styles={{
+ body: {
+ maxHeight: '75vh',
+ paddingRight: '16px',
+ overflowY: 'auto'
+ }
+ }}
+ >
+
+
+
+
+
+ );
+};
+
+export default TaskManagement;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TaskMessageInfo.css b/src/pages/MJPackage/TaskMessageInfo.css
new file mode 100644
index 0000000..abb782f
--- /dev/null
+++ b/src/pages/MJPackage/TaskMessageInfo.css
@@ -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;
+ }
+}
diff --git a/src/pages/MJPackage/TaskMessageInfo.tsx b/src/pages/MJPackage/TaskMessageInfo.tsx
new file mode 100644
index 0000000..d2f2afa
--- /dev/null
+++ b/src/pages/MJPackage/TaskMessageInfo.tsx
@@ -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 (
+
+
+
+
+
+ LaiTool MJ生图管理系统
+
+
+ AI 图像生成任务管理平台
+
+
+
+
+
+
+ 欢迎回来,{formatTokenDisplay(tokenCacheItem?.token, 10)}
+
+ } danger onClick={TokenLogout}>
+ 退出登录
+
+
+
+
+
+
+
+ {/* 信息卡片区域 */}
+
+
+
+
+ {/* 表格区域 */}
+
+
+
+
+
+
+
+
+ {messageHolder}
+
+ );
+};
+
+export default TaskMessageInfo;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TaskMessageInfo/InfoCards.tsx b/src/pages/MJPackage/TaskMessageInfo/InfoCards.tsx
new file mode 100644
index 0000000..1f3a5ce
--- /dev/null
+++ b/src/pages/MJPackage/TaskMessageInfo/InfoCards.tsx
@@ -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: ,
+ color: '#1890ff',
+ suffix: ''
+ },
+ {
+ title: '今日绘图限制',
+ value: `${tokenCacheItem?.dailyUsage} / ${tokenCacheItem?.dailyLimit}`,
+ icon: ,
+ color: '#52c41a',
+ suffix: ''
+ },
+ {
+ title: '并发限制',
+ value: `${tokenCacheItem?.currentlyExecuting} / ${tokenCacheItem?.concurrencyLimit}`,
+ icon: ,
+ color: '#faad14',
+ suffix: ''
+ },
+ {
+ title: 'Token 到期时间',
+ value: `${tokenCacheItem?.expiresAt ? FormatDate(tokenCacheItem?.expiresAt) : '无限制'}`,
+ icon: ,
+ color: '#ff4d4f',
+ suffix: '',
+ }
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+ 欢迎使用 MJ 绘图系统 - 请关注使用配额和到期时间
+
+
+
+
+ }
+ type="info"
+ closable
+ showIcon={false}
+ style={{
+ marginBottom: 16,
+ borderRadius: '10px',
+ border: '1px solid #e6f7ff'
+ }}
+ />
+
+
+
+
+ {cardData.map((item, index) => (
+
+
+
+
+ {item.icon}
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+export default InfoCards;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TaskMessageInfo/TaskTable.tsx b/src/pages/MJPackage/TaskMessageInfo/TaskTable.tsx
new file mode 100644
index 0000000..2f51331
--- /dev/null
+++ b/src/pages/MJPackage/TaskMessageInfo/TaskTable.tsx
@@ -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 (
+
+ {config.icon}
+ {config.text}
+
+ );
+};
+
+const TaskTable: React.FC = () => {
+ const [loading, setLoading] = useState(false);
+ const tableRef = useRef(null);
+
+ const [taskCollection, setTaskCollection] = useState>([]);
+
+ const [taskData, setTaskData] = useState();
+
+ const [messageApi, messageHolder] = message.useMessage();
+ const { setTokenCacheItem } = useMJPStore((state) => state);
+
+ const [tableParams, setTableParams] = useState({
+ pagination: {
+ current: 1,
+ pageSize: 10,
+ showQuickJumper: true,
+ totalBoundaryShowSizeChanger: true,
+ },
+ });
+
+ // 添加弹窗相关状态
+ const [taskDetailVisible, setTaskDetailVisible] = useState(false);
+ const [currentTaskData, setCurrentTaskData] = useState(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 = [
+ {
+ title: '任务ID',
+ dataIndex: 'taskId',
+ key: 'taskId',
+ width: 120,
+ fixed: 'left',
+ render: (text: string, record: MJP.MJApiTasks) => (
+
+ )
+ },
+ {
+ 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 暂无提示词;
+ }
+ return (
+
+
+ {promptText}
+
+
+ );
+ }
+ },
+ {
+ 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 ?
+ {record.propertieJson.botType} : "-"
+ },
+ },
+ {
+ 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 ?
+ {record.propertieJson?.progress} : "-"
+ },
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'startTime',
+ key: 'startTime',
+ width: 150,
+ render: (text: string, record: MJP.MJApiTasks) => (
+
+ {FormatDate(record.startTime)}
+
+ )
+ },
+ {
+ title: '完成时间',
+ dataIndex: 'completeTime',
+ key: 'completeTime',
+ width: 150,
+ render: (text: string, record: MJP.MJApiTasks) => (
+
+ {record.endTime ? FormatDate(record.endTime) : "-"}
+
+ )
+ },
+ {
+ 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 -;
+ }
+ return (
+
+
+ {promptText}
+
+
+ );
+ }
+ },
+ {
+ 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 (
+
+ {hasImage ? (
+
+
+ 加载中...
+
+ }
+ loading='lazy'
+ fallback='https://lms.laitool.cn/im/empty_image.png'
+ />
+
+ ) : (
+
+ 暂无图片
+
+ )}
+
+ )
+ }
+ }
+ ];
+
+ // 基础的查询任务
+ async function QueryTaskBasic(thirdPartyTaskId: string | undefined, pagination: TablePaginationConfig | null): Promise {
+ 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;
+ // 初始 任务数据
+ 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 = {};
+ 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, sorter: SorterResult | SorterResult[], extra: TableCurrentDataSource): Promise {
+ 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 (
+
+ 任务列表
+ 总计 {taskData?.total} 条记录
+
+ }
+ className="image-table-card"
+ extra={
+
+ }
+ size="middle"
+ style={{ width: 280 }}
+ onSearch={handleSearch}
+ />
+
+ }
+ >
+
+ {messageHolder}
+ {/* 任务详情弹窗 */}
+
+ 📋
+ 任务详情
+ {currentTaskData && (
+
+ {currentTaskData.taskId}
+
+ )}
+
+ }
+ width={modalWidth}
+ open={taskDetailVisible}
+ footer={null}
+ closable={true}
+ closeIcon={}
+ onCancel={() => {
+ setTaskDetailVisible(false);
+ setCurrentTaskData(null);
+ }}
+ styles={{
+ body: {
+ maxHeight: '75vh',
+ paddingRight: '16px',
+ overflowY: 'auto'
+ }
+ }}
+ destroyOnClose={true}
+ >
+ {currentTaskData && (
+
+ )}
+
+
+ );
+};
+
+export default TaskTable;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TokenLogin.css b/src/pages/MJPackage/TokenLogin.css
new file mode 100644
index 0000000..1b3a753
--- /dev/null
+++ b/src/pages/MJPackage/TokenLogin.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/pages/MJPackage/TokenLogin.tsx b/src/pages/MJPackage/TokenLogin.tsx
new file mode 100644
index 0000000..63adb73
--- /dev/null
+++ b/src/pages/MJPackage/TokenLogin.tsx
@@ -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 (
+
+
+
+ }
+ placeholder="请输入您的Token"
+ autoComplete="off"
+ />
+
+
+
+
+
+
+
+ {messageHolder}
+
+ );
+};
+
+export default TokenLogin;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TokenManagement.tsx b/src/pages/MJPackage/TokenManagement.tsx
new file mode 100644
index 0000000..2f72cdd
--- /dev/null
+++ b/src/pages/MJPackage/TokenManagement.tsx
@@ -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>([]);
+ const [simpleData, setSimpleData] = useState>>();
+ const [form] = Form.useForm(); // 添加 form 实例
+
+ const [healthAndCache, setHealthAndCache] = useState();
+
+ const [title, setTitle] = useState("新增Token");
+
+
+ const [modalVisible, setModalVisible] = useState(false);
+ const [tokenId, setTokenId] = useState(0);
+ const { setFormRef, resetForm } = useFormReset();
+
+ const { getButtonStyle } = useGlassButtonStyles();
+
+ const [tableParams, setTableParams] = useState({
+ 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(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 无时间限制;
+ }
+
+ 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 {text};
+ }
+
+ return 已到期;
+ };
+
+ // 表格列定义
+ const columns: ColumnsType = [
+ {
+ title: 'Token ID',
+ dataIndex: 'id',
+ key: 'id',
+ width: 80,
+ fixed: 'left'
+ },
+ {
+ title: 'Token',
+ dataIndex: 'token',
+ key: 'token',
+ width: 200,
+ render: (text: string) => (
+
+
+ {formatTokenDisplay(text, 20)}
+
+ }
+ onClick={() => {
+ navigator.clipboard.writeText(text);
+ messageApi.success('Token 已复制到剪贴板');
+ }}
+ style={{
+ padding: '0 4px',
+ height: '20px',
+ color: '#1890ff'
+ }}
+ />
+
+ )
+ },
+ {
+ title: '每日限制',
+ dataIndex: 'dailyLimit',
+ key: 'dailyLimit',
+ width: 100,
+ render: (_, record) => (
+
+
{record.dailyLimit > 0 ? record.dailyLimit : '不限制'}
+
+ 已用: {record.dailyUsage}
+
+
+ )
+ },
+ {
+ title: '总限制',
+ dataIndex: 'totalLimit',
+ key: 'totalLimit',
+ width: 100,
+ render: (_, record) => (
+
+
{record.totalLimit > 0 ? record.totalLimit : '不限制'}
+
+ 已用: {record.totalUsage}
+
+
+ )
+ },
+ {
+ title: '并发限制',
+ dataIndex: 'concurrencyLimit',
+ key: 'concurrencyLimit',
+ width: 100,
+ render: (_, record) => (
+
+
{record.concurrencyLimit}
+
+ 执行中: {record.currentlyExecuting}
+
+
+ )
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ width: 100,
+ render: (_, record) => getStatusTag(record.expiresAt),
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'createdAt',
+ key: 'createdAt',
+ width: 150,
+ render: (time: Date) => (
+
+ {FormatDate(time)}
+
+ ),
+ },
+ {
+ title: '过期时间',
+ dataIndex: 'expiresAt',
+ key: 'expiresAt',
+ width: 150,
+ render: (time: Date | null) => (
+
+ {time ? FormatDate(time) : '无时间限制'}
+
+ )
+ },
+ {
+ title: '最后活动',
+ dataIndex: 'lastActivityTime',
+ key: 'lastActivityTime',
+ width: 150,
+ render: (time: Date | null, record) => (
+
+
+ {time && time != record.createdAt ? FormatDate(time) : '-'}
+
+
+ )
+ },
+ {
+ title: '操作',
+ key: 'action',
+ fixed: 'right',
+ width: 170,
+ render: (_, record: MJP.TokenCacheItem) => (
+
+
+
+
+
+ )
+ }
+ ];
+
+ async function QueryTokenBasic(params: QueryTokenParams | null, pagination: TablePaginationConfig | null): Promise {
+ 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 = []
+
+ // 初始 任务数据
+ 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 = {};
+ 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, sorter: SorterResult | SorterResult[], extra: TableCurrentDataSource): Promise {
+ 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 {
+ 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 (
+
+
+ {messageHolder}
+ {modalHolder}
+
+
+ {
+ stats.map((stat, index) => (
+
+
+
+
+ {stat.title}
+
+
+ {stat.value}
+
+
+
+ {stat.iconText}
+
+
+
+ ))
+
+ }
+
+
+ {/* 主表格卡片 */}
+
+ Token 管理
+ 共 {simpleData?.total} 条记录
+
+ }
+ extra={
+ } onClick={handleAdd}>
+ 新增 Token
+
+ }
+ >
+ {/* 搜索表单区域 */}
+
+
+
+
+
+
+
+
+ }
+ style={{ borderRadius: '6px' }}
+ >
+ 搜索
+
+
+
+
+
+
+ {/* 数据表格 */}
+
+
+
+
+
+ {
+ tokenId == 0 ? :
+
+ }
+
+
+
+
+ 🔍
+ Token 使用记录
+
+ }
+ width={modalWidth}
+ open={viewModalVisible}
+ footer={null}
+ closable={true}
+ closeIcon={}
+ onCancel={() => setViewModalVisible(false)}
+ styles={{
+ body: {
+ maxHeight: '75vh',
+ paddingRight: '16px',
+ overflowY: 'auto'
+ }, content: {
+ paddingRight: 0
+ }
+ }}
+ >
+ {currentViewToken && (
+ {
+ navigator.clipboard.writeText(token);
+ messageApi.success('Token 已复制到剪贴板');
+ }}
+ />
+ )}
+
+
+
+ );
+};
+
+export default TokenManagement;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TokenManagement/AddToken.tsx b/src/pages/MJPackage/TokenManagement/AddToken.tsx
new file mode 100644
index 0000000..cf32f7f
--- /dev/null
+++ b/src/pages/MJPackage/TokenManagement/AddToken.tsx
@@ -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 = ({ setFormRef }) => {
+ const [form] = Form.useForm();
+ 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 (
+
+
+ 生成
+
+ }
+ />
+
+
+
+
+ 标准化
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {messageHolder}
+
+ );
+};
+
+export default AddToken;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TokenManagement/ModifyToken.tsx b/src/pages/MJPackage/TokenManagement/ModifyToken.tsx
new file mode 100644
index 0000000..75d989f
--- /dev/null
+++ b/src/pages/MJPackage/TokenManagement/ModifyToken.tsx
@@ -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 = ({ setFormRef, tokenId }) => {
+ const [form] = Form.useForm();
+ 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 (
+
+
+
+
+
+
+
+ 标准化
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {messageHolder}
+
+
+ );
+};
+
+export default ModifyToken;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TokenManagement/TaskInfo.tsx b/src/pages/MJPackage/TokenManagement/TaskInfo.tsx
new file mode 100644
index 0000000..d5307da
--- /dev/null
+++ b/src/pages/MJPackage/TokenManagement/TaskInfo.tsx
@@ -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 = ({ taskData, isAdmin }) => {
+ const containerRef = useRef(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 (
+
+ {/* 任务基本信息 */}
+
+
+ 📋 任务基本信息
+
+
+
+
+ messageApi.success('任务ID已复制')
+ }}
+ style={{
+ margin: 0,
+ borderRadius: '4px',
+ fontSize: containerWidth < 500 ? '12px' : '14px',
+ wordBreak: 'break-word'
+ }}
+ >
+ {taskData.taskId || '无'}
+
+
+
+
+ messageApi.success('第三方任务ID已复制')
+ }}
+ style={{
+ margin: 0,
+ borderRadius: '4px',
+ fontSize: containerWidth < 500 ? '12px' : '14px',
+ wordBreak: 'break-word'
+ }}
+ >
+ {taskData.thirdPartyTaskId || '无'}
+
+
+
+ {isAdmin ?
+ {taskData.tokenId}
+ : null}
+
+
+ {getStatusTag(taskData.status || '')}
+
+
+
+ {properties.action || '-'}
+
+
+
+ {properties.progress || "-"}
+
+
+
+ {FormatDate(taskData.startTime)}
+
+
+
+ {taskData.endTime ? FormatDate(taskData.endTime) : '-'}
+
+
+
+ {getDuration()}
+
+
+
+
+ {/* 提示词信息 */}
+
+
+ 💭 提示词信息
+
+
+
+
+
+ 原始提示词:
+
+ messageApi.success('原始提示词已复制')
+ }}
+ style={{
+ margin: 0,
+ padding: '8px',
+ backgroundColor: '#f5f5f5',
+ borderRadius: '4px',
+ fontSize: containerWidth < 500 ? '12px' : '14px',
+ wordBreak: 'break-word'
+ }}
+ >
+ {properties.prompt || '无'}
+
+
+
+
+
+ 英文提示词:
+
+ messageApi.success('英文提示词已复制')
+ }}
+ style={{
+ margin: 0,
+ padding: '8px',
+ backgroundColor: '#f5f5f5',
+ borderRadius: '4px',
+ fontSize: containerWidth < 500 ? '12px' : '14px',
+ wordBreak: 'break-word'
+ }}
+ >
+ {properties.promptEn || '无'}
+
+
+
+ {properties.properties?.finalPrompt && (
+
+
+ 最终提示词:
+
+ messageApi.success('最终提示词已复制')
+ }}
+ style={{
+ margin: 0,
+ padding: '8px',
+ backgroundColor: '#f0f9ff',
+ borderRadius: '4px',
+ fontSize: containerWidth < 500 ? '12px' : '14px',
+ wordBreak: 'break-word'
+ }}
+ >
+ {properties.properties.finalPrompt || '无'}
+
+
+ )}
+
+
+
+ {/* 生成结果 */}
+ {properties.imageUrl && (
+
+
+ 🖼️ 生成结果
+
+
+
+
+
+ }
+ onClick={() => window.open(properties.imageUrl, '_blank')}
+ title="在新窗口打开"
+ />
+
+ }
+ >
+
+
,
+ maskClassName: 'custom-mask'
+ }}
+ />
+
+ 尺寸: {properties.imageWidth} × {properties.imageHeight}
+
+
+ {/* 添加图片地址复制功能 */}
+
+
图片地址:
+
messageApi.success('图片地址已复制')
+ }}
+ style={{
+ margin: 0,
+ padding: '4px',
+ backgroundColor: '#f5f5f5',
+ borderRadius: '4px',
+ fontSize: '10px',
+ wordBreak: 'break-all',
+ marginTop: '4px'
+ }}
+ >
+ {formatTokenDisplay(properties.imageUrl, 30)}
+
+
+
+
+
+
+
+
+
+
+
+
提交时间
+
+ {properties.submitTime ? FormatDate(new Date(properties.submitTime)) : '-'}
+
+
+
+
+
+
+
结束时间
+
+ {properties.finishTime ? FormatDate(new Date(properties.submitTime)) : '-'}
+
+
+
+
+
+
+
机器人类型
+
+ {properties.botType || '-'}
+
+
+
+
+
+
+
Discord实例
+
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 : '-'}
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* 只保留失败原因 */}
+ {properties.failReason && (
+
+
+ ❌ 失败原因
+
+
+
+
+
+ !
+
+
+
+
+ 任务执行失败,详细信息如下:
+
+
+
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}
+
+
+
+ 💡 提示:如果问题持续存在,请检查提示词格式或联系技术支持
+
+
+
+
+
+ )}
+
+ {messageHolder}
+
+ );
+};
+
+export default TaskInfo;
\ No newline at end of file
diff --git a/src/pages/MJPackage/TokenManagement/TokenInfo.tsx b/src/pages/MJPackage/TokenManagement/TokenInfo.tsx
new file mode 100644
index 0000000..901cb40
--- /dev/null
+++ b/src/pages/MJPackage/TokenManagement/TokenInfo.tsx
@@ -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 = ({ tokenData, onCopyToken }) => {
+ const containerRef = useRef(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 无时间限制;
+ }
+
+ 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 {text};
+ }
+
+ return 已到期;
+ };
+
+ // 添加历史记录的类型定义
+ 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 => {
+ const baseColumns: ColumnsType = [
+ {
+ title: '序号',
+ key: 'index',
+ width: containerWidth < 500 ? 35 : containerWidth < 700 ? 40 : 50,
+ align: 'center',
+ render: (_, __, index) => (
+
+ {index + 1}
+
+ ),
+ },
+ {
+ 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 (
+
+
+ {FormatDate(date, true)}
+
+ {dateLabel && containerWidth >= 500 && (
+
+ {dateLabel}
+
+ )}
+
+ );
+ },
+ 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 (
+
+ {usage}
+
+ );
+ },
+ 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) => (
+
+ {usage >= 1000 ? `${(usage / 1000).toFixed(1)}k` : usage}
+
+ ),
+ 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 (
+
+
+ {FormatDate(date)}
+
+
+
+ );
+ },
+ }
+ ];
+ 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: (
+
+
📋 复制失败,请手动复制以下信息:
+
+ {dateString}
+
+
+ ),
+ duration: 10
+ });
+
+ });
+ }
+
+ return (
+
+ {/* Token 基本信息 */}
+
+
+ 📋 基本信息
+
+
+
+
+ {tokenData.id}
+
+
+
+
+ {formatTokenDisplay(tokenData.token, config.tokenDisplayLength)}
+
+ }
+ onClick={() => onCopyToken(tokenData.token)}
+ title="复制Token"
+ style={{ minWidth: 'auto', padding: '2px' }}
+ />
+
+
+
+
+
+ {formatTokenDisplay(tokenData.useToken, config.tokenDisplayLength)}
+
+ }
+ onClick={() => onCopyToken(tokenData.useToken)}
+ title="复制Token"
+ style={{ minWidth: 'auto', padding: '2px' }}
+ />
+
+
+
+ {getStatusTag(tokenData.expiresAt)}
+
+
+ {FormatDate(tokenData.createdAt)}
+
+
+
+ {tokenData.expiresAt ? FormatDate(tokenData.expiresAt) : '无时间限制'}
+
+
+
+
+ {tokenData.lastActivityTime ? FormatDate(tokenData.lastActivityTime) : '-'}
+
+
+
+
+
+
+
+
+ {/* 使用统计 */}
+
+
+ 📊 使用统计
+
+
+
+
+
+
+ {tokenData.dailyUsage || 0}
+
+
+ 每日使用 / {tokenData.dailyLimit > 0 ? tokenData.dailyLimit : '∞'}
+
+
+
+
+
+
+ {tokenData.totalUsage || 0}
+
+
+ 总使用量 / {tokenData.totalLimit > 0 ? tokenData.totalLimit : '∞'}
+
+
+
+
+
+
+ {tokenData.currentlyExecuting || 0}
+
+
+ 并发执行 / {tokenData.concurrencyLimit}
+
+
+
+
+
+
+ {/* 历史使用数据 */}
+ {
+ tokenData.historyUseJson && Array.isArray(tokenData.historyUseJson) && tokenData.historyUseJson.length > 0 ? (
+
+
+ 📈 历史使用记录
+
+
+ {/* 统计信息 */}
+
+
+
+
+
+ {tokenData.historyUseJson.length}
+
+
记录天数
+
+
+
+
+
+ {tokenData.historyUseJson.reduce((sum, record) => sum + record.DailyUsage, 0)}
+
+
总使用量
+
+
+
+
+
+ {(tokenData.historyUseJson.reduce((sum, record) => sum + record.DailyUsage, 0) / tokenData.historyUseJson.length).toFixed(1)}
+
+
平均每日
+
+
+
+
+
+ {Math.max(...tokenData.historyUseJson.map(record => record.DailyUsage))}
+
+
最高单日
+
+
+
+
+
+ {/* 历史记录表格 */}
+
+
+ ) : (
+
+ 暂无历史使用记录
+
+ )}
+ {messageHolder}
+
+ );
+};
+
+export default TokenInfo;
\ No newline at end of file
diff --git a/src/pages/Machine/MachineManagement/index.tsx b/src/pages/Machine/MachineManagement/index.tsx
index e7df1c9..bd25ab9 100644
--- a/src/pages/Machine/MachineManagement/index.tsx
+++ b/src/pages/Machine/MachineManagement/index.tsx
@@ -4,13 +4,12 @@ import TemplateContainer from "@/pages/TemplateContainer";
import { DeactivationMachine, MachinePermanent, QueryMachineList } from "@/services/services/machine";
import { FormatDate } from "@/util/time";
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 { FilterValue, SorterResult, TableCurrentDataSource } from "antd/es/table/interface";
-import { delay, set } from "lodash";
import { useEffect, useState } from "react";
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";
const MachineManagement: React.FC = () => {
diff --git a/src/pages/Options/LaitoolOptions/BasicOptions/SimpleOptions/index.tsx b/src/pages/Options/LaitoolOptions/BasicOptions/SimpleOptions/index.tsx
index 57701b5..5298ba0 100644
--- a/src/pages/Options/LaitoolOptions/BasicOptions/SimpleOptions/index.tsx
+++ b/src/pages/Options/LaitoolOptions/BasicOptions/SimpleOptions/index.tsx
@@ -1,5 +1,6 @@
import { AllOptionKeyName } from '@/services/enum/optionEnum';
import { GetOptions, getOptionsStringValue, SaveOptions } from '@/services/services/options/optionsTool';
+import { OptionModel } from '@/services/typing/options/option';
import { useOptionsStore } from '@/store/options';
import { useSoftStore } from '@/store/software';
import { Button, Card, Form, Input } from 'antd';
diff --git a/src/pages/Options/LaitoolOptions/DubSetting/DubSettingTTsOptions/index.tsx b/src/pages/Options/LaitoolOptions/DubSetting/DubSettingTTsOptions/index.tsx
index 73323c6..d4976ac 100644
--- a/src/pages/Options/LaitoolOptions/DubSetting/DubSettingTTsOptions/index.tsx
+++ b/src/pages/Options/LaitoolOptions/DubSetting/DubSettingTTsOptions/index.tsx
@@ -1,5 +1,6 @@
import { AllOptionKeyName } from '@/services/enum/optionEnum';
import { GetOptions, getOptionsStringValue, SaveOptions } from '@/services/services/options/optionsTool';
+import { OptionModel } from '@/services/typing/options/option';
import { useOptionsStore } from '@/store/options';
import { useSoftStore } from '@/store/software';
import { Button, Card, Col, Form, Input, message, Row } from 'antd';
diff --git a/src/pages/Other/MachineIdAuthorization/AddMachineIdAuthorization.tsx b/src/pages/Other/MachineIdAuthorization/AddMachineIdAuthorization.tsx
index 807e731..363d817 100644
--- a/src/pages/Other/MachineIdAuthorization/AddMachineIdAuthorization.tsx
+++ b/src/pages/Other/MachineIdAuthorization/AddMachineIdAuthorization.tsx
@@ -1,12 +1,8 @@
import React, { useEffect, useState } from 'react';
-import { Form, Input, DatePicker, Select, Button, message, FormInstance, Space } from 'antd';
-import moment from 'moment';
-import { useNavigate } from 'react-router-dom';
+import { Form, Input, DatePicker, Select, Button, message, FormInstance } from 'antd';
import { GetMachineAuthorizationTypeOptions } from '@/services/enum/machineAuthorizationEnum';
-const { TextArea } = Input;
import CryptoJS from 'crypto-js'; // 添加这一行导入
import * as LZString from 'lz-string';
-import cusRequest from '@/request';
import { AddMachineIdAuthorizationFunc } from '@/services/services/other';
@@ -136,49 +132,6 @@ const AddMachineIdAuthorization: React.FC = ({ 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 (
diff --git a/src/pages/Other/MachineIdAuthorization/index.tsx b/src/pages/Other/MachineIdAuthorization/index.tsx
index abcb68c..bfd9ea4 100644
--- a/src/pages/Other/MachineIdAuthorization/index.tsx
+++ b/src/pages/Other/MachineIdAuthorization/index.tsx
@@ -4,12 +4,10 @@ import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
import TemplateContainer from '@/pages/TemplateContainer';
import { useModel } from '@umijs/max';
import { ColumnsType, FilterValue, SorterResult, TableCurrentDataSource, TablePaginationConfig } from 'antd/es/table/interface';
-import { objectToQueryString } from '@/services/services/common';
import { FormatDate } from '@/util/time';
import { GetMachineAuthorizationTypeOption, GetMachineAuthorizationTypeOptions } from '@/services/enum/machineAuthorizationEnum';
import { useFormReset } from '@/hooks/useFormReset';
import AddMachineIdAuthorization from './AddMachineIdAuthorization';
-import cusRequest from '@/request';
import { isEmpty } from 'lodash';
import ModifyMachineIdAuthorization from './ModifyMachineIdAuthorization';
import { BatchDeleteMachine, DeleteMachineAuthorization, QueryMachineAuthorization } from '@/services/services/other';
diff --git a/src/pages/User/UserManage/UserManagement/index.tsx b/src/pages/User/UserManage/UserManagement/index.tsx
index 55dadbe..05c50f5 100644
--- a/src/pages/User/UserManage/UserManagement/index.tsx
+++ b/src/pages/User/UserManage/UserManagement/index.tsx
@@ -4,16 +4,13 @@ import { QueryRoleOption } from "@/services/services/role";
import { UserInfo } from "@/services/services/user";
import { FormatDate } from "@/util/time";
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 { FilterValue, SorterResult, TableCurrentDataSource } from "antd/es/table/interface";
import { useEffect, useState } from "react";
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 { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon";
-import DiceIcon from "@/components/Icon/DiceIcon";
-import { generateRandomPassword } from "@/util/password";
import ResetUserPassword from "./ResetUserPassword";
import SofrwareControlManagement from "@/pages/Software/SofrwareControl/SofrwareControlManagement";
diff --git a/src/services/services/index.ts b/src/services/services/index.ts
index c45629b..0de3231 100644
--- a/src/services/services/index.ts
+++ b/src/services/services/index.ts
@@ -6,10 +6,12 @@ import * as api from './api';
import * as login from './login';
import * as role from './role';
import * as user from './user';
+import * as mjp from './mjp';
export default {
api,
login,
role,
- user
+ user,
+ mjp
};
diff --git a/src/services/services/mjp.ts b/src/services/services/mjp.ts
new file mode 100644
index 0000000..f279336
--- /dev/null
+++ b/src/services/services/mjp.ts
@@ -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 - 返回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 {
+ if (isEmpty(token)) {
+ throw new Error("Token不能为空");
+ }
+ // 开始调用请求token信息接口
+ const res = await cusRequest(`/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 - 返回查询任务数据的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 {
+ let data = {
+ thirdPartyTaskId,
+ page: tableParams.pagination?.current,
+ pageSize: tableParams.pagination?.pageSize,
+ }
+ let query = objectToQueryString(data)
+ let res = await cusRequest(`/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的数字ID,用于ID查询(可选)
+ * @returns Promise> - 返回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>(`/api/TokenManagement/QueryTokenCollection?${query}`, {
+ method: 'GET',
+ });
+ if (res.code != 1) {
+ throw new Error(res.message);
+ }
+ return res.data as BasicModel.QueryCollection;
+}
+
+/**
+ * 管理员获取系统健康状态和Token缓存统计信息
+ * @description 获取系统健康监控数据,包含Token缓存统计、系统状态、运行时间等信息,用于管理员监控系统运行状况
+ * @returns Promise - 返回系统健康状态和缓存统计信息的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(`/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 - 返回操作结果消息的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 {
+ // 验证Token参数不能为空
+ if (isEmpty(tokenParams.token)) {
+ throw new Error("Token不能为空");
+ }
+
+ // 发起POST请求添加新Token
+ let res = await cusRequest(`/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的唯一标识ID,必须大于0
+ * @returns Promise - 返回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 {
+ // 验证Token ID参数
+ if (!tokenId || tokenId <= 0) {
+ throw new Error("Token ID无效");
+ }
+
+ // 发起GET请求获取Token详情
+ let res = await cusRequest(`/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的唯一标识ID,用于定位要修改的Token
+ * @param tokenParams.token - Token字符串,不能为空
+ * @param tokenParams.dailyLimit - 每日使用限制,必须大于0
+ * @param tokenParams.totalLimit - 总使用限制,必须大于0
+ * @param tokenParams.concurrencyLimit - 并发请求限制,必须大于0
+ * @param tokenParams.useDayCount - 可使用天数,必须大于0
+ * @returns Promise - 返回操作结果消息的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 {
+ // 验证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(`/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> - 返回任务集合查询结果的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>(`/api/TokenManagement/QueryTaskCollection?${query}`, {
+ method: 'GET',
+ });
+ if (res.code != 1) {
+ throw new Error(res.message);
+ }
+ return res.data as BasicModel.QueryCollection;
+}
+
+/**
+ * 管理员获取当日任务统计信息
+ * @description 管理员权限下获取当日的任务统计数据,包含总任务数、完成数、失败数和进行中的任务数,用于仪表板数据展示和系统监控
+ * @returns Promise - 返回当日任务统计信息的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(`/api/TokenManagement/GetDayTaskStatistics`, {
+ method: 'GET',
+ });
+ if (res.code != 1) {
+ throw new Error(res.message);
+ }
+ return res.data as MJP.TaskStatistics;
+}
diff --git a/src/services/typing/access.d.ts b/src/services/typing/access.d.ts
index 32ee029..8f3ba99 100644
--- a/src/services/typing/access.d.ts
+++ b/src/services/typing/access.d.ts
@@ -3,7 +3,7 @@ declare namespace AccessType {
canPrompt: boolean;
canRoleManagement: boolean;
/** 是不是显示数据管理 */
- canOptionManagement : boolean;
+ canOptionManagement: boolean;
//#region 用户权限
/** 是不是显示用户管理的菜单 */
@@ -72,6 +72,14 @@ declare namespace AccessType {
canAddForeverSoftwareControl: boolean;
/** 是否可以删除软件控制权限 */
canDeleteSoftwareControl: boolean;
+
+ //#endregion
+
+ //#region 生图包权限
+
+ /** 是不是可以管理生图包 */
+ canManagementMJPackage: boolean;
+
//#endregion
}
}
\ No newline at end of file
diff --git a/src/services/typing/mjp.d.ts b/src/services/typing/mjp.d.ts
new file mode 100644
index 0000000..4526a7a
--- /dev/null
+++ b/src/services/typing/mjp.d.ts
@@ -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 | null;
+ }
+
+ /**
+ * Token详情信息接口
+ * @description 定义Token的完整信息结构,包含ID、Token字符串、各种限制和时间信息
+ */
+ 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 | null;
+
+ /** 占用并发 */
+ currentlyExecuting: number;
+ }
+
+ /**
+ * Token 和任务集合接口
+ */
+ interface TokenAndTaskCollection extends TokenCacheItem {
+
+ /** 任务集合 */
+ taskCollections: Array
+
+ }
+
+ /**
+ * 查询返回数据的集合
+ */
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/src/store/mjp.ts b/src/store/mjp.ts
new file mode 100644
index 0000000..ade4a0d
--- /dev/null
+++ b/src/store/mjp.ts
@@ -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((set, get) => ({
+ /** Token 缓存项信息 */
+ tokenCacheItem: null,
+
+ /** 设置 Token 缓存项 */
+ setTokenCacheItem: (tokenCacheItem: MJP.TokenCacheItem | null) => {
+ console.log('Store收到数据:', tokenCacheItem);
+ set({ tokenCacheItem: tokenCacheItem });
+ },
+
+
+}));
\ No newline at end of file
diff --git a/src/store/options.ts b/src/store/options.ts
index 7117334..74f0dbf 100644
--- a/src/store/options.ts
+++ b/src/store/options.ts
@@ -1,3 +1,4 @@
+import { OptionModel } from '@/services/typing/options/option';
import { create } from 'zustand';
export const useOptionsStore = create((set: (arg0: any) => any) => ({
diff --git a/src/util/text.ts b/src/util/text.ts
new file mode 100644
index 0000000..f287118
--- /dev/null
+++ b/src/util/text.ts
@@ -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);
+};
\ No newline at end of file
diff --git a/src/util/time.ts b/src/util/time.ts
index a0dcba6..93ce242 100644
--- a/src/util/time.ts
+++ b/src/util/time.ts
@@ -1,4 +1,4 @@
-export function FormatDate(date: Date): string {
+export function FormatDate(date: Date, onlyDay: boolean = false): string {
// 如果传入的是字符串,尝试将其转换为 Date 对象
if (typeof date === 'string') {
date = new Date(date);
@@ -6,6 +6,11 @@ export function FormatDate(date: Date): string {
if (!(date instanceof Date) || isNaN(date.getTime())) {
return '';
}
+ if (onlyDay) {
+ return date.getFullYear() + '-' +
+ pad(date.getMonth() + 1) + '-' +
+ pad(date.getDate());
+ }
return date.getFullYear() + '-' +
pad(date.getMonth() + 1) + '-' +
pad(date.getDate()) + ' ' +
@@ -19,3 +24,12 @@ function pad(number: number) {
return (number < 10 ? '0' : '') + number;
}
+
+/**
+ * 延时多少秒,返回一个Promise
+ * @param time 延时时间,单位毫秒
+ * @returns viod
+ */
+export async function TimeDelay(time: number): Promise {
+ return new Promise(resolve => setTimeout(resolve, time));
+}