v 1.1.2 生图包管理界面
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Form, Input, InputNumber, Button, message, FormInstance } from 'antd';
|
||||
import { adminAddToken } from '@/services/services/mjp';
|
||||
|
||||
interface AddTokenProps {
|
||||
setFormRef: (form: FormInstance) => void;
|
||||
}
|
||||
|
||||
export const defaultTokenValues: MJP.AddAndModifyTokenParams = {
|
||||
token: "",
|
||||
useToken: "",
|
||||
dailyLimit: 200,
|
||||
totalLimit: 6000,
|
||||
concurrencyLimit: 5,
|
||||
useDayCount: 30
|
||||
}
|
||||
|
||||
const AddToken: React.FC<AddTokenProps> = ({ setFormRef }) => {
|
||||
const [form] = Form.useForm<MJP.AddAndModifyTokenParams>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
|
||||
useEffect(() => {
|
||||
setFormRef(form);
|
||||
// 设置默认值
|
||||
form.setFieldsValue({ ...defaultTokenValues });
|
||||
}, [form, setFormRef]);
|
||||
|
||||
// 提交添加 Token
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
let res = await adminAddToken(form.getFieldsValue())
|
||||
messageApi.success(res);
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ ...defaultTokenValues });
|
||||
}
|
||||
|
||||
// 生成一个随机的 Token
|
||||
const generateToken = () => {
|
||||
const randomToken = crypto.randomUUID().replace(/-/g, '').substring(0, 32);
|
||||
form.setFieldsValue({ token: randomToken });
|
||||
messageApi.success('已生成新的 Token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Token
|
||||
*/
|
||||
const formatString = () => {
|
||||
let useToken = form.getFieldValue('useToken');
|
||||
if (useToken && useToken.startsWith('sk-')) {
|
||||
// 移除 前面三个字符
|
||||
useToken = useToken.substring(3);
|
||||
}
|
||||
form.setFieldsValue({ useToken });
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
name="token"
|
||||
label="用户Token"
|
||||
rules={[{ required: true, message: '请输入或生成 Token' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入 Token 或点击生成"
|
||||
addonAfter={
|
||||
<Button type="link" onClick={generateToken} style={{ padding: 0 }}>
|
||||
生成
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="useToken"
|
||||
label="使用的Token"
|
||||
rules={[{ required: true, message: '请输入或生成 实际Token' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入 Token 或点击生成"
|
||||
addonAfter={
|
||||
<Button type="link" onClick={formatString} style={{ padding: 0 }}>
|
||||
标准化
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="dailyLimit"
|
||||
label="每日限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入每日限制' },
|
||||
{ type: 'number', min: 1, message: '每日限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入每日使用限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="totalLimit"
|
||||
label="总限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入总限制' },
|
||||
{ type: 'number', min: 1, message: '总限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入总使用限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="concurrencyLimit"
|
||||
label="并发限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入并发限制' },
|
||||
{ type: 'number', min: 1, message: '并发限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入并发限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="useDayCount"
|
||||
label="使用天数"
|
||||
rules={[
|
||||
{ required: true, message: '请输入使用天数' },
|
||||
{ type: 'number', min: 1, message: '使用天数必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入可使用天数"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
addonAfter="天"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ textAlign: 'right', marginTop: 24 }}>
|
||||
<Button
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
保存
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
{messageHolder}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddToken;
|
||||
@@ -0,0 +1,234 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Form, Input, InputNumber, Button, message, FormInstance, Spin } from 'antd';
|
||||
import { adminGetTokenById, adminModifyToken } from '@/services/services/mjp';
|
||||
import { FormatDate } from '@/util/time';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
interface ModifyTokenProps {
|
||||
setFormRef: (form: FormInstance) => void;
|
||||
tokenId: number; // Token ID,用于编辑时获取数据
|
||||
}
|
||||
|
||||
const ModifyToken: React.FC<ModifyTokenProps> = ({ setFormRef, tokenId }) => {
|
||||
const [form] = Form.useForm<MJP.AddAndModifyTokenParams & MJP.MJAPITokens>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
|
||||
useEffect(() => {
|
||||
setFormRef(form);
|
||||
// 加载Token详情
|
||||
loadTokenDetail();
|
||||
}, [form, setFormRef, tokenId]);
|
||||
|
||||
// 加载Token详情数据
|
||||
const loadTokenDetail = async () => {
|
||||
if (!tokenId || tokenId <= 0) {
|
||||
messageApi.error('无效的Token ID');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoadingData(true);
|
||||
let res = await adminGetTokenById(tokenId);
|
||||
|
||||
form.setFieldsValue({
|
||||
...res,
|
||||
createdAt: res.createdAt ? FormatDate(res.createdAt) : '-',
|
||||
expiresAt: res.expiresAt ? FormatDate(res.expiresAt) : '-',
|
||||
useDayCount: -1 // 默认值为 -1,表示不修改
|
||||
|
||||
});
|
||||
messageApi.success('获取Token详情成功');
|
||||
} catch (error: any) {
|
||||
messageApi.error(`获取Token详情失败: ${error.message}`);
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交修改 Token
|
||||
const handleSubmit = async (values: MJP.AddAndModifyTokenParams) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (!tokenId || tokenId <= 0) {
|
||||
messageApi.error('无效的Token ID');
|
||||
return;
|
||||
}
|
||||
if (values.token == null || isEmpty(values.token)) {
|
||||
messageApi.error('Token 不能为空');
|
||||
return;
|
||||
}
|
||||
// 开始调用修改方法
|
||||
let res = await adminModifyToken(tokenId, values);
|
||||
messageApi.success(res);
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置表单信息
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
// 重置为初始加载的数据,而不是默认值
|
||||
loadTokenDetail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化Token格式
|
||||
*/
|
||||
const formatString = () => {
|
||||
let token = form.getFieldValue('token');
|
||||
if (token && token.startsWith('sk-')) {
|
||||
// 移除 前面三个字符
|
||||
token = token.substring(3);
|
||||
}
|
||||
form.setFieldsValue({ token });
|
||||
};
|
||||
|
||||
return (
|
||||
<Spin spinning={loadingData} tip="正在加载 Token 数据...">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
|
||||
<Form.Item
|
||||
name="token"
|
||||
label="Token"
|
||||
rules={[{ required: true, message: '请输入或生成 Token' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入 Token"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="useToken"
|
||||
label="实际TOKEN"
|
||||
rules={[{ required: true, message: '请输入或生成实际 Token' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入 Token"
|
||||
addonAfter={
|
||||
<Button type="link" onClick={formatString} style={{ padding: 0, height: 20 }}>
|
||||
标准化
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="dailyLimit"
|
||||
label="每日限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入每日限制' },
|
||||
{ type: 'number', min: 1, message: '每日限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入每日使用限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="totalLimit"
|
||||
label="总限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入总限制' },
|
||||
{ type: 'number', min: 1, message: '总限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入总使用限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="concurrencyLimit"
|
||||
label="并发限制"
|
||||
rules={[
|
||||
{ required: true, message: '请输入并发限制' },
|
||||
{ type: 'number', min: 1, message: '并发限制必须大于 0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入并发限制"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="createdAt"
|
||||
label="创建时间"
|
||||
>
|
||||
<Input
|
||||
placeholder="创建时间"
|
||||
readOnly
|
||||
disabled
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="expiresAt"
|
||||
label="停用时间"
|
||||
>
|
||||
<Input
|
||||
placeholder="停用时间"
|
||||
readOnly
|
||||
disabled
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="useDayCount"
|
||||
label="使用天数(-1 和 0 不修改)"
|
||||
rules={[
|
||||
{ required: true, message: '请输入使用天数' },
|
||||
{ type: 'number', min: -1, message: '使用天数必须大于 -1' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="请输入可使用天数"
|
||||
style={{ width: '100%' }}
|
||||
min={-1}
|
||||
addonAfter="天"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ textAlign: 'right', marginTop: 24 }}>
|
||||
<Button
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={handleReset}
|
||||
disabled={loadingData}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
disabled={loadingData}
|
||||
>
|
||||
保存修改
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
{messageHolder}
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModifyToken;
|
||||
@@ -0,0 +1,564 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { Row, Col, Tag, Button, Space, Descriptions, Typography, message, Image, Card, Divider } from 'antd';
|
||||
import { CopyOutlined, LinkOutlined, DownloadOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { formatTokenDisplay } from '@/util/text';
|
||||
import { FormatDate } from '@/util/time';
|
||||
import { useGlassButtonStyles } from '@/hooks/useGlassButtonStyles';
|
||||
import { getStatusTag } from '../TaskMessageInfo/TaskTable';
|
||||
|
||||
const { Text, Title, Paragraph } = Typography;
|
||||
|
||||
interface TaskDetailProps {
|
||||
taskData: MJP.MJApiTasks;
|
||||
isAdmin?: boolean
|
||||
}
|
||||
|
||||
|
||||
const TaskInfo: React.FC<TaskDetailProps> = ({ taskData, isAdmin }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerWidth, setContainerWidth] = useState(800);
|
||||
const { getButtonStyle } = useGlassButtonStyles();
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
|
||||
// 监听容器宽度变化
|
||||
useEffect(() => {
|
||||
const updateWidth = () => {
|
||||
if (containerRef.current) {
|
||||
const width = containerRef.current.offsetWidth;
|
||||
setContainerWidth(width);
|
||||
}
|
||||
};
|
||||
|
||||
updateWidth();
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { width } = entry.contentRect;
|
||||
setContainerWidth(width);
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', updateWidth);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', updateWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 响应式配置
|
||||
const getResponsiveConfig = () => {
|
||||
if (containerWidth < 500) {
|
||||
return {
|
||||
descriptionColumns: 1,
|
||||
statisticColumns: { xs: 24, sm: 24, md: 24, lg: 24, xl: 24 },
|
||||
summaryColumns: { xs: 24, sm: 24, md: 12, lg: 12, xl: 6 },
|
||||
tokenDisplayLength: 8
|
||||
};
|
||||
} else if (containerWidth < 700) {
|
||||
return {
|
||||
descriptionColumns: 2,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 12, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 24, sm: 12, md: 12, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 12
|
||||
};
|
||||
} else if (containerWidth < 900) {
|
||||
return {
|
||||
descriptionColumns: 2,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 12, sm: 12, md: 6, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 15
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
descriptionColumns: 3,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 12, sm: 6, md: 6, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 20
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getResponsiveConfig();
|
||||
|
||||
// 解析属性JSON
|
||||
const properties = taskData.propertieJson || {};
|
||||
|
||||
// 计算耗时
|
||||
const getDuration = () => {
|
||||
if (!taskData.endTime || !taskData.startTime) return '-';
|
||||
|
||||
const duration = new Date(taskData.endTime).getTime() - new Date(taskData.startTime).getTime();
|
||||
const seconds = Math.floor(duration / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes}分${seconds % 60}秒`;
|
||||
}
|
||||
return `${seconds}秒`;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* 任务基本信息 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#1890ff',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
📋 任务基本信息
|
||||
</Title>
|
||||
|
||||
<Descriptions
|
||||
column={config.descriptionColumns}
|
||||
size="small"
|
||||
labelStyle={{
|
||||
width: 'auto',
|
||||
minWidth: containerWidth < 500 ? '60px' : '80px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}
|
||||
contentStyle={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item label="任务ID">
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: taskData.taskId || '',
|
||||
onCopy: () => messageApi.success('任务ID已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{taskData.taskId || '无'}
|
||||
</Paragraph>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="第三方任务ID">
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: taskData.thirdPartyTaskId || '',
|
||||
onCopy: () => messageApi.success('第三方任务ID已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{taskData.thirdPartyTaskId || '无'}
|
||||
</Paragraph>
|
||||
</Descriptions.Item>
|
||||
|
||||
{isAdmin ? <Descriptions.Item label="Token ID">
|
||||
<Tag color="blue">{taskData.tokenId}</Tag>
|
||||
</Descriptions.Item> : null}
|
||||
|
||||
<Descriptions.Item label="状态">
|
||||
{getStatusTag(taskData.status || '')}
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="动作类型">
|
||||
<Tag color="purple">{properties.action || '-'}</Tag>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="进度">
|
||||
<Tag color="purple">{properties.progress || "-"}</Tag>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="开始时间">
|
||||
<span>{FormatDate(taskData.startTime)}</span>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="结束时间">
|
||||
<span>{taskData.endTime ? FormatDate(taskData.endTime) : '-'}</span>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="耗时">
|
||||
<span>{getDuration()}</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
{/* 提示词信息 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#52c41a',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
💭 提示词信息
|
||||
</Title>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Card size="small" style={{ marginBottom: '8px' }}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong style={{ fontSize: '12px', color: '#666' }}>原始提示词:</Text>
|
||||
</div>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.prompt || '',
|
||||
onCopy: () => messageApi.success('原始提示词已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '8px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{properties.prompt || '无'}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
|
||||
<Card size="small" style={{ marginBottom: '8px' }}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong style={{ fontSize: '12px', color: '#666' }}>英文提示词:</Text>
|
||||
</div>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.promptEn || '',
|
||||
onCopy: () => messageApi.success('英文提示词已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '8px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{properties.promptEn || '无'}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
|
||||
{properties.properties?.finalPrompt && (
|
||||
<Card size="small">
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong style={{ fontSize: '12px', color: '#666' }}>最终提示词:</Text>
|
||||
</div>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.properties.finalPrompt || '',
|
||||
onCopy: () => messageApi.success('最终提示词已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '8px',
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{properties.properties.finalPrompt || '无'}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 生成结果 */}
|
||||
{properties.imageUrl && (
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#faad14',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
🖼️ 生成结果
|
||||
</Title>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Card
|
||||
size="small"
|
||||
title="结果图片"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<LinkOutlined />}
|
||||
onClick={() => window.open(properties.imageUrl, '_blank')}
|
||||
title="在新窗口打开"
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={properties.imageUrl}
|
||||
alt="生成结果"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
preview={{
|
||||
mask: <EyeOutlined style={{ fontSize: '20px' }} />,
|
||||
maskClassName: 'custom-mask'
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
|
||||
尺寸: {properties.imageWidth} × {properties.imageHeight}
|
||||
</div>
|
||||
|
||||
{/* 添加图片地址复制功能 */}
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong style={{ fontSize: '11px', color: '#666' }}>图片地址:</Text>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.imageUrl || '',
|
||||
onCopy: () => messageApi.success('图片地址已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '4px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
fontSize: '10px',
|
||||
wordBreak: 'break-all',
|
||||
marginTop: '4px'
|
||||
}}
|
||||
>
|
||||
{formatTokenDisplay(properties.imageUrl, 30)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={16}>
|
||||
<Card size="small" title="技术详情">
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#666'
|
||||
}}>提交时间</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{properties.submitTime ? FormatDate(new Date(properties.submitTime)) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#666'
|
||||
}}>结束时间</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{properties.finishTime ? FormatDate(new Date(properties.submitTime)) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#666'
|
||||
}}>机器人类型</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{properties.botType || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#666'
|
||||
}}>Discord实例</div>
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.properties?.discordInstanceId || '',
|
||||
onCopy: () => messageApi.success('Discord实例ID已复制')
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '2px',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
wordBreak: 'break-word',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{properties.properties?.discordInstanceId ?
|
||||
properties.properties.discordInstanceId : '-'}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 只保留失败原因 */}
|
||||
{properties.failReason && (
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#ff4d4f',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
❌ 失败原因
|
||||
</Title>
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
borderColor: '#ffccc7',
|
||||
backgroundColor: '#fff2f0'
|
||||
}}
|
||||
styles={{
|
||||
body: { padding: '16px' }
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
backgroundColor: '#ff4d4f',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
<span style={{
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold'
|
||||
}}>!</span>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
marginBottom: '8px',
|
||||
fontSize: '13px',
|
||||
color: '#666',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
任务执行失败,详细信息如下:
|
||||
</div>
|
||||
|
||||
<Paragraph
|
||||
copyable={{
|
||||
text: properties.failReason || '',
|
||||
onCopy: () => messageApi.success('失败原因已复制'),
|
||||
tooltips: ['复制失败原因', '已复制']
|
||||
}}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '12px',
|
||||
backgroundColor: '#ffffff',
|
||||
border: '1px solid #ffccc7',
|
||||
borderRadius: '6px',
|
||||
fontSize: containerWidth < 500 ? '12px' : '13px',
|
||||
color: '#d4380d',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: '1.6',
|
||||
boxShadow: '0 2px 4px rgba(255, 77, 79, 0.1)'
|
||||
}}
|
||||
>
|
||||
{properties.failReason}
|
||||
</Paragraph>
|
||||
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#fff7e6',
|
||||
border: '1px solid #ffd591',
|
||||
borderRadius: '4px',
|
||||
fontSize: '11px',
|
||||
color: '#ad6800'
|
||||
}}>
|
||||
💡 <strong>提示:</strong>如果问题持续存在,请检查提示词格式或联系技术支持
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messageHolder}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskInfo;
|
||||
@@ -0,0 +1,696 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { Row, Col, Tag, Button, Space, Statistic, Descriptions, Typography, message } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { formatTokenDisplay } from '@/util/text';
|
||||
import { FormatDate } from '@/util/time';
|
||||
import Table, { ColumnsType } from 'antd/es/table';
|
||||
import { useGlassButtonStyles } from '@/hooks/useGlassButtonStyles';
|
||||
import { systemConfig } from '../../../../config/systemConfig';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
interface TokenDetailProps {
|
||||
tokenData: MJP.TokenCacheItem;
|
||||
onCopyToken: (token: string) => void;
|
||||
}
|
||||
|
||||
const TokenInfo: React.FC<TokenDetailProps> = ({ tokenData, onCopyToken }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerWidth, setContainerWidth] = useState(800);
|
||||
const { getButtonStyle } = useGlassButtonStyles();
|
||||
const [messageApi, messageHolder] = message.useMessage();
|
||||
|
||||
// 监听容器宽度变化
|
||||
useEffect(() => {
|
||||
const updateWidth = () => {
|
||||
if (containerRef.current) {
|
||||
const width = containerRef.current.offsetWidth;
|
||||
setContainerWidth(width);
|
||||
console.log('Container width updated:', width); // 调试用
|
||||
}
|
||||
};
|
||||
|
||||
// 初始设置
|
||||
updateWidth();
|
||||
|
||||
// 使用 ResizeObserver 监听容器大小变化
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { width } = entry.contentRect;
|
||||
setContainerWidth(width);
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
// 也监听窗口大小变化作为后备
|
||||
window.addEventListener('resize', updateWidth);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', updateWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 根据容器宽度动态计算响应式配置
|
||||
const getResponsiveConfig = () => {
|
||||
console.log('Current container width:', containerWidth); // 调试用
|
||||
|
||||
if (containerWidth < 500) {
|
||||
return {
|
||||
descriptionColumns: 1,
|
||||
statisticColumns: { xs: 24, sm: 24, md: 24, lg: 24, xl: 24 },
|
||||
summaryColumns: { xs: 24, sm: 24, md: 12, lg: 12, xl: 6 },
|
||||
tokenDisplayLength: 8,
|
||||
showSimplePagination: true
|
||||
};
|
||||
} else if (containerWidth < 700) {
|
||||
return {
|
||||
descriptionColumns: 2,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 12, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 24, sm: 12, md: 12, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 12,
|
||||
showSimplePagination: true
|
||||
};
|
||||
} else if (containerWidth < 900) {
|
||||
return {
|
||||
descriptionColumns: 2,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 12, sm: 12, md: 6, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 15,
|
||||
showSimplePagination: false
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
descriptionColumns: 3,
|
||||
statisticColumns: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8 },
|
||||
summaryColumns: { xs: 12, sm: 6, md: 6, lg: 6, xl: 6 },
|
||||
tokenDisplayLength: 20,
|
||||
showSimplePagination: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getResponsiveConfig();
|
||||
|
||||
// 状态标签
|
||||
const getStatusTag = (expiresAt: Date | null | undefined) => {
|
||||
if (!expiresAt) {
|
||||
return <Tag color="blue">无时间限制</Tag>;
|
||||
}
|
||||
|
||||
const expireDate = new Date(expiresAt);
|
||||
const currentDate = new Date();
|
||||
const timeDiff = expireDate.getTime() - currentDate.getTime();
|
||||
|
||||
if (timeDiff > 0) {
|
||||
const daysLeft = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
|
||||
|
||||
let color = 'green';
|
||||
let text = '使用中';
|
||||
|
||||
if (daysLeft <= 1) {
|
||||
color = 'red';
|
||||
text = `今日过期`;
|
||||
} else if (daysLeft <= 3) {
|
||||
color = 'orange';
|
||||
text = `${daysLeft}天后过期`;
|
||||
} else if (daysLeft <= 7) {
|
||||
color = 'yellow';
|
||||
text = `${daysLeft}天后过期`;
|
||||
} else if (daysLeft <= 30) {
|
||||
text = `${daysLeft}天后过期`;
|
||||
}
|
||||
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
}
|
||||
|
||||
return <Tag color="red">已到期</Tag>;
|
||||
};
|
||||
|
||||
// 添加历史记录的类型定义
|
||||
interface HistoryRecord {
|
||||
TokenId: number;
|
||||
Date: string;
|
||||
DailyUsage: number;
|
||||
TotalUsage: number;
|
||||
LastActivityAt: string;
|
||||
HistoryUse: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
// 处理历史数据的函数
|
||||
const processHistoryData = (historyUseJson: HistoryRecord[]): HistoryRecord[] => {
|
||||
if (!Array.isArray(historyUseJson)) {
|
||||
return [];
|
||||
}
|
||||
console.log(historyUseJson)
|
||||
|
||||
return historyUseJson
|
||||
.map((record, index) => ({
|
||||
...record,
|
||||
key: `${record.TokenId}_${record.Date}_${index}`
|
||||
}))
|
||||
.sort((a, b) => new Date(b.Date).getTime() - new Date(a.Date).getTime());
|
||||
};
|
||||
|
||||
// 根据容器宽度动态调整表格列
|
||||
const getTableColumns = (): ColumnsType<HistoryRecord> => {
|
||||
const baseColumns: ColumnsType<HistoryRecord> = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: containerWidth < 500 ? 35 : containerWidth < 700 ? 40 : 50,
|
||||
align: 'center',
|
||||
render: (_, __, index) => (
|
||||
<span style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#8c8c8c',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
{index + 1}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '日期',
|
||||
dataIndex: 'Date',
|
||||
key: 'Date',
|
||||
width: containerWidth < 500 ? 70 : containerWidth < 700 ? 80 : 100,
|
||||
render: (date: Date) => {
|
||||
const dateObj = new Date(date);
|
||||
const today = new Date();
|
||||
const diffTime = today.getTime() - dateObj.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) - 1;
|
||||
|
||||
let dateLabel = '';
|
||||
let labelColor = '#bfbfbf';
|
||||
|
||||
if (diffDays === 1) {
|
||||
dateLabel = '昨天';
|
||||
labelColor = '#73d13d';
|
||||
} else if (diffDays === 0) {
|
||||
dateLabel = '今天';
|
||||
labelColor = '#40a9ff';
|
||||
} else if (diffDays <= 7) {
|
||||
dateLabel = `${diffDays}天前`;
|
||||
labelColor = '#ffa940';
|
||||
} else {
|
||||
dateLabel = `${diffDays}天前`;
|
||||
labelColor = '#ffa940';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#262626',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
{FormatDate(date, true)}
|
||||
</div>
|
||||
{dateLabel && containerWidth >= 500 && (
|
||||
<div style={{
|
||||
fontSize: '9px',
|
||||
color: labelColor,
|
||||
marginTop: '1px'
|
||||
}}>
|
||||
{dateLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sorter: (a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime(),
|
||||
},
|
||||
{
|
||||
title: '当日',
|
||||
dataIndex: 'DailyUsage',
|
||||
key: 'DailyUsage',
|
||||
width: containerWidth < 500 ? 50 : containerWidth < 700 ? 60 : 80,
|
||||
align: 'center',
|
||||
render: (usage: number) => {
|
||||
return (
|
||||
<span style={{
|
||||
fontWeight: '600',
|
||||
color: '#1677ff',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}>
|
||||
{usage}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
sorter: (a, b) => a.DailyUsage - b.DailyUsage,
|
||||
},
|
||||
{
|
||||
title: '累计',
|
||||
dataIndex: 'TotalUsage',
|
||||
key: 'TotalUsage',
|
||||
width: containerWidth < 500 ? 50 : containerWidth < 700 ? 60 : 80,
|
||||
align: 'center',
|
||||
render: (usage: number) => (
|
||||
<span style={{
|
||||
fontWeight: '600',
|
||||
color: '#1677ff',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}>
|
||||
{usage >= 1000 ? `${(usage / 1000).toFixed(1)}k` : usage}
|
||||
</span>
|
||||
),
|
||||
sorter: (a, b) => a.TotalUsage - b.TotalUsage,
|
||||
}, {
|
||||
title: '最后活跃时间',
|
||||
dataIndex: 'LastActivityAt',
|
||||
key: 'LastActivityAt',
|
||||
width: containerWidth < 500 ? 50 : containerWidth < 700 ? 60 : 80,
|
||||
align: 'center',
|
||||
render: (date: Date) => {
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
color: '#262626',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
{FormatDate(date)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
];
|
||||
return baseColumns;
|
||||
};
|
||||
|
||||
// 复制使用信息
|
||||
function copyTaskTokenToUser(token: MJP.TokenCacheItem) {
|
||||
let dateString =
|
||||
`Token: ${token.token}
|
||||
创建时间: ${FormatDate(token.createdAt)}
|
||||
过期时间: ${token.expiresAt ? FormatDate(token.expiresAt) : '无时间限制'}
|
||||
每日使用限制: ${token.dailyLimit > 0 ? token.dailyLimit : '无限制'}
|
||||
总使用限制: ${token.totalLimit > 0 ? token.totalLimit : '无限制'}
|
||||
并发限制: ${token.concurrencyLimit > 0 ? token.concurrencyLimit : '无限制'}
|
||||
LaiTool设置文档:${systemConfig.mjPackage.laitoolDoc}
|
||||
API调用使用文档:${systemConfig.mjPackage.doc}
|
||||
查询网址:https://lms.laitool.cn/mjp/task
|
||||
⚠️ 重要提示:Token 为敏感凭证,请妥善保管,避免泄露。如因保管不当造成损失,后果自负。
|
||||
`
|
||||
// 写入到剪贴板
|
||||
navigator.clipboard.writeText(dateString).then(() => {
|
||||
messageApi.success('Token 信息已复制到剪贴板!', 3);
|
||||
}).catch(err => {
|
||||
// 复制失败 ,弹出上面的文本 ,自行复制
|
||||
// 显示复制失败的提示,并提供手动复制选项
|
||||
messageApi.error({
|
||||
content: (
|
||||
<div>
|
||||
<div style={{ marginBottom: '8px' }}>📋 复制失败,请手动复制以下信息:</div>
|
||||
<div style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{dateString}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
duration: 10
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* Token 基本信息 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#1890ff',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
📋 基本信息
|
||||
</Title>
|
||||
|
||||
<Descriptions
|
||||
column={config.descriptionColumns}
|
||||
size="small"
|
||||
labelStyle={{
|
||||
width: 'auto',
|
||||
minWidth: containerWidth < 500 ? '60px' : '80px',
|
||||
fontSize: containerWidth < 500 ? '11px' : '12px'
|
||||
}}
|
||||
contentStyle={{
|
||||
fontSize: containerWidth < 500 ? '11px' : '12px'
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item label="Token ID">
|
||||
<Text strong style={{ fontSize: containerWidth < 500 ? '11px' : '12px' }}>{tokenData.id}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Token">
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
maxWidth: containerWidth < 500 ? '150px' : '200px'
|
||||
}}>
|
||||
<Text
|
||||
code
|
||||
style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
fontFamily: 'monospace',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{formatTokenDisplay(tokenData.token, config.tokenDisplayLength)}
|
||||
</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined style={{ fontSize: '12px' }} />}
|
||||
onClick={() => onCopyToken(tokenData.token)}
|
||||
title="复制Token"
|
||||
style={{ minWidth: 'auto', padding: '2px' }}
|
||||
/>
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="UseToken">
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
maxWidth: containerWidth < 500 ? '150px' : '200px'
|
||||
}}>
|
||||
<Text
|
||||
code
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
fontFamily: 'monospace',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{formatTokenDisplay(tokenData.useToken, config.tokenDisplayLength)}
|
||||
</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined style={{ fontSize: '12px' }} />}
|
||||
onClick={() => onCopyToken(tokenData.useToken)}
|
||||
title="复制Token"
|
||||
style={{ minWidth: 'auto', padding: '2px' }}
|
||||
/>
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{getStatusTag(tokenData.expiresAt)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
<span style={{ fontSize: containerWidth < 500 ? '11px' : '12px' }}>{FormatDate(tokenData.createdAt)}</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="过期时间">
|
||||
<span style={{ fontSize: containerWidth < 500 ? '11px' : '12px' }}>
|
||||
{tokenData.expiresAt ? FormatDate(tokenData.expiresAt) : '无时间限制'}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后活动">
|
||||
<span style={{ fontSize: containerWidth < 500 ? '11px' : '12px' }}>
|
||||
{tokenData.lastActivityTime ? FormatDate(tokenData.lastActivityTime) : '-'}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item >
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => copyTaskTokenToUser(tokenData)}
|
||||
style={{ ...getButtonStyle('primary').getStyle() }}
|
||||
onMouseEnter={(e) => getButtonStyle('primary').getMouseEnterStyle(e)}
|
||||
onMouseLeave={(e) => getButtonStyle('primary').getMouseLeaveStyle(e)}
|
||||
>复制Token信息-给用户</Button>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
{/* 使用统计 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={5} style={{
|
||||
color: '#52c41a',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
📊 使用统计
|
||||
</Title>
|
||||
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col {...config.statisticColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '8px 4px' : '12px 8px',
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderRadius: '8px',
|
||||
minHeight: containerWidth < 500 ? '60px' : '80px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '14px' : '16px',
|
||||
fontWeight: 'bold',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
{tokenData.dailyUsage || 0}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
color: '#666',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
每日使用 / {tokenData.dailyLimit > 0 ? tokenData.dailyLimit : '∞'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.statisticColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '8px 4px' : '12px 8px',
|
||||
backgroundColor: '#f6ffed',
|
||||
borderRadius: '8px',
|
||||
minHeight: containerWidth < 500 ? '60px' : '80px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '14px' : '16px',
|
||||
fontWeight: 'bold',
|
||||
color: '#52c41a'
|
||||
}}>
|
||||
{tokenData.totalUsage || 0}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
color: '#666',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
总使用量 / {tokenData.totalLimit > 0 ? tokenData.totalLimit : '∞'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.statisticColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '8px 4px' : '12px 8px',
|
||||
backgroundColor: '#fff2f0',
|
||||
borderRadius: '8px',
|
||||
minHeight: containerWidth < 500 ? '60px' : '80px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '14px' : '16px',
|
||||
fontWeight: 'bold',
|
||||
color: '#ff4d4f'
|
||||
}}>
|
||||
{tokenData.currentlyExecuting || 0}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '12px',
|
||||
color: '#666',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
并发执行 / {tokenData.concurrencyLimit}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 历史使用数据 */}
|
||||
{
|
||||
tokenData.historyUseJson && Array.isArray(tokenData.historyUseJson) && tokenData.historyUseJson.length > 0 ? (
|
||||
<div>
|
||||
<Title level={5} style={{
|
||||
color: '#faad14',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
paddingBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
📈 历史使用记录
|
||||
</Title>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{tokenData.historyUseJson.length}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#666'
|
||||
}}>记录天数</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{tokenData.historyUseJson.reduce((sum, record) => sum + record.DailyUsage, 0)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#666'
|
||||
}}>总使用量</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{(tokenData.historyUseJson.reduce((sum, record) => sum + record.DailyUsage, 0) / tokenData.historyUseJson.length).toFixed(1)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#666'
|
||||
}}>平均每日</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col {...config.summaryColumns}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: containerWidth < 500 ? '6px' : '8px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{Math.max(...tokenData.historyUseJson.map(record => record.DailyUsage))}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: containerWidth < 500 ? '10px' : '11px',
|
||||
color: '#666'
|
||||
}}>最高单日</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 历史记录表格 */}
|
||||
<div style={{
|
||||
width: '100%',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<Table
|
||||
columns={getTableColumns()}
|
||||
dataSource={processHistoryData(tokenData.historyUseJson)}
|
||||
size="small"
|
||||
scroll={{
|
||||
x: 'max-content',
|
||||
y: containerWidth < 500 ? 200 : 250
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: '8px',
|
||||
padding: containerWidth < 500 ? '6px' : '8px'
|
||||
}}
|
||||
pagination={false} // 完全禁用分页
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
backgroundColor: '#fff7e6',
|
||||
borderRadius: '8px',
|
||||
textAlign: 'center',
|
||||
color: '#d46b08',
|
||||
fontSize: containerWidth < 500 ? '12px' : '14px'
|
||||
}}>
|
||||
<Text strong>暂无历史使用记录</Text>
|
||||
</div>
|
||||
)}
|
||||
{messageHolder}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenInfo;
|
||||
Reference in New Issue
Block a user