“历史素材”

This commit is contained in:
sjy
2026-07-11 11:32:20 +08:00
parent f5f4be2986
commit 742c8688bc
19 changed files with 377 additions and 188 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-I3eif2op.js"></script> <script type="module" crossorigin src="/assets/index-B8KgQUTE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css"> <link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
</head> </head>
<body> <body>
@@ -30,6 +30,7 @@ interface UploadSelectorProps {
usedAudioCount?: number; usedAudioCount?: number;
maxAudioDuration?: number; maxAudioDuration?: number;
usedAudioDuration?: number; usedAudioDuration?: number;
hideLimitHint?: boolean;
} }
const UploadSelector: React.FC<UploadSelectorProps> = ({ const UploadSelector: React.FC<UploadSelectorProps> = ({
@@ -52,6 +53,7 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
usedAudioCount, usedAudioCount,
maxAudioDuration, maxAudioDuration,
usedAudioDuration, usedAudioDuration,
hideLimitHint,
}) => { }) => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [portraitPickerOpen, setPortraitPickerOpen] = useState(false); const [portraitPickerOpen, setPortraitPickerOpen] = useState(false);
@@ -243,6 +245,7 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
usedAudioCount={usedAudioCount} usedAudioCount={usedAudioCount}
maxAudioDuration={maxAudioDuration} maxAudioDuration={maxAudioDuration}
usedAudioDuration={usedAudioDuration} usedAudioDuration={usedAudioDuration}
hideLimitHint={hideLimitHint}
/> />
<PrivatePortraitAssetPicker <PrivatePortraitAssetPicker
@@ -120,7 +120,7 @@ const PrivatePortraitLibraryPanel: React.FC = () => {
activeKey={activeKey} activeKey={activeKey}
onChange={handleTabChange} onChange={handleTabChange}
items={items} items={items}
destroyInactiveTabPane={false} destroyOnHidden={false}
tabBarExtraContent={ tabBarExtraContent={
<div style={{ fontSize: 14, fontWeight: 700, color: '#4f46e5', marginTop: 8 }}>{quotaText}//</div> <div style={{ fontSize: 14, fontWeight: 700, color: '#4f46e5', marginTop: 8 }}>{quotaText}//</div>
} }
@@ -13,7 +13,7 @@ interface Props {
const PrivatePortraitProjectList: React.FC<Props> = ({ items, selectedId, onSelect }) => { const PrivatePortraitProjectList: React.FC<Props> = ({ items, selectedId, onSelect }) => {
if (!items.length) return <Empty description="暂无项目组" />; if (!items.length) return <Empty description="暂无项目组" />;
return ( return (
<Space direction="vertical" style={{ width: '100%' }} size={10}> <Space orientation="vertical" style={{ width: '100%' }} size={10}>
{items.map((project) => { {items.map((project) => {
const active = selectedId === project.id; const active = selectedId === project.id;
return ( return (
@@ -155,7 +155,7 @@ const RealPersonLibraryPanel: React.FC = () => {
onOk={validateSession ? undefined : handleCreate} onOk={validateSession ? undefined : handleCreate}
okText="开始认证并创建" okText="开始认证并创建"
confirmLoading={creating} confirmLoading={creating}
maskClosable={!polling} mask={{ closable: !polling }}
> >
{!validateSession ? ( {!validateSession ? (
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
@@ -338,7 +338,7 @@ const VirtualMaterialPanel: React.FC = () => {
<Card <Card
key={asset.id} key={asset.id}
hoverable hoverable
bodyStyle={{ padding: 12 }} styles={{ body: { padding: 12 } }}
style={{ borderRadius: 16, overflow: 'hidden', borderColor: '#eef2f7' }} style={{ borderRadius: 16, overflow: 'hidden', borderColor: '#eef2f7' }}
cover={( cover={(
<div style={{ height: 170, background: '#f8fafc', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}> <div style={{ height: 170, background: '#f8fafc', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
@@ -563,7 +563,7 @@ const VirtualMaterialPanel: React.FC = () => {
</Space> </Space>
</Modal> </Modal>
<Modal title="素材预览" open={previewOpen} onCancel={() => setPreviewOpen(false)} footer={null} width={760} destroyOnClose> <Modal title="素材预览" open={previewOpen} onCancel={() => setPreviewOpen(false)} footer={null} width={760} destroyOnHidden>
<div style={{ minHeight: 420, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#0f172a', borderRadius: 12, overflow: 'hidden' }}> <div style={{ minHeight: 420, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#0f172a', borderRadius: 12, overflow: 'hidden' }}>
{previewType === 'Video' ? ( {previewType === 'Video' ? (
<video src={previewUrl} controls autoPlay style={{ maxWidth: '100%', maxHeight: 520 }} /> <video src={previewUrl} controls autoPlay style={{ maxWidth: '100%', maxHeight: 520 }} />
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Button, Empty, Input, List, Modal, Select, Space, Spin, Tag, Typography, message } from 'antd'; import { Button, Empty, Input, Modal, Select, Space, Spin, Tag, Typography, message } from 'antd';
import { CheckOutlined, PictureOutlined, ReloadOutlined, SearchOutlined, VideoCameraOutlined } from '@ant-design/icons'; import { CheckOutlined, PictureOutlined, ReloadOutlined, SearchOutlined, VideoCameraOutlined } from '@ant-design/icons';
import { import {
getPrivatePortraitProjects, getPrivatePortraitProjects,
@@ -251,33 +251,36 @@ const PrivatePortraitAssetPicker: React.FC<PrivatePortraitAssetPickerProps> = ({
<Button size="small" icon={<ReloadOutlined />} onClick={loadProjects} loading={loadingProjects} /> <Button size="small" icon={<ReloadOutlined />} onClick={loadProjects} loading={loadingProjects} />
</Space> </Space>
<Spin spinning={loadingProjects}> <Spin spinning={loadingProjects}>
<List {projects.length === 0 ? (
dataSource={projects} <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无项目组" />
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无项目组" /> }} ) : (
renderItem={(item) => ( <div>
<List.Item {projects.map((item) => (
onClick={() => setProjectId(item.id)} <div
style={{ key={item.id}
cursor: 'pointer', onClick={() => setProjectId(item.id)}
padding: '10px 12px', style={{
borderRadius: 10, cursor: 'pointer',
marginBottom: 6, padding: '10px 12px',
border: projectId === item.id ? '1px solid #8b5cf6' : '1px solid transparent', borderRadius: 10,
background: projectId === item.id ? '#f5f3ff' : '#fff', marginBottom: 6,
}} border: projectId === item.id ? '1px solid #8b5cf6' : '1px solid transparent',
> background: projectId === item.id ? '#f5f3ff' : '#fff',
<div style={{ width: '100%' }}> }}
<Text strong ellipsis style={{ display: 'block' }}>{item.name}</Text> >
<Text type="secondary" style={{ fontSize: 12 }}> <div style={{ width: '100%' }}>
{/* {item.activeAssetCount || 0} */} <Text strong ellipsis style={{ display: 'block' }}>{item.name}</Text>
{typeof item.activeImageAssetCount === 'number' || typeof item.activeVideoAssetCount === 'number' <Text type="secondary" style={{ fontSize: 12 }}>
? `${item.activeImageAssetCount || 0} / 视 ${item.activeVideoAssetCount || 0}` {/* {item.activeAssetCount || 0} */}
: ''} {typeof item.activeImageAssetCount === 'number' || typeof item.activeVideoAssetCount === 'number'
</Text> ? `${item.activeImageAssetCount || 0} / 视 ${item.activeVideoAssetCount || 0}`
: ''}
</Text>
</div>
</div> </div>
</List.Item> ))}
)} </div>
/> )}
</Spin> </Spin>
</div> </div>
@@ -232,7 +232,7 @@ const UploadResourceHistoryPanel: React.FC = () => {
const checked = selectedIds.has(item.id); const checked = selectedIds.has(item.id);
return ( return (
<div key={item.id} style={{ width: '17%', minWidth: 240, margin: 16, border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0', borderRadius: 16, overflow: 'hidden', background: '#fff', boxShadow: '0 10px 24px rgba(15,23,42,0.06)' }}> <div key={item.id} style={{ width: '17%', minWidth: 240, margin: 16, border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0', borderRadius: 16, overflow: 'hidden', background: '#fff', boxShadow: '0 10px 24px rgba(15,23,42,0.06)' }}>
<div style={{ position: 'relative', background: '#f1f5f9' }}> <div style={{ position: 'relative', background: '#f1f5f9', cursor: batchMode ? 'pointer' : 'default' }} onClick={() => batchMode && toggle(item.id)}>
{renderMedia(item)} {renderMedia(item)}
{batchMode && <Checkbox checked={checked} onChange={() => toggle(item.id)} style={{ position: 'absolute', top: 10, left: 10, background: '#fff', borderRadius: 6, padding: 4 }} />} {batchMode && <Checkbox checked={checked} onChange={() => toggle(item.id)} style={{ position: 'absolute', top: 10, left: 10, background: '#fff', borderRadius: 6, padding: 4 }} />}
<Tag color="purple" icon={typeIcon(item.resourceType)} style={{ position: 'absolute', top: 10, right: 10, margin: 0 }}>{typeLabel(item.resourceType)}</Tag> <Tag color="purple" icon={typeIcon(item.resourceType)} style={{ position: 'absolute', top: 10, right: 10, margin: 0 }}>{typeLabel(item.resourceType)}</Tag>
@@ -26,15 +26,72 @@ interface UploadResourceHistoryPickerProps {
usedAudioCount?: number; usedAudioCount?: number;
maxAudioDuration?: number; maxAudioDuration?: number;
usedAudioDuration?: number; usedAudioDuration?: number;
hideLimitHint?: boolean;
} }
const buildPreviewUrl = (url: string) => { const buildPreviewUrl = (url: string) => {
if (!url) return ''; if (!url) return '';
if (/^(https?:|data:|blob:)/i.test(url)) return url; if (/^data:|blob:/i.test(url)) return url;
if (/^https?:\/\//i.test(url)) {
const parsed = new URL(url);
if (parsed.pathname.startsWith('/uploads')) {
return parsed.pathname + parsed.search;
}
return url;
}
const base = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, ''); const base = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, '');
return `${base}${url.startsWith('/') ? '' : '/'}${url}`; return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
}; };
const validateImageDimensions = (width: number, height: number): string | null => {
if (width < 300 || width > 6000) return `图片宽度需在 300~6000px 之间,当前为 ${width}px`;
if (height < 300 || height > 6000) return `图片高度需在 300~6000px 之间,当前为 ${height}px`;
const ratio = width / height;
if (ratio < 0.4 || ratio > 2.5) return `图片宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`;
return null;
};
const validateVideoDimensions = (width: number, height: number): string | null => {
if (width < 300 || width > 6000) return `视频宽度需在 300~6000px 之间,当前为 ${width}px`;
if (height < 300 || height > 6000) return `视频高度需在 300~6000px 之间,当前为 ${height}px`;
const ratio = width / height;
if (ratio < 0.4 || ratio > 2.5) return `视频宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`;
const totalPixels = width * height;
if (totalPixels < 409600) return `视频总像素数过小(${width}×${height}=${totalPixels}),需 ≥ 640×640=409600`;
if (totalPixels > 8295044) return `视频总像素数过大(${width}×${height}=${totalPixels}),需 ≤ 3326×2494=8295044`;
return null;
};
const getItemValidationError = (item: UploadResourceHistoryItem): string | null => {
const itemAny = item as any;
const width = itemAny.width || itemAny.videoWidth || itemAny.imageWidth || 0;
const height = itemAny.height || itemAny.videoHeight || itemAny.imageHeight || 0;
if (item.resourceType === 'image') {
if (width > 0 && height > 0) {
return validateImageDimensions(width, height);
}
} else if (item.resourceType === 'video') {
if (width > 0 && height > 0) {
return validateVideoDimensions(width, height);
}
}
return null;
};
const getImageDimensions = (url: string): Promise<{ width: number; height: number }> => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
resolve({ width: img.width, height: img.height });
};
img.onerror = () => {
resolve({ width: 0, height: 0 });
};
img.src = url;
});
};
const typeIcon = (type: string) => { const typeIcon = (type: string) => {
if (type === 'video') return <VideoCameraOutlined />; if (type === 'video') return <VideoCameraOutlined />;
if (type === 'audio') return <AudioOutlined />; if (type === 'audio') return <AudioOutlined />;
@@ -64,6 +121,7 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
usedAudioCount, usedAudioCount,
maxAudioDuration, maxAudioDuration,
usedAudioDuration, usedAudioDuration,
hideLimitHint,
}) => { }) => {
const [resourceType, setResourceType] = useState<MediaTypeFilter>(''); const [resourceType, setResourceType] = useState<MediaTypeFilter>('');
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
@@ -175,11 +233,42 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
if (open) setCheckedMap(new Map()); if (open) setCheckedMap(new Map());
}, [open]); }, [open]);
const toggle = (item: UploadResourceHistoryItem) => { const toggle = async (item: UploadResourceHistoryItem) => {
if (selectedIdSet.has(item.id)) { if (selectedIdSet.has(item.id)) {
message.warning('该素材已经在参考内容中'); message.warning('该素材已经在参考内容中');
return; return;
} }
const itemAny = item as any;
let width = itemAny.width || itemAny.videoWidth || itemAny.imageWidth || 0;
let height = itemAny.height || itemAny.videoHeight || itemAny.imageHeight || 0;
if (item.resourceType === 'image') {
if (width <= 0 || height <= 0) {
const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl);
if (preview) {
const dims = await getImageDimensions(preview);
width = dims.width;
height = dims.height;
}
}
if (width > 0 && height > 0) {
const error = validateImageDimensions(width, height);
if (error) {
message.error(`${item.fileName || '图片'}${error}`);
return;
}
}
} else if (item.resourceType === 'video') {
if (width > 0 && height > 0) {
const error = validateVideoDimensions(width, height);
if (error) {
message.error(`${item.fileName || '视频'}${error}`);
return;
}
}
}
setCheckedMap((prev) => { setCheckedMap((prev) => {
const next = new Map(prev); const next = new Map(prev);
if (next.has(item.id)) next.delete(item.id); if (next.has(item.id)) next.delete(item.id);
@@ -249,19 +338,21 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
/> />
<Button icon={<ReloadOutlined />} onClick={loadGroups}></Button> <Button icon={<ReloadOutlined />} onClick={loadGroups}></Button>
</Space> </Space>
<div style={{ display: 'flex', gap: 16, marginTop: 12, flexWrap: 'wrap' }}> {!hideLimitHint && (
<span style={{ fontSize: 13, color: imageExceeded ? '#ef4444' : '#64748b', fontWeight: imageExceeded ? 600 : 400 }}> <div style={{ display: 'flex', gap: 16, marginTop: 12, flexWrap: 'wrap' }}>
{selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} <span style={{ fontSize: 13, color: imageExceeded ? '#ef4444' : '#64748b', fontWeight: imageExceeded ? 600 : 400 }}>
</span> {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'}
<span style={{ fontSize: 13, color: videoExceeded || durationExceeded ? '#ef4444' : '#64748b', fontWeight: videoExceeded || durationExceeded ? 600 : 400 }}>
{selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} {selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}
</span>
{maxAudioCount !== undefined && (
<span style={{ fontSize: 13, color: audioExceeded || audioDurationExceeded ? '#ef4444' : '#64748b', fontWeight: audioExceeded || audioDurationExceeded ? 600 : 400 }}>
{selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} {selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}
</span> </span>
)} <span style={{ fontSize: 13, color: videoExceeded || durationExceeded ? '#ef4444' : '#64748b', fontWeight: videoExceeded || durationExceeded ? 600 : 400 }}>
</div> {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} {selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}
</span>
{maxAudioCount !== undefined && (
<span style={{ fontSize: 13, color: audioExceeded || audioDurationExceeded ? '#ef4444' : '#64748b', fontWeight: audioExceeded || audioDurationExceeded ? 600 : 400 }}>
{selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} {selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}
</span>
)}
</div>
)}
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '240px 1fr', gap: 16, height: 500 }}> <div style={{ display: 'grid', gridTemplateColumns: '240px 1fr', gap: 16, height: 500 }}>
@@ -310,7 +401,9 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 14 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 14 }}>
{items.map((item) => { {items.map((item) => {
const checked = checkedMap.has(item.id); const checked = checkedMap.has(item.id);
const disabled = selectedIdSet.has(item.id); const selectedDisabled = selectedIdSet.has(item.id);
const validationError = getItemValidationError(item);
const disabled = selectedDisabled || !!validationError;
const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl); const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl);
return ( return (
<div <div
@@ -326,6 +419,7 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
overflow: 'hidden', overflow: 'hidden',
boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)', boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)',
}} }}
title={validationError ? validationError : undefined}
> >
<div style={{ background: '#f1f5f9' }}> <div style={{ background: '#f1f5f9' }}>
{item.resourceType === 'image' ? ( {item.resourceType === 'image' ? (
@@ -340,6 +434,7 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
<Space size={6} style={{ marginBottom: 6 }}> <Space size={6} style={{ marginBottom: 6 }}>
<Tag color="purple" icon={typeIcon(item.resourceType)} style={{ margin: 0 }}>{typeLabel(item.resourceType)}</Tag> <Tag color="purple" icon={typeIcon(item.resourceType)} style={{ margin: 0 }}>{typeLabel(item.resourceType)}</Tag>
{item.durationSeconds ? <Tag style={{ margin: 0 }}>{Number(item.durationSeconds).toFixed(1)}s</Tag> : null} {item.durationSeconds ? <Tag style={{ margin: 0 }}>{Number(item.durationSeconds).toFixed(1)}s</Tag> : null}
{validationError && <Tag color="red" style={{ margin: 0 }}></Tag>}
</Space> </Space>
<Text ellipsis style={{ display: 'block', fontSize: 13, color: '#334155' }} title={item.fileName || item.id}> <Text ellipsis style={{ display: 'block', fontSize: 13, color: '#334155' }} title={item.fileName || item.id}>
{item.fileName || item.id} {item.fileName || item.id}
@@ -352,7 +447,12 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
<CheckOutlined /> <CheckOutlined />
</div> </div>
)} )}
{disabled && <div style={{ position: 'absolute', top: 8, right: 8 }}><Tag></Tag></div>} {selectedDisabled && <div style={{ position: 'absolute', top: 8, right: 8 }}><Tag></Tag></div>}
{validationError && (
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'rgba(239, 68, 68, 0.9)', color: '#fff', padding: '4px 8px', fontSize: 11, textAlign: 'center', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{validationError}
</div>
)}
</div> </div>
); );
})} })}
+12 -6
View File
@@ -978,7 +978,7 @@ const AIChatPage: React.FC = () => {
} }
console.log(mediaReferences); // console.log(mediaReferences);
// 创建用户消息对象 // 创建用户消息对象
const newMessage: Message = { const newMessage: Message = {
@@ -1240,7 +1240,7 @@ const AIChatPage: React.FC = () => {
try { try {
const { width, height } = await getImageDimensions(file); const { width, height } = await getImageDimensions(file);
const error = validateVideoDimensions(width, height); const error = validateImageDimensions(width, height);
if (error) { if (error) {
antdMessage.error(error); antdMessage.error(error);
return false; return false;
@@ -1316,7 +1316,7 @@ const AIChatPage: React.FC = () => {
if (isImage) { if (isImage) {
try { try {
const { width, height } = await getImageDimensions(file); const { width, height } = await getImageDimensions(file);
const error = validateVideoDimensions(width, height); const error = validateImageDimensions(width, height);
if (error) { if (error) {
antdMessage.error(error); antdMessage.error(error);
return false; return false;
@@ -1456,7 +1456,7 @@ const AIChatPage: React.FC = () => {
if (isImage) { if (isImage) {
try { try {
const { width, height } = await getImageDimensions(file); const { width, height } = await getImageDimensions(file);
const error = validateVideoDimensions(width, height); const error = validateImageDimensions(width, height);
if (error) { if (error) {
antdMessage.error(error); antdMessage.error(error);
return false; return false;
@@ -2183,13 +2183,16 @@ const AIChatPage: React.FC = () => {
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setInputValue(msg.originalPrompt || ''); setInputValue(msg.originalPrompt || '');
if (msg.genType) {
setMediaType(msg.genType);
}
if (msg.mediaReferences && msg.mediaReferences.length > 0) { if (msg.mediaReferences && msg.mediaReferences.length > 0) {
const hasFirstLastFrame = msg.mediaReferences.some((ref: any) => ref.role === 'first_frame' || ref.role === 'last_frame'); const hasFirstLastFrame = msg.mediaReferences.some((ref: any) => ref.role === 'first_frame' || ref.role === 'last_frame');
if (hasFirstLastFrame && msg.genType === 'video') { if (hasFirstLastFrame && msg.genType === 'video') {
const first = msg.mediaReferences.find((ref: any) => ref.role === 'first_frame'); const first = msg.mediaReferences.find((ref: any) => ref.role === 'first_frame');
const last = msg.mediaReferences.find((ref: any) => ref.role === 'last_frame'); const last = msg.mediaReferences.find((ref: any) => ref.role === 'last_frame');
setFirstFrame(first ? { ...first, label: first.label || '' } : null); setFirstFrame(first ? { ...first, label: first.label || '', duration: first.duration } : null);
setLastFrame(last ? { ...last, label: last.label || '' } : null); setLastFrame(last ? { ...last, label: last.label || '', duration: last.duration } : null);
setCurrentMedia([]); setCurrentMedia([]);
setReferenceMode('first_last_frame'); setReferenceMode('first_last_frame');
} else { } else {
@@ -2199,6 +2202,7 @@ const AIChatPage: React.FC = () => {
url: ref.url, url: ref.url,
label: ref.label || '', label: ref.label || '',
role: ref.role, role: ref.role,
duration: ref.duration,
}))); })));
setFirstFrame(null); setFirstFrame(null);
setLastFrame(null); setLastFrame(null);
@@ -2887,6 +2891,7 @@ const AIChatPage: React.FC = () => {
usedAudioCount={currentMedia.filter(m => m.type === 'audio').length} usedAudioCount={currentMedia.filter(m => m.type === 'audio').length}
maxAudioDuration={15} maxAudioDuration={15}
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)} usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
hideLimitHint={mediaType === 'image'}
> >
<div <div
style={{ style={{
@@ -3068,6 +3073,7 @@ const AIChatPage: React.FC = () => {
usedAudioCount={currentMedia.filter(m => m.type === 'audio').length} usedAudioCount={currentMedia.filter(m => m.type === 'audio').length}
maxAudioDuration={15} maxAudioDuration={15}
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)} usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
hideLimitHint={mediaType === 'image'}
> >
<div <div
style={{ style={{
+51 -4
View File
@@ -261,6 +261,25 @@ const GeneratePage: React.FC = () => {
const [creditRatios, setCreditRatios] = useState<any>([]); const [creditRatios, setCreditRatios] = useState<any>([]);
const [cimage, setCimage] = useState<any>([]); const [cimage, setCimage] = useState<any>([]);
const validateImageDimensions = (width: number, height: number): string | null => {
if (width < 300 || width > 6000) return `图片宽度需在 300~6000px 之间,当前为 ${width}px`;
if (height < 300 || height > 6000) return `图片高度需在 300~6000px 之间,当前为 ${height}px`;
const ratio = width / height;
if (ratio < 0.4 || ratio > 2.5) return `图片宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`;
return null;
};
const validateVideoDimensions = (width: number, height: number): string | null => {
if (width < 300 || width > 6000) return `视频宽度需在 300~6000px 之间,当前为 ${width}px`;
if (height < 300 || height > 6000) return `视频高度需在 300~6000px 之间,当前为 ${height}px`;
const ratio = width / height;
if (ratio < 0.4 || ratio > 2.5) return `视频宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`;
const totalPixels = width * height;
if (totalPixels < 409600) return `视频总像素数过小(${width}×${height}=${totalPixels}),需 ≥ 640×640=409600`;
if (totalPixels > 8295044) return `视频总像素数过大(${width}×${height}=${totalPixels}),需 ≤ 3326×2494=8295044`;
return null;
};
const handlePasteUpload = async (file: File) => { const handlePasteUpload = async (file: File) => {
const isImage = file.type.startsWith("image/"); const isImage = file.type.startsWith("image/");
const isVideo = file.type.startsWith("video/"); const isVideo = file.type.startsWith("video/");
@@ -299,23 +318,51 @@ const GeneratePage: React.FC = () => {
} }
let fileDuration = 0; let fileDuration = 0;
if (isVideo) { if (isVideo) {
fileDuration = await new Promise<number>((resolve) => { const videoInfo = await new Promise<{ duration: number; width: number; height: number }>((resolve) => {
const video = document.createElement("video"); const video = document.createElement("video");
video.preload = "metadata"; video.preload = "metadata";
video.onloadedmetadata = () => { video.onloadedmetadata = () => {
resolve(video.duration || 0); resolve({ duration: video.duration || 0, width: video.videoWidth || 0, height: video.videoHeight || 0 });
video.remove(); video.remove();
}; };
video.onerror = () => { video.onerror = () => {
resolve(0); resolve({ duration: 0, width: 0, height: 0 });
video.remove(); video.remove();
}; };
video.src = URL.createObjectURL(file); video.src = URL.createObjectURL(file);
}); });
fileDuration = videoInfo.duration;
if (videoInfo.width > 0 && videoInfo.height > 0) {
const error = validateVideoDimensions(videoInfo.width, videoInfo.height);
if (error) {
message.error(`${error}`);
return false;
}
}
if (videoDuration + fileDuration > MAX_VIDEO_DURATION) { if (videoDuration + fileDuration > MAX_VIDEO_DURATION) {
message.error(`视频总时长不能超过${MAX_VIDEO_DURATION}`); message.error(`视频总时长不能超过${MAX_VIDEO_DURATION}`);
return false; return false;
} }
} else {
const imageInfo = await new Promise<{ width: number; height: number }>((resolve) => {
const img = document.createElement('img');
img.onload = () => {
resolve({ width: img.width, height: img.height });
URL.revokeObjectURL(img.src);
};
img.onerror = () => {
resolve({ width: 0, height: 0 });
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(file);
});
if (imageInfo.width > 0 && imageInfo.height > 0) {
const error = validateImageDimensions(imageInfo.width, imageInfo.height);
if (error) {
message.error(`${error}`);
return false;
}
}
} }
setUploading(true); setUploading(true);
try { try {
@@ -1968,7 +2015,7 @@ const GeneratePage: React.FC = () => {
} }
}} }}
rows={3} rows={3}
placeholder="上传参考素材、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..." placeholder="上传参考素材(只用做模型理解,不参与生成)、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..."
maxLength={500} maxLength={500}
bordered={false} bordered={false}
autoSize={{ minRows: 2, maxRows: 6 }} autoSize={{ minRows: 2, maxRows: 6 }}
+10 -10
View File
@@ -299,7 +299,7 @@ const GeneratedRecord: React.FC = () => {
folder?.file(filename, blob); folder?.file(filename, blob);
successCount++; successCount++;
} catch (error) { } catch (error) {
console.warn(`文件下载失败(CORS限制): ${filename},将使用备用方式下载`); // console.warn(`文件下载失败(CORS限制): ${filename},将使用备用方式下载`);
hasError = true; hasError = true;
break; break;
} }
@@ -448,7 +448,7 @@ const GeneratedRecord: React.FC = () => {
const res = await getPreTestList({ page: 1, pageSize: 100 }); const res = await getPreTestList({ page: 1, pageSize: 100 });
setPreTestTemplates(res.data?.data || res.data || []); setPreTestTemplates(res.data?.data || res.data || []);
} catch (error) { } catch (error) {
console.error('加载前测模板失败:', error); // console.error('加载前测模板失败:', error);
} finally { } finally {
setPreTestTemplatesLoading(false); setPreTestTemplatesLoading(false);
} }
@@ -489,7 +489,7 @@ const GeneratedRecord: React.FC = () => {
setOauthList(data || []); setOauthList(data || []);
setOauthTotal(res.pagination.total || 0); setOauthTotal(res.pagination.total || 0);
} catch (error) { } catch (error) {
console.error('加载授权列表失败:', error); // console.error('加载授权列表失败:', error);
setOauthList([]); setOauthList([]);
setOauthTotal(0); setOauthTotal(0);
} finally { } finally {
@@ -513,7 +513,7 @@ const GeneratedRecord: React.FC = () => {
}); });
setOpenTypeMap(map); setOpenTypeMap(map);
} catch (error) { } catch (error) {
console.error('加载历史授权账户列表失败:', error); // console.error('加载历史授权账户列表失败:', error);
setHistoryOAuthList([]); setHistoryOAuthList([]);
setOpenTypeMap({}); setOpenTypeMap({});
} finally { } finally {
@@ -532,7 +532,7 @@ const GeneratedRecord: React.FC = () => {
setUploadHistoryList(data || []); setUploadHistoryList(data || []);
setUploadHistoryTotal(res.pagination?.total || res.total || 0); setUploadHistoryTotal(res.pagination?.total || res.total || 0);
} catch (error) { } catch (error) {
console.error('加载推送历史失败:', error); // console.error('加载推送历史失败:', error);
setUploadHistoryList([]); setUploadHistoryList([]);
setUploadHistoryTotal(0); setUploadHistoryTotal(0);
} finally { } finally {
@@ -669,7 +669,7 @@ const GeneratedRecord: React.FC = () => {
setIsPreTest('2'); setIsPreTest('2');
setPreTestTemplate(''); setPreTestTemplate('');
} catch (error: any) { } catch (error: any) {
console.error('批量推送失败:', error); // console.error('批量推送失败:', error);
message.error(error.message || '批量推送失败'); message.error(error.message || '批量推送失败');
} finally { } finally {
setUploading(false); setUploading(false);
@@ -699,7 +699,7 @@ const GeneratedRecord: React.FC = () => {
}); });
message.success('文件名更新成功'); message.success('文件名更新成功');
} catch (error: any) { } catch (error: any) {
console.error('文件名更新失败:', error); // console.error('文件名更新失败:', error);
message.error(error.message || '文件名更新失败'); message.error(error.message || '文件名更新失败');
} }
}; };
@@ -733,11 +733,11 @@ const GeneratedRecord: React.FC = () => {
}), }),
})); }));
}); });
console.log(response); // console.log(response);
const successCount = response?.successCount || 0; const successCount = response?.successCount || 0;
message.success(`已更新 ${successCount} 个文件名`); message.success(`已更新 ${successCount} 个文件名`);
} catch (error: any) { } catch (error: any) {
console.error('文件名更新失败:', error); // console.error('文件名更新失败:', error);
message.error(error.message || '文件名更新失败'); message.error(error.message || '文件名更新失败');
} }
}; };
@@ -1263,7 +1263,7 @@ const GeneratedRecord: React.FC = () => {
}}> }}>
<Spin <Spin
indicator={<LoadingOutlined style={{ fontSize: 24, color: '#64748b' }} spin />} indicator={<LoadingOutlined style={{ fontSize: 24, color: '#64748b' }} spin />}
tip="加载中..." description="加载中..."
size="large" size="large"
/> />
</div> </div>
+2 -2
View File
@@ -206,7 +206,7 @@ const HomePage: React.FC = () => {
setActiveCaseTab(firstId); setActiveCaseTab(firstId);
// 初始请求第一个 tab 的数据 // 初始请求第一个 tab 的数据
getHomeCaseButton(firstId).then((btnRes: any) => { getHomeCaseButton(firstId).then((btnRes: any) => {
console.log('caseButton:', btnRes); // console.log('caseButton:', btnRes);
if (btnRes?.categories?.[0]?.assets) { if (btnRes?.categories?.[0]?.assets) {
setCaseAssets(btnRes.categories[0].assets); setCaseAssets(btnRes.categories[0].assets);
} }
@@ -357,7 +357,7 @@ const HomePage: React.FC = () => {
{ {
icon: <FileTextOutlined style={{ fontSize: 24 }} />, icon: <FileTextOutlined style={{ fontSize: 24 }} />,
title: '爆款复刻', title: '爆款复刻',
description: '上传参考视频与产品图片,一键复刻爆款视频开头', description: '上传参考视频与产品图片,一键复刻爆款',
action: '立即创作', action: '立即创作',
path: '/initial', path: '/initial',
}, },
+12 -5
View File
@@ -14,6 +14,13 @@ function clonePlain<T>(value: T): T {
return value === undefined ? value : JSON.parse(JSON.stringify(value)); return value === undefined ? value : JSON.parse(JSON.stringify(value));
} }
const buildMediaUrl = (url: string): string => {
if (!url) return '';
if (/^https?:\/\//i.test(url)) return url;
const base = (import.meta.env.VITE_API_BASE || 'http://localhost:8000').replace(/\/$/, '');
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
};
function InitialInfo() { function InitialInfo() {
const navigate = useNavigate(); const navigate = useNavigate();
const { creatID } = useParams<{ creatID: string }>(); const { creatID } = useParams<{ creatID: string }>();
@@ -101,7 +108,7 @@ function InitialInfo() {
}, [previewVisible, previewType]); }, [previewVisible, previewType]);
const openPreview = (url: string, type: 'image' | 'video') => { const openPreview = (url: string, type: 'image' | 'video') => {
console.log(url, type); // console.log(url, type);
setPreviewUrl(url); setPreviewUrl(url);
setPreviewType(type); setPreviewType(type);
setPreviewVisible(true); setPreviewVisible(true);
@@ -587,7 +594,7 @@ function InitialInfo() {
{taskDetail?.material?.materialVideoUrl ? ( {taskDetail?.material?.materialVideoUrl ? (
<video <video
src={taskDetail.material.materialVideoUrl} src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
style={{ width: '100%', height: '100%', objectFit: 'contain' }} style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/> />
) : ( ) : (
@@ -602,7 +609,7 @@ function InitialInfo() {
</span> </span>
<div className="medio_box" style={{ aspectRatio: '1/1', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialImageUrl ? 'pointer' : 'default' }} onClick={() => taskDetail?.material?.materialImageUrl && openPreview(taskDetail.material.materialImageUrl, 'image')}> <div className="medio_box" style={{ aspectRatio: '1/1', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialImageUrl ? 'pointer' : 'default' }} onClick={() => taskDetail?.material?.materialImageUrl && openPreview(taskDetail.material.materialImageUrl, 'image')}>
{taskDetail?.material?.materialImageUrl ? ( {taskDetail?.material?.materialImageUrl ? (
<img src={taskDetail.material.materialImageUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> <img src={buildMediaUrl(taskDetail.material.materialImageUrl)} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
) : ( ) : (
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}></div> <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}></div>
)} )}
@@ -721,7 +728,7 @@ function InitialInfo() {
<div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.08)', boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)' }}> <div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.08)', boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)' }}>
{taskDetail?.material?.materialVideoUrl ? ( {taskDetail?.material?.materialVideoUrl ? (
<video <video
src={taskDetail.material.materialVideoUrl} src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
controls controls
/> />
@@ -741,7 +748,7 @@ function InitialInfo() {
<div style={{ height: 180, borderRadius: 10, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div style={{ height: 180, borderRadius: 10, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{taskDetail?.material?.materialImageUrl ? ( {taskDetail?.material?.materialImageUrl ? (
<img <img
src={taskDetail.material.materialImageUrl} src={buildMediaUrl(taskDetail.material.materialImageUrl)}
alt="产品图片" alt="产品图片"
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
/> />
+1 -1
View File
@@ -128,7 +128,7 @@ const ProjectsPage: React.FC = () => {
backgroundClip: 'text', backgroundClip: 'text',
// textAlign: 'center', // textAlign: 'center',
}}> }}>
</h2> </h2>
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}> <p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
{projects.length} · {projects.length} ·
+9 -1
View File
@@ -473,7 +473,7 @@ function RemoveInfo() {
</> </>
) : ( ) : (
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 12 }}> <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 12 }}>
{record.splitStatus === 'failed' ? '切割失败' : '切割中'} {record.splitStatus === 'failed' ? '切割失败' : record.splitStatus === 'pending' ? '等待切割' : '切割中'}
</div> </div>
)} )}
</div> </div>
@@ -486,6 +486,14 @@ function RemoveInfo() {
render: (_: any, record: any) => { render: (_: any, record: any) => {
const splitStatus = record.split_status || record.splitStatus; const splitStatus = record.split_status || record.splitStatus;
if (splitStatus === 'pending') {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 14, color: '#64748b' }}></span>
</div>
);
}
if (splitStatus === 'processing') { if (splitStatus === 'processing') {
return ( return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+11 -4
View File
@@ -14,6 +14,13 @@ function clonePlain<T>(value: T): T {
return value === undefined ? value : JSON.parse(JSON.stringify(value)); return value === undefined ? value : JSON.parse(JSON.stringify(value));
} }
const buildMediaUrl = (url: string): string => {
if (!url) return '';
if (/^https?:\/\//i.test(url)) return url;
const base = (import.meta.env.VITE_API_BASE || 'http://localhost:8000').replace(/\/$/, '');
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
};
function InitialInfo() { function InitialInfo() {
const navigate = useNavigate(); const navigate = useNavigate();
const { creatID } = useParams<{ creatID: string }>(); const { creatID } = useParams<{ creatID: string }>();
@@ -590,7 +597,7 @@ function InitialInfo() {
{taskDetail?.material?.materialVideoUrl ? ( {taskDetail?.material?.materialVideoUrl ? (
<video <video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.material.materialVideoUrl}`} src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
style={{ width: '100%', height: '100%', objectFit: 'contain' }} style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/> />
) : ( ) : (
@@ -605,7 +612,7 @@ function InitialInfo() {
</span> </span>
<div className="medio_box" style={{ aspectRatio: '1/1', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialImageUrl ? 'pointer' : 'default' }} onClick={() => taskDetail?.material?.materialImageUrl && openPreview(taskDetail.material.materialImageUrl, 'image')}> <div className="medio_box" style={{ aspectRatio: '1/1', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialImageUrl ? 'pointer' : 'default' }} onClick={() => taskDetail?.material?.materialImageUrl && openPreview(taskDetail.material.materialImageUrl, 'image')}>
{taskDetail?.material?.materialImageUrl ? ( {taskDetail?.material?.materialImageUrl ? (
<img src={taskDetail.material.materialImageUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> <img src={buildMediaUrl(taskDetail.material.materialImageUrl)} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
) : ( ) : (
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}></div> <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}></div>
)} )}
@@ -721,7 +728,7 @@ function InitialInfo() {
<div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.06)' }}> <div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.06)' }}>
{taskDetail?.material?.materialVideoUrl ? ( {taskDetail?.material?.materialVideoUrl ? (
<video <video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.material.materialVideoUrl}`} src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
controls controls
/> />
@@ -741,7 +748,7 @@ function InitialInfo() {
<div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.06)' }}> <div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.06)' }}>
{taskDetail?.material?.materialImageUrl ? ( {taskDetail?.material?.materialImageUrl ? (
<img <img
src={taskDetail.material.materialImageUrl} src={buildMediaUrl(taskDetail.material.materialImageUrl)}
alt="产品图片" alt="产品图片"
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
/> />
+8
View File
@@ -4,4 +4,12 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: {
proxy: {
'/uploads': {
target: 'http://ceshi.apiforeign.minzhong.cn',
changeOrigin: true,
},
},
},
}) })