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 InitialReplication from './pages/InitialReplication';
import RemoveLens from './pages/RemoveLens'; import RemoveLens from './pages/RemoveLens';
import GeneratedRecord from './pages/GeneratedRecord'; import GeneratedRecord from './pages/GeneratedRecord';
import AuthorizationPage from './pages/AuthorizationPage';
@@ -93,6 +94,8 @@ const App = () => {
<Route path="initial" element={<InitialReplication />} /> <Route path="initial" element={<InitialReplication />} />
<Route path="removelens" element={<RemoveLens />} /> <Route path="removelens" element={<RemoveLens />} />
<Route path="generated" element={<GeneratedRecord />} /> <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); 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;
+492 -280
View File
@@ -15,8 +15,10 @@ import {
Modal, Modal,
} from 'antd'; } from 'antd';
import { getParameters, createGenerationTask, getgen_list,getEngine,uploadImage, import {
uploadVideo, getCreditRatios } from '../api'; getParameters, createGenerationTask, getgen_list, getEngine, uploadImage,
uploadVideo, getCreditRatios, deleteHistory,calculateCredits
} from '../api';
import { import {
PlusOutlined, PlusOutlined,
@@ -31,6 +33,9 @@ import {
CaretDownOutlined, CaretDownOutlined,
SwapOutlined, SwapOutlined,
WarningOutlined, WarningOutlined,
SettingOutlined,
LayoutOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
const { Header, Sider, Content } = Layout; const { Header, Sider, Content } = Layout;
@@ -61,7 +66,7 @@ interface Message {
ratio?: string; ratio?: string;
resolution?: string; resolution?: string;
timestamp?: string; timestamp?: string;
engine_id:string; engine_id: string;
} }
@@ -185,7 +190,7 @@ const AIChatPage: React.FC = () => {
const [videoDuration, setVideoDuration] = useState(5); const [videoDuration, setVideoDuration] = useState(5);
const [videoAspectRatio, setVideoAspectRatio] = useState<string>('16:9'); const [videoAspectRatio, setVideoAspectRatio] = useState<string>('16:9');
const [videoResolution, setVideoResolution] = useState<string>('720p'); const [videoResolution, setVideoResolution] = useState<string>('720p');
const [expandedEngine, setExpandedEngine] = useState<string | null>(null); const [showEngineModal, setShowEngineModal] = useState(false);
const [engineOptions, setEngineOptions] = useState<{ const [engineOptions, setEngineOptions] = useState<{
ratios: string[]; ratios: string[];
resolutions: string[]; resolutions: string[];
@@ -199,50 +204,32 @@ const AIChatPage: React.FC = () => {
const [enginesele, setEnginesele] = useState<any>([]); const [enginesele, setEnginesele] = useState<any>([]);
const [creditRatios, setCreditRatios] = useState<any[]>([]); const [creditRatios, setCreditRatios] = useState<any[]>([]);
const [cimage, setCimage] = useState<any[]>([]); const [cimage, setCimage] = useState<any[]>([]);
const [creditCalculationData, setCreditCalculationData] = useState<any[]>([]);
// 计算视频积分 // 获取预估积分 - 根据引擎ID、类型和分辨率计算
const calcVideoCredits = (duration: number, resolution: string): number => {
for (let i = 0; i < creditRatios.length; i++) {
const ratio = creditRatios[i];
if (ratio.resolution === resolution) {
const result = Math.round((ratio.baseCredits + ratio.perSecondCredits * duration) * ratio.ratio);
return result;
}
}
return 0;
};
// 计算图片积分
const getImageCredits = (imageSize: string): number => {
// 将 imageSize 转换为 cimage 中的 resolution 格式
const resolution = imageSize
// 尝试精确匹配
for (let i = 0; i < cimage.length; i++) {
const item = cimage[i];
if (item.resolution === resolution) {
return item.baseCredits;
}
}
// 如果没有精确匹配,尝试查找第一个可用的积分配置
// if (cimage.length > 0) {
// console.log('getImageCredits fallback to first item:', cimage[0].baseCredits);
// return cimage[0].baseCredits;
// }
return 0;
};
// 获取预估积分
const getEstimatedCredits = (): number => { const getEstimatedCredits = (): number => {
// 根据当前选择的引擎ID、类型和分辨率查找对应的积分配置
const config = creditCalculationData.find((item: any) =>
item.modelConfigId === countType &&
item.genType === mediaType &&
item.resolution === (mediaType === 'video' ? videoResolution : selectedResolution)
);
if (!config) {
return 0;
}
console.log(config);
// 根据配置计算积分
if (mediaType === 'video') { if (mediaType === 'video') {
return calcVideoCredits(videoDuration, videoResolution); // 视频:(秒数 × perSecondCredits + baseCredits) × ratio
return Math.round((videoDuration * config.perSecondCredits + config.baseCredits) * config.ratio);
} else { } else {
return getImageCredits(selectedResolution); // 图片:baseCredits × ratio
return config.baseCredits * config.ratio;
} }
}; };
@@ -300,21 +287,76 @@ const AIChatPage: React.FC = () => {
} }
}, [mediaType, enginesele]); }, [mediaType, enginesele]);
// 点击外部关闭弹窗
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
// 关闭引擎选择弹窗
if (showEngineModal && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
setShowEngineModal(false);
}
// 关闭图片设置弹窗
if (showImageSettingsModal && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
setShowImageSettingsModal(false);
}
// 关闭视频设置弹窗
if (showVideoSettingsModal && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
setShowVideoSettingsModal(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showEngineModal, showImageSettingsModal, showVideoSettingsModal]);
// 初始化获取参数 - 只在组件挂载时执行一次 // 初始化获取参数 - 只在组件挂载时执行一次
useEffect(() => { useEffect(() => {
getEngine() getEngine()
.then((data:any) => { .then((data: any) => {
console.log('引擎', data);
setEnginesele(data.engine); setEnginesele(data.engine);
// 如果是视频模式,初始化视频参数选项
if (data.engine.video && data.engine.video.length > 0) {
const defaultEngine = data.engine.video[0];
setEngineOptions({
ratios: defaultEngine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
resolutions: defaultEngine.supportedResolutions || ['480p', '720p', '1080p'],
durations: defaultEngine.supportedDurations || [5, 8, 10, 12, 15],
});
// 设置默认选中值
if (defaultEngine.supportedRatios?.length > 0) {
setVideoAspectRatio(defaultEngine.supportedRatios[0]);
}
if (defaultEngine.supportedResolutions?.length > 0) {
setVideoResolution(defaultEngine.supportedResolutions[0]);
}
if (defaultEngine.supportedDurations?.length > 0) {
setVideoDuration(defaultEngine.supportedDurations[0]);
}
// 设置默认引擎
setCountType(defaultEngine.id);
}
}) })
.catch(() => { .catch(() => {
}); });
getCreditRatios() getCreditRatios()
.then((data: any) => { .then((data: any) => {
console.log('积分', data);
setCreditRatios(data.video || []); setCreditRatios(data.video || []);
setCimage(data.image || []); setCimage(data.image || []);
}) })
.catch((error) => { .catch((error) => {
}); });
calculateCredits().then((data: any) => {
console.log('积分计算', data);
// 保存积分计算数据
setCreditCalculationData(data);
})
getgen_list(Pagebreak).then((data: any) => { getgen_list(Pagebreak).then((data: any) => {
let mess_list = data.items let mess_list = data.items
let total = data.total let total = data.total
@@ -523,7 +565,7 @@ const AIChatPage: React.FC = () => {
id: '', id: '',
gen_type: mediaType, gen_type: mediaType,
original_prompt: inputValue.trim(), original_prompt: inputValue.trim(),
engine_id:countType, engine_id: countType,
idempotency_key: new Date().toLocaleString('zh-CN'), idempotency_key: new Date().toLocaleString('zh-CN'),
// 统一的媒体数组,包含 name、type、url // 统一的媒体数组,包含 name、type、url
@@ -750,7 +792,7 @@ const AIChatPage: React.FC = () => {
// ==================== 渲染 ==================== // ==================== 渲染 ====================
return ( return (
<Layout style={{ minHeight: '95vh', background: '#fafafa' }}> <Layout style={{ height: '94vh', background: '#fafafa', overflow: 'hidden' }}>
{/* 左侧边栏 - 对话列表(已隐藏,保留代码) */} {/* 左侧边栏 - 对话列表(已隐藏,保留代码) */}
{false && ( {false && (
<Sider <Sider
@@ -930,9 +972,11 @@ const AIChatPage: React.FC = () => {
)} )}
</div> </div>
{/* 遍历消息列表 */} {/* 遍历消息列表 */}
{gen_list.map((message) => ( {(() => {
const msgApi = message;
return gen_list.map((msg) => (
<div <div
key={message.id} key={msg.id}
style={{ style={{
display: 'flex', display: 'flex',
justifyContent: 'flex-start', // 所有消息左对齐 justifyContent: 'flex-start', // 所有消息左对齐
@@ -967,33 +1011,63 @@ const AIChatPage: React.FC = () => {
padding: '12px 16px', padding: '12px 16px',
width: '600px', width: '600px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)', boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
position: 'relative',
}} }}
> >
{/* 文本内容 */} {/* 删除按钮 - 右上角 */}
<p style={{ margin: 0, fontSize: 14, color: '#333' }}> <div style={{ position: 'absolute', top: 8, right: 8 }}>
{message.originalPrompt} <Popconfirm
{message.mediaReferences && message.mediaReferences.length > 0 && ( title="确定要删除吗?"
<span onConfirm={async () => {
style={{ marginLeft: 20, color: '#6366f1', cursor: 'pointer' }} await deleteHistory(msg.id);
onClick={(e) => { msgApi.success('删除成功');
e.stopPropagation(); // 直接在本地列表中删除对应数据
const target = e.currentTarget as HTMLElement; setGen_list(prev => prev.filter(item => item.id !== msg.id));
const rect = target.getBoundingClientRect(); setTotalnumber(prev => prev - 1);
setAttachmentPopupPosition({ }}
x: rect.left, okText="确定"
y: rect.top - 10 cancelText="取消"
}); >
setAttachmentPopupMessageId(message.id); <button
setAttachmentPopupVisible(true); onClick={(e) => e.stopPropagation()}
style={{
width: 28,
height: 28,
borderRadius: 8,
border: 'none',
background: 'rgba(0,0,0,0.05)',
color: '#999',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease',
padding: 0,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#fff2f0';
e.currentTarget.style.color = '#ff4d4f';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(0,0,0,0.05)';
e.currentTarget.style.color = '#999';
}} }}
> >
<DeleteOutlined style={{ fontSize: 12 }} />
</span> </button>
)} </Popconfirm>
</div>
{/* 文本内容 */}
<p style={{ margin: 0, fontSize: 14, color: '#333' }}>
{msg.originalPrompt}
</p> </p>
{/* 根据 status 显示不同内容 */} {/* 根据 status 显示不同内容 */}
{/* 生成中 - 显示加载动画 */} {/* 生成中 - 显示加载动画 */}
{message.status === 'generating' && ( {msg.status === 'generating' && (
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 16, marginTop: 12 }}> <div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 16, marginTop: 12 }}>
<div style={{ background: '#fff', borderRadius: '16px 16px 16px 4px', padding: '12px 16px', boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}> <div style={{ background: '#fff', borderRadius: '16px 16px 16px 4px', padding: '12px 16px', boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
@@ -1008,27 +1082,27 @@ const AIChatPage: React.FC = () => {
</div> </div>
)} )}
{/* 生成失败 - 显示失败提示 */} {/* 生成失败 - 显示失败提示 */}
{message.status === 'failed' && ( {msg.status === 'failed' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10, marginTop: 12, padding: '12px', background: '#fff2f0', borderRadius: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10, marginTop: 12, padding: '12px', background: '#fff2f0', borderRadius: 8 }}>
<WarningOutlined style={{ color: '#ff4d4f', fontSize: 16 }} /> <WarningOutlined style={{ color: '#ff4d4f', fontSize: 16 }} />
<span style={{ fontSize: 14, color: '#ff4d4f' }}></span> <span style={{ fontSize: 14, color: '#ff4d4f' }}></span>
</div> </div>
)} )}
{/* 已完成 - 显示媒体内容 */} {/* 已完成 - 显示媒体内容 */}
{message.status === 'completed' && ( {msg.status === 'completed' && (
<div style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 8, marginBottom: 10, marginTop: 12, width: '100%', }}> <div style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 8, marginBottom: 10, marginTop: 12, width: '100%', }}>
<div <div
onClick={() => { onClick={() => {
setPreviewUrl(message.genType === 'image' ? message.imageUrl : message.videoUrl); setPreviewUrl(msg.genType === 'image' ? msg.imageUrl : msg.videoUrl);
setPreviewType(message.genType === 'image' ? 'image' : 'video'); setPreviewType(msg.genType === 'image' ? 'image' : 'video');
setPreviewVisible(true); setPreviewVisible(true);
}} }}
style={{ cursor: 'pointer', overflow: 'hidden', borderRadius: 8, position: 'relative' ,height: 200 }} style={{ cursor: 'pointer', overflow: 'hidden', borderRadius: 8, position: 'relative', height: 200 }}
> >
{message.genType === 'image' ? ( {msg.genType === 'image' ? (
<img <img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${message.imageUrl}&w=300&p=50`} src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${msg.imageUrl}&w=300&p=50`}
alt={message.name} alt={msg.name}
style={{ width: '100%', height: '100%', borderRadius: 8, objectFit: 'contain', backgroundColor: '#f1f5f9', transition: 'transform 0.2s' }} style={{ width: '100%', height: '100%', borderRadius: 8, objectFit: 'contain', backgroundColor: '#f1f5f9', transition: 'transform 0.2s' }}
onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }} onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
@@ -1036,8 +1110,8 @@ const AIChatPage: React.FC = () => {
) : ( ) : (
<> <>
<img <img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${message.videoCoverUrl}&w=300&p=50`} src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${msg.videoCoverUrl}&w=300&p=50`}
alt={message.name} alt={msg.name}
style={{ width: '100%', height: '100%', borderRadius: 8, objectFit: 'contain', backgroundColor: '#f1f5f9', transition: 'transform 0.2s' }} style={{ width: '100%', height: '100%', borderRadius: 8, objectFit: 'contain', backgroundColor: '#f1f5f9', transition: 'transform 0.2s' }}
onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }} onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
@@ -1059,7 +1133,7 @@ const AIChatPage: React.FC = () => {
boxShadow: '0 4px 12px rgba(0,0,0,0.3)', boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
}}> }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#fff"> <svg width="24" height="24" viewBox="0 0 24 24" fill="#fff">
<path d="M8 5v14l11-7z"/> <path d="M8 5v14l11-7z" />
</svg> </svg>
</div> </div>
</> </>
@@ -1071,22 +1145,48 @@ const AIChatPage: React.FC = () => {
</div> </div>
{/* 时间戳和参数信息 */} {/* 时间戳和参数信息 */}
<div style={{ margin: 4, fontSize: 11, color: '#999', textAlign: 'left' }}> <div style={{ margin: 4, fontSize: 11, color: '#999', textAlign: 'left', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
<span>{message.createdAt?.replace('T', ' ').split('.')[0]}</span> <span>{msg.createdAt?.replace('T', ' ').split('.')[0]}</span>
<span style={{ marginLeft: 8 }}>{message.genType === 'image' {/* 引擎标签 */}
? `${message.imageProportion || ''} ${message.imagePx || ''} ${message.imageSize || ''}` <span >
: `${message.duration || ''}s ${message.aspectRatio || ''} ${message.resolution || ''}` {/* <SettingsOutlined style={{ fontSize: 12 }} /> */}
}</span> {msg.engineSnapshot.name}
<span style={{ marginLeft: 20 }}>{message.engineSnapshot.name}</span> </span>
<span style={{ marginLeft: 20 }}>{message.creditsCost}</span> {/* 参数标签 */}
<span >
{/* <LayoutGridOutlined style={{ fontSize: 12 }} /> */}
{msg.genType === 'image'
? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}`
: `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}`
}
</span>
<span style={{ marginLeft: 8 }}>{msg.creditsCost}</span>
{msg.mediaReferences && msg.mediaReferences.length > 0 && (
<span
style={{ marginLeft: 20, color: '#6366f1', cursor: 'pointer' }}
onClick={(e) => {
e.stopPropagation();
const target = e.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
setAttachmentPopupPosition({
x: rect.left,
y: rect.top - 10
});
setAttachmentPopupMessageId(msg.id);
setAttachmentPopupVisible(true);
}}
>
</span>
)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
))} ))})()}
{/* 消息列表底部标记 - 用于自动滚动 */} {/* 消息列表底部标记 - 用于自动滚动 */}
@@ -1128,74 +1228,74 @@ const AIChatPage: React.FC = () => {
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontSize: 14, fontWeight: 500 }}></span> <span style={{ fontSize: 14, fontWeight: 500 }}></span>
<button <button
onClick={() => { onClick={() => {
setAttachmentPopupVisible(false); setAttachmentPopupVisible(false);
setAttachmentPopupMessageId(null); setAttachmentPopupMessageId(null);
}} }}
style={{ style={{
width: 24, width: 24,
height: 24, height: 24,
borderRadius: '50%', borderRadius: '50%',
border: 'none', border: 'none',
background: '#ff4d4f', background: '#ff4d4f',
cursor: 'pointer', cursor: 'pointer',
fontSize: 14, fontSize: 14,
color: '#fff', color: '#fff',
fontWeight: 'bold', fontWeight: 'bold',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
transition: 'all 0.2s', transition: 'all 0.2s',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = '#ff7875'; e.currentTarget.style.background = '#ff7875';
e.currentTarget.style.transform = 'scale(1.1)'; e.currentTarget.style.transform = 'scale(1.1)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = '#ff4d4f'; e.currentTarget.style.background = '#ff4d4f';
e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.transform = 'scale(1)';
}} }}
> >
× ×
</button> </button>
</div>
{gen_list.find((msg: any) => msg.id === attachmentPopupMessageId)?.mediaReferences?.map((ref: any, idx: number) => (
<div
key={idx}
onClick={() => {
setAttachmentPreviewUrl(ref.url);
setAttachmentPreviewType(ref.url.includes('.mp4') || ref.url.includes('.mov') || ref.url.includes('.avi') || ref.url.includes('.video') ? 'video' : 'image');
setAttachmentPreviewName(ref.name);
setAttachmentPreviewVisible(true);
// 关闭附件悬浮窗,避免层级覆盖问题
setAttachmentPopupVisible(false);
setAttachmentPopupMessageId(null);
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 12px',
borderRadius: 6,
cursor: 'pointer',
fontSize: 13,
color: '#333',
transition: 'background 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.background = '#f5f5f5'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
{ref.type === 'image' ? (
<PictureOutlined style={{ color: '#3b82f6', fontSize: 14 }} />
) : (
<VideoCameraOutlined style={{ color: '#f59e0b', fontSize: 14 }} />
)}
{ref.name}
</div> </div>
))} {gen_list.find((msg: any) => msg.id === attachmentPopupMessageId)?.mediaReferences?.map((ref: any, idx: number) => (
<div
key={idx}
onClick={() => {
setAttachmentPreviewUrl(ref.url);
setAttachmentPreviewType(ref.url.includes('.mp4') || ref.url.includes('.mov') || ref.url.includes('.avi') || ref.url.includes('.video') ? 'video' : 'image');
setAttachmentPreviewName(ref.name);
setAttachmentPreviewVisible(true);
// 关闭附件悬浮窗,避免层级覆盖问题
setAttachmentPopupVisible(false);
setAttachmentPopupMessageId(null);
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 12px',
borderRadius: 6,
cursor: 'pointer',
fontSize: 13,
color: '#333',
transition: 'background 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.background = '#f5f5f5'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
{ref.type === 'image' ? (
<PictureOutlined style={{ color: '#3b82f6', fontSize: 14 }} />
) : (
<VideoCameraOutlined style={{ color: '#f59e0b', fontSize: 14 }} />
)}
{ref.name}
</div>
))}
</div> </div>
</> </>
)} )}
@@ -1360,24 +1460,159 @@ const AIChatPage: React.FC = () => {
border: '1px solid #e2e8f0', border: '1px solid #e2e8f0',
height: 28, height: 28,
}}> */} }}> */}
{/* <Text style={{ fontSize: 12, color: '#94a3b8' }}>类型</Text> */} {/* <Text style={{ fontSize: 12, color: '#94a3b8' }}>类型</Text> */}
<Select <Select
value={mediaType} value={mediaType}
onChange={(val) => setMediaType(val)} onChange={(val) => setMediaType(val)}
style={{ width: 90,outline:'none',background: '#f8f9fc',border: '1px solid #e2e8f0', }} style={{
size="small" width: 100,
> outline: 'none',
<Option value="image"> background: '#f1f5f9',
<PictureOutlined style={{ marginRight: 4 }} /> border: 'none',
borderRadius: 8,
</Option> height: 34,
<Option value="video"> }}
<VideoCameraOutlined style={{ marginRight: 4 }} /> size="middle"
>
</Option> <Option value="image">
</Select> <PictureOutlined style={{ marginRight: 6, fontSize: 14 }} />
<span style={{ fontSize: 14, fontWeight: 500, color: '#64748b' }}></span>
</Option>
<Option value="video">
<VideoCameraOutlined style={{ marginRight: 6, fontSize: 14 }} />
<span style={{ fontSize: 14, fontWeight: 500, color: '#64748b' }}></span>
</Option>
</Select>
{/* </div> */} {/* </div> */}
{/* 引擎选择器 */}
<div style={{ position: 'relative', display: 'inline-block' }}>
<button
onClick={() => setShowEngineModal(true)}
className="image-settings-trigger"
style={{
minWidth: 200,
padding: '4px 12px',
height: 34,
borderRadius: 8,
border: 'none',
backgroundColor: '#f1f5f9',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
transition: 'all 0.2s',
}}
>
<SettingOutlined style={{ fontSize: 14, color: '#64748b' }} />
<Text style={{
fontSize: 14,
fontWeight: 500,
color: '#64748b',
}}>
{mediaType === 'image' ? enginesele.image?.find((e: any) => e.id === countType)?.name : enginesele.video?.find((e: any) => e.id === countType)?.name || '选择引擎'}
</Text>
</button>
{showEngineModal && (
<div
className="image-settings-popover"
style={{
position: 'absolute',
bottom: 'calc(100% + 8px)',
left: 0,
width: 360,
backgroundColor: '#fff',
borderRadius: 16,
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
padding: 16,
border: 'none',
zIndex: 9999,
}}
onClick={(e) => e.stopPropagation()}
>
{/* 选择引擎 */}
<div style={{ marginBottom: 8 }}>
<Text style={{
display: 'block',
marginBottom: 8,
fontSize: 12,
fontWeight: 500,
color: '#666666',
}}>
</Text>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}>
{(mediaType === 'image' ? enginesele.image : enginesele.video)?.map((engine: any) => (
<button
key={engine.id}
onClick={() => {
setCountType(engine.id);
// 根据选中的引擎更新视频参数选项
if (mediaType === 'video') {
setEngineOptions({
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
durations: engine.supportedDurations || [5, 8, 10, 12, 15],
});
// 更新默认选中值,确保在新引擎支持的范围内
if (!engine.supportedRatios?.includes(videoAspectRatio)) {
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
}
if (!engine.supportedResolutions?.includes(videoResolution)) {
setVideoResolution(engine.supportedResolutions?.[0] || '720p');
}
if (!engine.supportedDurations?.includes(videoDuration)) {
setVideoDuration(engine.supportedDurations?.[0] || 5);
}
}
setShowEngineModal(false);
}}
style={{
flex: 1,
minHeight: 48,
borderRadius: 8,
border: countType === engine.id
? '2px solid #6366f1'
: '1px solid #e5e7eb',
backgroundColor: countType === engine.id
? '#fff'
: '#f9fafb',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
padding: '8px 12px',
transition: 'all 0.2s',
textAlign: 'left',
}}
>
<span style={{
fontSize: 13,
fontWeight: countType === engine.id ? 600 : 500,
color: countType === engine.id ? '#6366f1' : '#4b5563',
marginBottom: 2,
}}>
{engine.name}
</span>
<span style={{
fontSize: 11,
color: '#9ca3af',
}}>
</span>
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* 图片设置按钮 */} {/* 图片设置按钮 */}
{mediaType === 'image' && ( {mediaType === 'image' && (
<div style={{ position: 'relative', display: 'inline-block' }}> <div style={{ position: 'relative', display: 'inline-block' }}>
@@ -1385,34 +1620,27 @@ const AIChatPage: React.FC = () => {
onClick={() => setShowImageSettingsModal(!showImageSettingsModal)} onClick={() => setShowImageSettingsModal(!showImageSettingsModal)}
className="image-settings-trigger" className="image-settings-trigger"
style={{ style={{
minWidth: 180, minWidth: 220,
padding: '0px 12px', padding: '4px 12px',
height: 28, height: 34,
borderRadius: 8, borderRadius: 8,
border: '1px solid #e2e8f0', border: 'none',
backgroundColor: '#f8f9fc', backgroundColor: '#f1f5f9',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', gap: 6,
gap: 8,
transition: 'all 0.2s', transition: 'all 0.2s',
}} }}
> >
<div style={{ textAlign: 'left' }}> <LayoutOutlined style={{ fontSize: 14, color: '#64748b' }} />
<Text style={{ <Text style={{
fontSize: 12, fontSize: 14,
fontWeight: 600, fontWeight: 500,
color: '#374151', color: '#64748b',
marginRight: 8, }}>
}}> {selectedRatio === 'auto' ? '智能' : selectedRatio} · {selectedResolution == '2K' ? '2K高清' : '4K超清'} · {width}×{height}
{selectedRatio === 'auto' ? '智能' : selectedRatio} </Text>
</Text>
<Text style={{ fontSize: 11, color: '#9ca3af' }}>
{selectedResolution == '2K' ? '高清 2K' : '超清 4K'} | {width}×{height}
</Text>
</div>
<CaretDownOutlined style={{ fontSize: 12, color: '#9ca3af' }} />
</button> </button>
{showImageSettingsModal && ( {showImageSettingsModal && (
@@ -1673,34 +1901,27 @@ const AIChatPage: React.FC = () => {
onClick={() => setShowVideoSettingsModal(!showVideoSettingsModal)} onClick={() => setShowVideoSettingsModal(!showVideoSettingsModal)}
className="image-settings-trigger" className="image-settings-trigger"
style={{ style={{
minWidth: 180, minWidth: 220,
padding: '0px 12px', padding: '4px 12px',
height: 28, height: 34,
borderRadius: 8, borderRadius: 8,
border: '1px solid #e2e8f0', border: 'none',
backgroundColor: '#f8f9fc', backgroundColor: '#f1f5f9',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', gap: 6,
gap: 8,
transition: 'all 0.2s', transition: 'all 0.2s',
}} }}
> >
<div style={{ textAlign: 'left' }}> <LayoutOutlined style={{ fontSize: 14, color: '#64748b' }} />
<Text style={{ <Text style={{
fontSize: 12, fontSize: 14,
fontWeight: 600, fontWeight: 500,
color: '#374151', color: '#64748b',
marginRight: 8, }}>
}}> {videoAspectRatio} · {videoDuration}s · {videoResolution}
{videoAspectRatio} </Text>
</Text>
<Text style={{ fontSize: 11, color: '#9ca3af' }}>
{videoDuration}s | {videoResolution}
</Text>
</div>
<CaretDownOutlined style={{ fontSize: 12, color: '#9ca3af' }} />
</button> </button>
{showVideoSettingsModal && ( {showVideoSettingsModal && (
@@ -1791,44 +2012,63 @@ const AIChatPage: React.FC = () => {
}}> }}>
</Text> </Text>
<div style={{ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
display: 'flex', <div style={{ flex: 1, position: 'relative', height: 24, display: 'flex', alignItems: 'center' }}>
flexWrap: 'wrap', {/* 背景轨道 */}
gap: 4, <div style={{
}}> position: 'absolute',
{engineOptions.durations.map((duration) => ( top: '50%',
<button left: 0,
key={duration} right: 0,
onClick={() => setVideoDuration(duration)} height: 6,
borderRadius: 3,
background: '#e5e7eb',
transform: 'translateY(-50%)',
}} />
{/* 已滑动部分 */}
<div style={{
position: 'absolute',
top: '50%',
left: 0,
height: 6,
borderRadius: 3,
background: '#6366f1',
width: `${((videoDuration - Math.min(...engineOptions.durations)) / (Math.max(...engineOptions.durations) - Math.min(...engineOptions.durations))) * 100}%`,
transform: 'translateY(-50%)',
}} />
{/* 滑块 */}
<input
type="range"
min={Math.min(...engineOptions.durations)}
max={Math.max(...engineOptions.durations)}
value={videoDuration}
onChange={(e) => setVideoDuration(Number(e.target.value))}
style={{ style={{
flex: '0 0 calc(20% - 4px)', position: 'relative',
minWidth: 50, width: '100%',
height: 42, height: 24,
borderRadius: 6, borderRadius: 3,
border: videoDuration === duration background: 'transparent',
? '2px solid #6366f1' outline: 'none',
: '1px solid #e5e7eb', appearance: 'none',
backgroundColor: videoDuration === duration
? '#6366f1'
: '#f9fafb',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', zIndex: 1,
justifyContent: 'center',
alignItems: 'center',
transition: 'all 0.2s',
}} }}
> />
<span style={{ </div>
fontSize: 12, <div style={{
fontWeight: 600, display: 'flex',
color: videoDuration === duration alignItems: 'center',
? '#fff' gap: 4,
: '#4b5563', padding: '4px 12px',
}}> backgroundColor: '#f1f5f9',
{duration}s borderRadius: 6,
</span> }}>
</button> <span style={{ fontSize: 14, fontWeight: 600, color: '#64748b' }}>
))} {videoDuration}
</span>
<span style={{ fontSize: 12, color: '#9ca3af' }}></span>
</div>
</div> </div>
</div> </div>
@@ -1882,34 +2122,6 @@ const AIChatPage: React.FC = () => {
)} )}
</div> </div>
)} )}
{/* 引擎选择器 */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '5px 12px',
borderRadius: 8,
background: '#f8f9fc',
border: '1px solid #e2e8f0',
height: 28,
}}>
<Text style={{ fontSize: 12, color: '#94a3b8' }}></Text>
<Select
placeholder="选择引擎"
value={countType}
onChange={(val) => {
setCountType(val);
}}
style={{ width: 200 ,outline:'none',background: '#f8f9fc',border: '1px solid #e2e8f0', }}
size="small"
>
{(mediaType === 'image' ? enginesele.image : enginesele.video)?.map((item:any) => (
<Option key={item.id} value={item.id}>{item.name}</Option>
))}
</Select>
</div>
{/* 预估积分 */} {/* 预估积分 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Text style={{ fontSize: 12, color: '#94a3b8' }}></Text> <Text style={{ fontSize: 12, color: '#94a3b8' }}></Text>
+84 -71
View File
@@ -11,7 +11,7 @@ import {
XOutlined, XOutlined,
ClockCircleOutlined, ClockCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { gethistory,gethistoryItems } from '../api'; import { gethistory, gethistoryItems } from '../api';
const { Search } = Input; const { Search } = Input;
const { Text } = Typography; const { Text } = Typography;
@@ -421,12 +421,12 @@ const GeneratedRecord: React.FC = () => {
opacity: 0, opacity: 0,
transition: 'opacity 0.2s', transition: 'opacity 0.2s',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.opacity = '1'; (e.currentTarget as HTMLElement).style.opacity = '1';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.opacity = '0'; (e.currentTarget as HTMLElement).style.opacity = '0';
}} }}
> >
</div> </div>
@@ -513,7 +513,7 @@ const GeneratedRecord: React.FC = () => {
}; };
// 分组加载更多 // 分组加载更多
const handleGroupLoadMore = async (time:string, date: string, page: number) => { const handleGroupLoadMore = async (time: string, date: string, page: number) => {
if (loadingGroups.has(date)) return; if (loadingGroups.has(date)) return;
setLoadingGroups(prev => new Set([...prev, date])); setLoadingGroups(prev => new Set([...prev, date]));
@@ -566,14 +566,14 @@ const GeneratedRecord: React.FC = () => {
return ( return (
<div> <div>
{/* Header */} {/* Header */}
<div style={{ marginBottom: 20 }}> {/* <div style={{ marginBottom: 20 }}>
<Typography.Title level={3} style={{ margin: '0 0 4px', color: '#1a1a2e', fontWeight: 700 }}> <Typography.Title level={3} style={{ margin: '0 0 4px', color: '#1a1a2e', fontWeight: 700 }}>
生成历史 生成历史
</Typography.Title> </Typography.Title>
<Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}> <Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}>
查看所有生成的视频和图片记录 查看所有生成的视频和图片记录
</Typography.Text> </Typography.Text>
</div> </div> */}
{/* First row filter: 项目记录 / 创作记录 */} {/* First row filter: 项目记录 / 创作记录 */}
<div style={{ <div style={{
@@ -680,7 +680,7 @@ const GeneratedRecord: React.FC = () => {
/> />
) : ( ) : (
<div style={{ padding: '0 4px' }}> <div style={{ padding: '0 4px' }}>
{recordlist.map((group: any,index: number) => ( {recordlist.map((group: any, index: number) => (
<div key={index} style={{ marginBottom: 32 }}> <div key={index} style={{ marginBottom: 32 }}>
{/* Date label */} {/* Date label */}
<div style={{ <div style={{
@@ -899,7 +899,7 @@ const GeneratedRecord: React.FC = () => {
{filterMedia === 'video' ? '视频' : '图片'} {filterMedia === 'video' ? '视频' : '图片'}
</span> </span>
</div> </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: '#94a3b8', fontSize: 13, flexShrink: 0, width: 40 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500, flex: 1, wordBreak: 'break-all' }}> <span style={{ color: '#334155', fontSize: 13, fontWeight: 500, flex: 1, wordBreak: 'break-all' }}>
{previewItem.originalPrompt} {previewItem.originalPrompt}
@@ -909,48 +909,48 @@ const GeneratedRecord: React.FC = () => {
{/* 分辨率 */} {/* 分辨率 */}
{filterMedia === 'image' && ( {filterMedia === 'image' && (
<> <>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span> <span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}> <span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imageProportion} {previewItem.imageProportion}
</span> </span>
</div> </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: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}> <span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imageSize} {previewItem.imageSize}
</span> </span>
</div> </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: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}> <span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imagePx} {previewItem.imagePx}
</span> </span>
</div> </div>
</> </>
)} )}
{filterMedia === 'video' && ( {filterMedia === 'video' && (
<> <>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span> <span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}> <span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.aspectRatio} {previewItem.aspectRatio}
</span> </span>
</div> </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: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}> <span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.resolution} {previewItem.resolution}
</span> </span>
</div> </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: '#94a3b8', fontSize: 13 }}>尺寸</span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}> <span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imagePx} {previewItem.imagePx}
</span> </span>
</div> */} </div> */}
</> </>
)} )}
{/* 时长(视频) */} {/* 时长(视频) */}
{filterMedia === 'video' && ( {filterMedia === 'video' && (
@@ -1038,29 +1038,42 @@ const GeneratedRecord: React.FC = () => {
</> </>
)} )}
{/* 操作按钮 */} {/* 操作按钮 */}
<div style={{ display: 'flex', gap: 12 ,marginTop:20}}> <div >
<Button <div style={{ display: 'flex', gap: 12, marginTop: 20 }}>
type="primary" <Button
icon={<DownloadOutlined />} type="primary"
onClick={() => { icon={<DownloadOutlined />}
// 暂停视频 onClick={() => {
if (videoRef.current) { // 暂停视频
videoRef.current.pause(); 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'); 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)} style={{ flex: 1, borderRadius: 8 }}
> disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'} >
</Button> {isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'}
<Button </Button>
onClick={handleClosePreview} <Button
style={{ flex: 1, borderRadius: 8 }} onClick={handleClosePreview}
> style={{ flex: 1, borderRadius: 8 }}
>
</Button>
</Button>
</div>
<div>
<Button
style={{width:'100%', borderRadius: 8 ,marginTop:20,color:'#4c49cc'}}
>
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>
+1
View File
@@ -332,6 +332,7 @@ const LoginPage: React.FC = () => {
</Button> </Button>
</Space.Compact> </Space.Compact>
</Form.Item> </Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请设置密码' }, { min: 6, message: '密码至少6位' }]}> <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} /> <Input.Password prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请设置密码(至少6位)" style={inputStyle} />
</Form.Item> </Form.Item>