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: () => (
- Copyright 2024 LaiTool Admins - 蜀ICP备2024079688号-1 +
), 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)} + + ))} + +
+ + {/* 底部说明 */} +
+ + 需要帮助? + + + 我们的团队随时为您提供支持 + + + + + +
+
+ + {/* 修复 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={ + + + + } + > + {/* 搜索表单区域 */} +
+ + + + + + + + + + + + + + + + +
+ + {/* 数据表格 */} + + + + + {/* 查看任务详情的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)} + + + +
+
+ + +
+ {/* 信息卡片区域 */} +
+ +
+ + {/* 表格区域 */} +
+ +
+
+
+ +
+ +
+
+ {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)} + +
+ ) + }, + { + 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={ + + } + > + {/* 搜索表单区域 */} +
+ + + + + + + + + + + + + + + {/* 数据表格 */} +
+ + + + + { + 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 && ( +
+ + 🖼️ 生成结果 + + + +
+ + + + + +
+
提交时间
+
+ {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)} + +
+
+ +
+ + {formatTokenDisplay(tokenData.useToken, config.tokenDisplayLength)} + +
+
+ + {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)); +}