ai
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import { Modal, Tooltip } from 'antd';
|
import { Modal, Tooltip, Empty, Input, List, Spin, Tag, Typography, message } from 'antd';
|
||||||
import { HistoryOutlined, UserOutlined, FolderOpenOutlined, PlusOutlined, LoadingOutlined, CheckOutlined, DownOutlined } from '@ant-design/icons';
|
import { HistoryOutlined, UserOutlined, FolderOpenOutlined, PlusOutlined, CheckOutlined, ReloadOutlined, SearchOutlined, PictureOutlined } from '@ant-design/icons';
|
||||||
|
import { getPrivatePortraitProjects, getPrivatePortraitSelectableAssets } from '../api';
|
||||||
|
import type { PrivatePortraitProject, PrivatePortraitSelectableAsset } from '../types';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
interface UploadSelectorProps {
|
interface UploadSelectorProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -48,21 +52,76 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockHistoryData = [];
|
const mockHistoryData = [];
|
||||||
const mockPortraitData = [
|
|
||||||
{ id: 1, url: '/src/assets/homebtn1.png', name: '人像1' },
|
|
||||||
{ id: 2, url: '/src/assets/homebtn1.png', name: '人像2' },
|
|
||||||
{ id: 3, url: '/src/assets/homebtn1.png', name: '人像3' },
|
|
||||||
{ id: 4, url: '/src/assets/homebtn1.png', name: '人像4' },
|
|
||||||
{ id: 5, url: '/src/assets/homebtn1.png', name: '人像5' },
|
|
||||||
{ id: 6, url: '/src/assets/homebtn1.png', name: '人像6' },
|
|
||||||
{ id: 7, url: '/src/assets/homebtn1.png', name: '人像7' },
|
|
||||||
{ id: 8, url: '/src/assets/homebtn1.png', name: '人像8' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const [selectedHistoryItems, setSelectedHistoryItems] = useState<number[]>([]);
|
const [selectedHistoryItems, setSelectedHistoryItems] = useState<number[]>([]);
|
||||||
const [selectedPortraitItems, setSelectedPortraitItems] = useState<number[]>([]);
|
const [selectedPortraitItems, setSelectedPortraitItems] = useState<Map<string, PrivatePortraitSelectableAsset>>(new Map());
|
||||||
const [historyActiveTab, setHistoryActiveTab] = useState<'asset' | 'history'>('asset');
|
const [historyActiveTab, setHistoryActiveTab] = useState<'asset' | 'history'>('asset');
|
||||||
const [portraitExpandedGroups, setPortraitExpandedGroups] = useState<number[]>([1]);
|
|
||||||
|
const [portraitProjects, setPortraitProjects] = useState<PrivatePortraitProject[]>([]);
|
||||||
|
const [portraitProjectId, setPortraitProjectId] = useState<string | undefined>();
|
||||||
|
const [portraitKeyword, setPortraitKeyword] = useState('');
|
||||||
|
const [portraitAssets, setPortraitAssets] = useState<PrivatePortraitSelectableAsset[]>([]);
|
||||||
|
const [loadingPortraitProjects, setLoadingPortraitProjects] = useState(false);
|
||||||
|
const [loadingPortraitAssets, setLoadingPortraitAssets] = useState(false);
|
||||||
|
|
||||||
|
const getPreviewUrl = (url?: string | null) => {
|
||||||
|
if (!url) return '';
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:') || url.startsWith('blob:')) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
const base = (import.meta.env.VITE_API_BASE || 'http://localhost:8000').replace(/\/$/, '');
|
||||||
|
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPortraitProjects = async () => {
|
||||||
|
setLoadingPortraitProjects(true);
|
||||||
|
try {
|
||||||
|
const res = await getPrivatePortraitProjects({ page: 1, pageSize: 100, status: 'active' });
|
||||||
|
setPortraitProjects(res.items || []);
|
||||||
|
if (!portraitProjectId && res.items?.length) {
|
||||||
|
setPortraitProjectId(res.items[0].id);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(err?.message || '加载真人素材项目失败');
|
||||||
|
} finally {
|
||||||
|
setLoadingPortraitProjects(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPortraitAssets = async () => {
|
||||||
|
setLoadingPortraitAssets(true);
|
||||||
|
try {
|
||||||
|
const res = await getPrivatePortraitSelectableAssets({ projectId: portraitProjectId, keyword: portraitKeyword.trim() || undefined, page: 1, pageSize: 100 });
|
||||||
|
setPortraitAssets(res.items || []);
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(err?.message || '加载真人素材失败');
|
||||||
|
} finally {
|
||||||
|
setLoadingPortraitAssets(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!portraitModalVisible) return;
|
||||||
|
setSelectedPortraitItems(new Map());
|
||||||
|
loadPortraitProjects();
|
||||||
|
}, [portraitModalVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!portraitModalVisible) return;
|
||||||
|
loadPortraitAssets();
|
||||||
|
}, [portraitModalVisible, portraitProjectId]);
|
||||||
|
|
||||||
|
const togglePortraitAsset = (asset: PrivatePortraitSelectableAsset) => {
|
||||||
|
setSelectedPortraitItems((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
if (next.has(asset.id)) {
|
||||||
|
next.delete(asset.id);
|
||||||
|
} else {
|
||||||
|
next.set(asset.id, asset);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const toggleHistoryItem = (id: number) => {
|
const toggleHistoryItem = (id: number) => {
|
||||||
setSelectedHistoryItems(prev =>
|
setSelectedHistoryItems(prev =>
|
||||||
@@ -70,18 +129,6 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePortraitItem = (id: number) => {
|
|
||||||
setSelectedPortraitItems(prev =>
|
|
||||||
prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglePortraitGroup = (groupId: number) => {
|
|
||||||
setPortraitExpandedGroups(prev =>
|
|
||||||
prev.includes(groupId) ? prev.filter(id => id !== groupId) : [...prev, groupId]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmHistorySelection = () => {
|
const confirmHistorySelection = () => {
|
||||||
const items = mockHistoryData.filter(item => selectedHistoryItems.includes(item.id));
|
const items = mockHistoryData.filter(item => selectedHistoryItems.includes(item.id));
|
||||||
onHistorySelect?.(items);
|
onHistorySelect?.(items);
|
||||||
@@ -91,14 +138,18 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmPortraitSelection = () => {
|
const confirmPortraitSelection = () => {
|
||||||
const items = mockPortraitData.filter(item => selectedPortraitItems.includes(item.id));
|
const selected = Array.from(selectedPortraitItems.values());
|
||||||
const transformedItems = items.map(item => ({
|
if (!selected.length) {
|
||||||
|
message.warning('请选择至少一个真人素材');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const transformedItems = selected.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
avatar: item.url,
|
avatar: getPreviewUrl(item.previewUrl),
|
||||||
}));
|
}));
|
||||||
onPortraitSelect?.(transformedItems);
|
onPortraitSelect?.(transformedItems);
|
||||||
setPortraitModalVisible(false);
|
setPortraitModalVisible(false);
|
||||||
setSelectedPortraitItems([]);
|
setSelectedPortraitItems(new Map());
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,12 +186,6 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const portraitGroups = [
|
|
||||||
{ id: 1, name: '111', items: mockPortraitData.slice(0, 4) },
|
|
||||||
{ id: 2, name: '222', items: mockPortraitData.slice(4, 6) },
|
|
||||||
{ id: 3, name: '333', items: mockPortraitData.slice(6, 8) },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
@@ -336,170 +381,193 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="真人人像库"
|
title="选择真人素材库"
|
||||||
open={portraitModalVisible}
|
open={portraitModalVisible}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setPortraitModalVisible(false);
|
setPortraitModalVisible(false);
|
||||||
setSelectedPortraitItems([]);
|
setSelectedPortraitItems(new Map());
|
||||||
}}
|
}}
|
||||||
footer={null}
|
width={920}
|
||||||
width={"80%"}
|
|
||||||
centered
|
centered
|
||||||
styles={{
|
footer={[
|
||||||
body: { padding: 0, position: "relative", overflow: 'auto' },
|
<button
|
||||||
}}
|
key="cancel"
|
||||||
|
onClick={() => {
|
||||||
|
setPortraitModalVisible(false);
|
||||||
|
setSelectedPortraitItems(new Map());
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: '8px 24px',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid #e2e8f0',
|
||||||
|
background: '#fff',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#64748b',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = '#cbd5e1';
|
||||||
|
e.currentTarget.style.background = '#f8fafc';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = '#e2e8f0';
|
||||||
|
e.currentTarget.style.background = '#fff';
|
||||||
|
}}
|
||||||
|
>取消</button>,
|
||||||
|
<button
|
||||||
|
key="ok"
|
||||||
|
onClick={confirmPortraitSelection}
|
||||||
|
style={{
|
||||||
|
padding: '8px 24px',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: 'none',
|
||||||
|
background: '#8b5cf6',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 600,
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#7c3aed';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = '#8b5cf6';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加选中素材({selectedPortraitItems.size})
|
||||||
|
</button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<div style={{ height: '500px', overflowY: 'auto' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '240px 1fr', gap: 16, minHeight: 480 }}>
|
||||||
|
<div style={{ border: '1px solid #eef0f4', borderRadius: 12, padding: 12, background: '#fafafa' }}>
|
||||||
{portraitGroups.map((group) => (
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12, alignItems: 'center' }}>
|
||||||
<div
|
<Text strong>项目组</Text>
|
||||||
key={group.id}
|
<button
|
||||||
style={{
|
onClick={loadPortraitProjects}
|
||||||
borderRadius: 12,
|
disabled={loadingPortraitProjects}
|
||||||
background: '#f8fafc',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={() => togglePortraitGroup(group.id)}
|
|
||||||
style={{
|
style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: 6,
|
||||||
|
border: '1px solid #e2e8f0',
|
||||||
|
background: '#fff',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#64748b',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
gap: 4,
|
||||||
padding: '12px 16px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.background = '#f1f5f9';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.background = 'transparent';
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 14, fontWeight: 600, color: '#1e293b' }}>
|
<ReloadOutlined style={{ fontSize: 12 }} />
|
||||||
{group.name}
|
</button>
|
||||||
</span>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, color: '#64748b', fontSize: 13 }}>
|
<Spin spinning={loadingPortraitProjects}>
|
||||||
<span>{portraitExpandedGroups.includes(group.id) ? '收起' : '展开'}</span>
|
<List
|
||||||
<DownOutlined
|
dataSource={portraitProjects}
|
||||||
|
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无项目组" /> }}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<List.Item
|
||||||
|
onClick={() => setPortraitProjectId(item.id)}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
cursor: 'pointer',
|
||||||
transform: portraitExpandedGroups.includes(group.id) ? 'rotate(180deg)' : 'rotate(0deg)',
|
padding: '10px 12px',
|
||||||
transition: 'transform 0.2s ease',
|
borderRadius: 10,
|
||||||
|
marginBottom: 6,
|
||||||
|
border: portraitProjectId === item.id ? '1px solid #8b5cf6' : '1px solid transparent',
|
||||||
|
background: portraitProjectId === item.id ? '#f5f3ff' : '#fff',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</div>
|
<div style={{ width: '100%' }}>
|
||||||
</div>
|
<Text strong ellipsis style={{ display: 'block' }}>{item.name}</Text>
|
||||||
{portraitExpandedGroups.includes(group.id) && (
|
<Text type="secondary" style={{ fontSize: 12 }}>Active {item.activeAssetCount || 0}</Text>
|
||||||
<div style={{ padding: '16px', borderTop: '1px solid #e2e8f0' }}>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
</List.Item>
|
||||||
{group.items.map((item) => (
|
)}
|
||||||
<div
|
/>
|
||||||
key={item.id}
|
</Spin>
|
||||||
onClick={() => togglePortraitItem(item.id)}
|
</div>
|
||||||
style={{
|
|
||||||
height: 120,
|
<div>
|
||||||
position: 'relative',
|
<div style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
|
||||||
borderRadius: 8,
|
<Input
|
||||||
overflow: 'hidden',
|
allowClear
|
||||||
cursor: 'pointer',
|
prefix={<SearchOutlined />}
|
||||||
aspectRatio: '1',
|
placeholder="搜索素材名称"
|
||||||
border: selectedPortraitItems.includes(item.id) ? '2px solid #ef4444' : '2px solid transparent',
|
value={portraitKeyword}
|
||||||
transition: 'all 0.2s ease',
|
onChange={(e) => setPortraitKeyword(e.target.value)}
|
||||||
}}
|
onPressEnter={loadPortraitAssets}
|
||||||
>
|
style={{ flex: 1 }}
|
||||||
<img
|
/>
|
||||||
src={item.url}
|
|
||||||
alt={item.name}
|
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
||||||
/>
|
|
||||||
{selectedPortraitItems.includes(item.id) && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 4,
|
|
||||||
right: 4,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: '#ef4444',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckOutlined style={{ fontSize: 12, color: '#fff' }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 16, paddingTop: 16, borderTop: '1px solid #e2e8f0' }}>
|
|
||||||
<div style={{ fontSize: 14, color: '#64748b' }}>
|
|
||||||
已选择 <span style={{ color: '#ef4444', fontWeight: 600 }}>{selectedPortraitItems.length}</span> 个素材
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: 12 }}>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={loadPortraitAssets}
|
||||||
setPortraitModalVisible(false);
|
disabled={loadingPortraitAssets}
|
||||||
setSelectedPortraitItems([]);
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 24px',
|
padding: '8px 16px',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
border: '1px solid #e2e8f0',
|
border: '1px solid #e2e8f0',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#64748b',
|
color: '#64748b',
|
||||||
transition: 'all 0.2s ease',
|
display: 'flex',
|
||||||
}}
|
alignItems: 'center',
|
||||||
onMouseEnter={(e) => {
|
gap: 4,
|
||||||
e.currentTarget.style.borderColor = '#cbd5e1';
|
|
||||||
e.currentTarget.style.background = '#f8fafc';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.borderColor = '#e2e8f0';
|
|
||||||
e.currentTarget.style.background = '#fff';
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
取消
|
<ReloadOutlined />
|
||||||
</button>
|
刷新
|
||||||
<button
|
|
||||||
onClick={confirmPortraitSelection}
|
|
||||||
style={{
|
|
||||||
padding: '8px 24px',
|
|
||||||
borderRadius: 8,
|
|
||||||
border: 'none',
|
|
||||||
background: '#ef4444',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 14,
|
|
||||||
color: '#fff',
|
|
||||||
fontWeight: 600,
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.background = '#dc2626';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.background = '#ef4444';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
确认使用
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<Spin spinning={loadingPortraitAssets}>
|
||||||
|
{portraitAssets.length === 0 ? (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无可选 Active 真人素材" style={{ marginTop: 120 }} />
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 12, maxHeight: 420, overflowY: 'auto', paddingRight: 4 }}>
|
||||||
|
{portraitAssets.map((asset) => {
|
||||||
|
const active = selectedPortraitItems.has(asset.id);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={asset.id}
|
||||||
|
onClick={() => togglePortraitAsset(asset)}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: active ? '2px solid #8b5cf6' : '1px solid #edf0f5',
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: '#fff',
|
||||||
|
boxShadow: active ? '0 8px 20px rgba(139,92,246,0.18)' : '0 4px 12px rgba(15,23,42,0.04)',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ aspectRatio: '1 / 1', background: '#f8fafc', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
{asset.previewUrl ? (
|
||||||
|
<img src={getPreviewUrl(asset.previewUrl)} alt={asset.name || '真人素材'} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||||
|
) : (
|
||||||
|
<PictureOutlined style={{ fontSize: 32, color: '#94a3b8' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{active && (
|
||||||
|
<div style={{ position: 'absolute', top: 8, right: 8, width: 24, height: 24, borderRadius: 12, background: '#8b5cf6', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<CheckOutlined />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ padding: 10 }}>
|
||||||
|
<Text strong ellipsis style={{ display: 'block' }}>{asset.name || '未命名素材'}</Text>
|
||||||
|
<div style={{ display: 'flex', gap: 4, marginTop: 6 }}>
|
||||||
|
<Tag color="green">Active</Tag>
|
||||||
|
<Tag>{asset.projectName}</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2520,21 +2520,8 @@ const AIChatPage: React.FC = () => {
|
|||||||
setCurrentMedia([...currentMedia, ...newMedia]);
|
setCurrentMedia([...currentMedia, ...newMedia]);
|
||||||
message.success(`成功添加${items.length}个历史记录`);
|
message.success(`成功添加${items.length}个历史记录`);
|
||||||
}}
|
}}
|
||||||
onPortraitSelect={async (items) => {
|
onPortraitSelect={(items) => {
|
||||||
const files: File[] = [];
|
handlePrivatePortraitAssetsSelected(items as any);
|
||||||
for (const item of items) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(item.avatar);
|
|
||||||
const blob = await response.blob();
|
|
||||||
const file = new File([blob], item.name, { type: blob.type });
|
|
||||||
files.push(file);
|
|
||||||
} catch {
|
|
||||||
message.error(`图片${item.name}下载失败`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (files.length > 0) {
|
|
||||||
await handleBatchUpload(files);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
uploading={uploading}
|
uploading={uploading}
|
||||||
tooltipTitle={mediaType === 'image'
|
tooltipTitle={mediaType === 'image'
|
||||||
@@ -2588,7 +2575,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</UploadSelector>
|
</UploadSelector>
|
||||||
)}
|
)}
|
||||||
{mediaType === 'video' && currentMedia.length === 0 && (
|
{/* {mediaType === 'video' && currentMedia.length === 0 && (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setPrivateAssetPickerOpen(true)}
|
onClick={() => setPrivateAssetPickerOpen(true)}
|
||||||
@@ -2596,7 +2583,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
真人素材库
|
真人素材库
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{/* 层叠附件展示 - 鼠标移入向右排列展开 */}
|
{/* 层叠附件展示 - 鼠标移入向右排列展开 */}
|
||||||
{currentMedia.length > 0 && (
|
{currentMedia.length > 0 && (
|
||||||
@@ -2713,21 +2700,8 @@ const AIChatPage: React.FC = () => {
|
|||||||
setCurrentMedia([...currentMedia, ...newMedia]);
|
setCurrentMedia([...currentMedia, ...newMedia]);
|
||||||
message.success(`成功添加${items.length}个历史记录`);
|
message.success(`成功添加${items.length}个历史记录`);
|
||||||
}}
|
}}
|
||||||
onPortraitSelect={async (items) => {
|
onPortraitSelect={(items) => {
|
||||||
const files: File[] = [];
|
handlePrivatePortraitAssetsSelected(items as any);
|
||||||
for (const item of items) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(item.avatar);
|
|
||||||
const blob = await response.blob();
|
|
||||||
const file = new File([blob], item.name, { type: blob.type });
|
|
||||||
files.push(file);
|
|
||||||
} catch {
|
|
||||||
message.error(`图片${item.name}下载失败`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (files.length > 0) {
|
|
||||||
await handleBatchUpload(files);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
uploading={uploading}
|
uploading={uploading}
|
||||||
tooltipTitle={mediaType === 'image'
|
tooltipTitle={mediaType === 'image'
|
||||||
@@ -2768,7 +2742,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</UploadSelector>
|
</UploadSelector>
|
||||||
)}
|
)}
|
||||||
{mediaType === 'video' && currentMedia.length > 0 && (
|
{/* {mediaType === 'video' && currentMedia.length > 0 && (
|
||||||
<Tooltip title="选择真人素材库">
|
<Tooltip title="选择真人素材库">
|
||||||
<div
|
<div
|
||||||
onClick={(e) => { e.stopPropagation(); setPrivateAssetPickerOpen(true); }}
|
onClick={(e) => { e.stopPropagation(); setPrivateAssetPickerOpen(true); }}
|
||||||
@@ -2795,7 +2769,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
真
|
真
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1838,17 +1838,18 @@ const GeneratePage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
message.success(`成功添加${items.length}个历史记录`);
|
message.success(`成功添加${items.length}个历史记录`);
|
||||||
}}
|
}}
|
||||||
onPortraitSelect={async (items) => {
|
onPortraitSelect={(items) => {
|
||||||
for (const item of items) {
|
items.forEach((item: any) => {
|
||||||
try {
|
setReferences(prev => [...prev, {
|
||||||
const response = await fetch(item.avatar);
|
url: item.previewUrl || '',
|
||||||
const blob = await response.blob();
|
type: 'image',
|
||||||
const file = new File([blob], item.name, { type: blob.type });
|
name: item.name || '真人素材',
|
||||||
await handlePasteUpload(file);
|
source: 'private_portrait_asset',
|
||||||
} catch {
|
private_asset_id: item.id,
|
||||||
message.error(`图片${item.name}下载失败`);
|
label: '',
|
||||||
}
|
}]);
|
||||||
}
|
});
|
||||||
|
message.success(`已添加 ${items.length} 个真人素材参考`);
|
||||||
}}
|
}}
|
||||||
uploading={uploading}
|
uploading={uploading}
|
||||||
tooltipTitle={`参考内容(${references.length}/10)`}
|
tooltipTitle={`参考内容(${references.length}/10)`}
|
||||||
|
|||||||
@@ -295,9 +295,8 @@
|
|||||||
|
|
||||||
.login-code-btn {
|
.login-code-btn {
|
||||||
height: 48px !important;
|
height: 48px !important;
|
||||||
border-radius: 0 10px 10px 0 !important;
|
border-radius: 10 !important;
|
||||||
border: 1.5px solid #e2e8f0 !important;
|
border: 1.5px solid #e2e8f0 !important;
|
||||||
border-left: none !important;
|
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
min-width: 100px !important;
|
min-width: 100px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user