ai创建积分计算,授权页面添加

This commit is contained in:
孙佳艺
2026-06-05 11:00:01 +08:00
parent 2ba50f6a7a
commit 8776c780a0
6 changed files with 817 additions and 417 deletions
+3
View File
@@ -12,6 +12,7 @@ import GenerateConver from './pages/GenerateConver';
import InitialReplication from './pages/InitialReplication';
import RemoveLens from './pages/RemoveLens';
import GeneratedRecord from './pages/GeneratedRecord';
import AuthorizationPage from './pages/AuthorizationPage';
@@ -93,6 +94,8 @@ const App = () => {
<Route path="initial" element={<InitialReplication />} />
<Route path="removelens" element={<RemoveLens />} />
<Route path="generated" element={<GeneratedRecord />} />
<Route path="authorization" element={<AuthorizationPage />} />
+7 -2
View File
@@ -342,7 +342,12 @@ export async function gethistoryItems(Pagebreak: any): Promise<any[]> {
return api.get('/generation-ai/history/'+Pagebreak);
}
// 删除ai对话历史记录
export async function deleteHistory(id: string): Promise<void> {
await api.delete(`/generation-ai/tasks/${id}`);
}
export async function calculateCredits(): Promise<any[]> {
return api.get('/credits/credit-ratios');
}
@@ -0,0 +1,166 @@
import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react';
import { Button, Table, Checkbox, Tag, Space, message } from 'antd';
import { PlusOutlined, CheckCircleOutlined, ClockCircleOutlined, CiCircleOutlined } from '@ant-design/icons';
// 模拟授权数据
const mockAuthorizations = [
{ id: '1867060028363785', status: 'active', description: '用户张三的API授权' },
{ id: '1867059757929740', status: 'pending', description: '用户李四的API授权' },
{ id: '1867059808785418', status: 'active', description: '用户王五的API授权' },
];
// 状态配置
const statusConfig = {
active: { label: '已授权', color: 'green', icon: CheckCircleOutlined },
pending: { label: '待授权', color: 'gold', icon: ClockCircleOutlined },
expired: { label: '已过期', color: 'red', icon: CiCircleOutlined },
revoked: { label: '已撤销', color: 'gray', icon: CiCircleOutlined },
};
const AuthorizationPage: React.FC = () => {
const [authorizations, setAuthorizations] = useState(mockAuthorizations);
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
// 状态标签渲染
const renderStatus = (status: string) => {
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.expired;
const Icon = config.icon;
return (
<Tag color={config.color} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Icon size={14} />
{config.label}
</Tag>
);
};
// 表格列配置
const columns = [
// {
// title: '',
// dataIndex: 'selection',
// key: 'selection',
// width: 60,
// render: (_: any, record: typeof mockAuthorizations[0]) => (
// <Checkbox
// checked={selectedRowKeys.includes(record.id)}
// onChange={(e) => {
// if (e.target.checked) {
// setSelectedRowKeys([...selectedRowKeys, record.id]);
// } else {
// setSelectedRowKeys(selectedRowKeys.filter(key => key !== record.id));
// }
// }}
// />
// ),
// },
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
render: (text: number) => <span style={{ color: '#94a3b8' }}>{text}</span>,
},
{
title: '授权ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
render: (text: string) => (
<span style={{ fontWeight: 500, color: '#1e293b' }}>{text}</span>
),
},
{
title: '授权状态',
dataIndex: 'status',
key: 'status',
width: 140,
render: (text: string) => renderStatus(text),
},
];
// 处理点击授权按钮
const handleAuthorize = () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择需要授权的记录');
return;
}
setLoading(true);
// 模拟授权操作
setTimeout(() => {
setAuthorizations(prev =>
prev.map(item =>
selectedRowKeys.includes(item.id) ? { ...item, status: 'active' } : item
)
);
setSelectedRowKeys([]);
setLoading(false);
message.success(`成功授权 ${selectedRowKeys.length} 条记录`);
}, 800);
};
// 准备表格数据(添加序号)
const tableData = authorizations.map((item, index) => ({
...item,
index: index + 1,
key: item.id,
}));
return (
<div style={{ padding: 24, minHeight: '94vh', background: '#f8fafc' }}>
{/* 页面标题 */}
<div style={{ marginBottom: 20 }}>
<h1 style={{ fontSize: 24, fontWeight: 600, color: '#1e293b', marginBottom: 8 }}>
</h1>
{/* <p style={{ color: '#64748b' }}>管理系统授权信息,查看和操作授权状态</p> */}
</div>
{/* 操作栏 */}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
<Button
type="primary"
size="large"
icon={<PlusOutlined />}
onClick={handleAuthorize}
loading={loading}
// disabled={selectedRowKeys.length === 0}
style={{
height: 40,
padding: '0 24px',
borderRadius: 8,
fontSize: 14,
fontWeight: 500,
}}
>
</Button>
</div>
{/* 表格 */}
<div style={{ background: '#fff', borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }}>
<Table
dataSource={tableData}
columns={columns}
pagination={{
pageSize: 10,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`,
}}
rowKey="id"
bordered={false}
style={{ padding: 16 }}
scroll={{ x: 'max-content' }}
/>
</div>
{/* 底部提示 */}
{/* <div style={{ marginTop: 16, textAlign: 'center', color: '#94a3b8', fontSize: 13 }}>
提示:勾选记录后点击"点击授权"按钮可批量授权
</div> */}
</div>
);
};
export default AuthorizationPage;
File diff suppressed because it is too large Load Diff
+127 -114
View File
@@ -11,7 +11,7 @@ import {
XOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import { gethistory,gethistoryItems } from '../api';
import { gethistory, gethistoryItems } from '../api';
const { Search } = Input;
const { Text } = Typography;
@@ -117,7 +117,7 @@ const GeneratedRecord: React.FC = () => {
// 从URL中提取exp时间戳(支持相对路径和完整URL)
const extractExpTimestamp = (url: string): number | null => {
if (!url) return null;
try {
// 尝试作为完整URL解析
const urlObj = new URL(url);
@@ -340,9 +340,9 @@ const GeneratedRecord: React.FC = () => {
<img
ref={mediaRef as React.RefObject<HTMLImageElement>}
src={coverUrl}
style={{
width: '100%',
height: '100%',
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: isLoaded ? 1 : 0,
transition: 'opacity 0.3s ease-in-out'
@@ -357,9 +357,9 @@ const GeneratedRecord: React.FC = () => {
ref={mediaRef as React.RefObject<HTMLImageElement>}
src={mediaUrl}
alt="图片预览"
style={{
width: '100%',
height: '100%',
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: isLoaded ? 1 : 0,
transition: 'opacity 0.3s ease-in-out'
@@ -421,12 +421,12 @@ const GeneratedRecord: React.FC = () => {
opacity: 0,
transition: 'opacity 0.2s',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.opacity = '1';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.opacity = '0';
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.opacity = '1';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.opacity = '0';
}}
>
</div>
@@ -434,7 +434,7 @@ const GeneratedRecord: React.FC = () => {
</div>
);
};
// 下载文件
const handleDownload = (item: any) => {
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl || item.imageUrl}`;
@@ -445,16 +445,16 @@ const GeneratedRecord: React.FC = () => {
link.click();
document.body.removeChild(link);
};
// 预览文件
const handlePreview = (item: any) => {
setPreviewItem(item);
setPreviewVisible(true);
// 触发事件通知布局组件关闭浮动按钮
window.dispatchEvent(new Event('previewOpen'));
};
// 关闭预览并暂停视频
const handleClosePreview = () => {
// 方法1: 使用 ref
@@ -470,7 +470,7 @@ const GeneratedRecord: React.FC = () => {
});
setPreviewVisible(false);
};
useEffect(() => {
setLoading(true);
let parameters = '';
@@ -490,7 +490,7 @@ const GeneratedRecord: React.FC = () => {
} else {
setRecordList(prev => [...prev, ...data]);
}
setTotalnumber(res?.totalDays || 0);
}).catch((err) => {
if (Pagebreak.page === 1) {
@@ -509,16 +509,16 @@ const GeneratedRecord: React.FC = () => {
page: prev.page + 1
}));
};
// 分组加载更多
const handleGroupLoadMore = async (time:string, date: string, page: number) => {
const handleGroupLoadMore = async (time: string, date: string, page: number) => {
if (loadingGroups.has(date)) return;
setLoadingGroups(prev => new Set([...prev, date]));
const addpage = page + 1;
let parameters = ``;
if (filterType === 'project') {
@@ -529,10 +529,10 @@ const GeneratedRecord: React.FC = () => {
try {
const res: any = await gethistoryItems(parameters);
// gethistoryItems 返回数组,直接使用
const newItems: any[] = res.items || [];
if (newItems && newItems.length > 0) {
setRecordList(prev => prev.map(group => {
if (group.generatedDate === time) {
@@ -566,14 +566,14 @@ const GeneratedRecord: React.FC = () => {
return (
<div>
{/* Header */}
<div style={{ marginBottom: 20 }}>
{/* <div style={{ marginBottom: 20 }}>
<Typography.Title level={3} style={{ margin: '0 0 4px', color: '#1a1a2e', fontWeight: 700 }}>
生成历史
</Typography.Title>
<Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}>
查看所有生成的视频和图片记录
</Typography.Text>
</div>
</div> */}
{/* First row filter: 项目记录 / 创作记录 */}
<div style={{
@@ -646,7 +646,7 @@ const GeneratedRecord: React.FC = () => {
: '#f8f9fc',
border: filterMedia === 'video' ? 'none' : '1px solid #e2e8f0',
color: filterMedia === 'video' ? '#fff' : '#64748b',
fontWeight: 600,
fontWeight: 600,
}}
icon={<VideoCameraOutlined />}
>
@@ -680,7 +680,7 @@ const GeneratedRecord: React.FC = () => {
/>
) : (
<div style={{ padding: '0 4px' }}>
{recordlist.map((group: any,index: number) => (
{recordlist.map((group: any, index: number) => (
<div key={index} style={{ marginBottom: 32 }}>
{/* Date label */}
<div style={{
@@ -750,7 +750,7 @@ const GeneratedRecord: React.FC = () => {
)}
</div>
)}
{/* 预览弹窗 */}
{previewVisible && previewItem && (
<div
@@ -811,7 +811,7 @@ const GeneratedRecord: React.FC = () => {
}}
/>
</div>
{/* 内容区域 */}
<div style={{
flex: 1,
@@ -825,12 +825,12 @@ const GeneratedRecord: React.FC = () => {
alignItems: 'center',
}}>
{/* 媒体预览 */}
<div style={{
<div style={{
flex: 1,
minWidth: '280px',
maxWidth: '800px',
display: 'flex',
alignItems: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '200px',
}}>
@@ -846,9 +846,9 @@ const GeneratedRecord: React.FC = () => {
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl}`}
controls
autoPlay
style={{
maxWidth: '100%',
maxHeight: '55vh',
style={{
maxWidth: '100%',
maxHeight: '55vh',
borderRadius: 8,
objectFit: 'contain',
}}
@@ -857,16 +857,16 @@ const GeneratedRecord: React.FC = () => {
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${previewItem.imageUrl}&w=300&q=50`}
alt="预览"
style={{
maxWidth: '100%',
maxHeight: '55vh',
objectFit: 'contain',
borderRadius: 8
style={{
maxWidth: '100%',
maxHeight: '55vh',
objectFit: 'contain',
borderRadius: 8
}}
/>
)}
</div>
{/* 参数信息 */}
<div style={{
width: '100%',
@@ -882,7 +882,7 @@ const GeneratedRecord: React.FC = () => {
<Typography.Text strong style={{ fontSize: 14, color: '#475569', display: 'block', marginBottom: 16 }}>
</Typography.Text>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* ID */}
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
@@ -891,7 +891,7 @@ const GeneratedRecord: React.FC = () => {
{previewItem.id || '-'}
</span>
</div> */}
{/* 类型 */}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
@@ -899,59 +899,59 @@ const GeneratedRecord: React.FC = () => {
{filterMedia === 'video' ? '视频' : '图片'}
</span>
</div>
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ display: 'flex', gap: 12 }}>
<span style={{ color: '#94a3b8', fontSize: 13, flexShrink: 0, width: 40 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500, flex: 1, wordBreak: 'break-all' }}>
{previewItem.originalPrompt}
</span>
</div>
{/* 分辨率 */}
{filterMedia === 'image' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imageProportion}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imageSize}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imagePx}
</span>
</div>
</>
)}
{filterMedia === 'video' && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imageProportion}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imageSize}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imagePx}
</span>
</div>
</>
)}
{filterMedia === 'video' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.aspectRatio}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.resolution}
</span>
</div>
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.aspectRatio}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.resolution}
</span>
</div>
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}>尺寸</span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imagePx}
</span>
</div> */}
</>
)}
</>
)}
{/* 时长(视频) */}
{filterMedia === 'video' && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
@@ -961,7 +961,7 @@ const GeneratedRecord: React.FC = () => {
</span>
</div>
)}
{/* 文件大小 */}
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}>文件大小</span>
@@ -969,7 +969,7 @@ const GeneratedRecord: React.FC = () => {
{previewItem.size ? formatFileSize(previewItem.size) : '-'}
</span>
</div> */}
{/* 创建时间 */}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
@@ -977,7 +977,7 @@ const GeneratedRecord: React.FC = () => {
{formatDateTime(previewItem.createdAt || previewItem.generatedDate)}
</span>
</div>
{/* 生成引擎 */}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
@@ -986,7 +986,7 @@ const GeneratedRecord: React.FC = () => {
</span>
</div>
</div>
{/* 分隔线 */}
<div style={{ borderTop: '1px dashed #e2e8f0', margin: '16px 0' }} />
@@ -1038,29 +1038,42 @@ const GeneratedRecord: React.FC = () => {
</>
)}
{/* 操作按钮 */}
<div style={{ display: 'flex', gap: 12 ,marginTop:20}}>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={() => {
// 暂停视频
if (videoRef.current) {
videoRef.current.pause();
}
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}`;
window.open(url, '_blank');
}}
style={{ flex: 1, borderRadius: 8 }}
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
>
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'}
</Button>
<Button
onClick={handleClosePreview}
style={{ flex: 1, borderRadius: 8 }}
>
</Button>
<div >
<div style={{ display: 'flex', gap: 12, marginTop: 20 }}>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={() => {
// 暂停视频
if (videoRef.current) {
videoRef.current.pause();
}
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}`;
window.open(url, '_blank');
}}
style={{ flex: 1, borderRadius: 8 }}
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
>
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'}
</Button>
<Button
onClick={handleClosePreview}
style={{ flex: 1, borderRadius: 8 }}
>
</Button>
</div>
<div>
<Button
style={{width:'100%', borderRadius: 8 ,marginTop:20,color:'#4c49cc'}}
>
</Button>
</div>
</div>
</div>
</div>
@@ -1095,14 +1108,14 @@ function formatDateTime(dateString: string): string {
if (!dateString) return '-';
const date = new Date(dateString);
if (isNaN(date.getTime())) return '-';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
+1
View File
@@ -332,6 +332,7 @@ const LoginPage: React.FC = () => {
</Button>
</Space.Compact>
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请设置密码' }, { min: 6, message: '密码至少6位' }]}>
<Input.Password prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请设置密码(至少6位)" style={inputStyle} />
</Form.Item>