对接素材列表解决

This commit is contained in:
Lrd
2026-06-25 09:34:40 +08:00
parent 8aafd7de32
commit bdb5ce891c
12 changed files with 1637 additions and 186 deletions
+290 -54
View File
@@ -1,8 +1,7 @@
import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react';
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Upload, Modal, Progress, Table, DatePicker } from 'antd';
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Modal, Table, DatePicker } from 'antd';
import dayjs from 'dayjs';
import {
SearchOutlined,
FilterOutlined,
VideoCameraOutlined,
PictureOutlined,
@@ -14,7 +13,7 @@ import {
UploadOutlined,
} from '@ant-design/icons';
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, getUploadHistory } from '../api';
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory } from '../api';
const { Search } = Input;
const { Text } = Typography;
@@ -48,16 +47,13 @@ const GeneratedRecord: React.FC = () => {
const [oauthLoading, setOauthLoading] = useState(false);
const [oauthTotal, setOauthTotal] = useState(0);
const [selectedOauthItems, setSelectedOauthItems] = useState<{ value: string; label: string } | undefined>(undefined);
const [materialFileNames, setMaterialFileNames] = useState<Map<string, string>>(new Map());
const [unifiedFileName, setUnifiedFileName] = useState('');
const updateFilenameDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [oauthPage, setOauthPage] = useState(1);
const [oauthPageSize, setOauthPageSize] = useState(10);
const [oauthSelectOpen, setOauthSelectOpen] = useState(false);
const [batchUploadProgress, setBatchUploadProgress] = useState<{
itemId: string;
status: 'pending' | 'uploading' | 'success' | 'error';
message: string;
}[]>([]);
// 上传任务历史弹窗相关状态
const [uploadHistoryModalVisible, setUploadHistoryModalVisible] = useState(false);
const [uploadHistoryList, setUploadHistoryList] = useState<any[]>([]);
@@ -501,7 +497,7 @@ const GeneratedRecord: React.FC = () => {
}}
onClick={(e) => {
e.stopPropagation();
onToggleSelect?.(item.id);
onToggleSelect?.(item.generatedResourceId || item.id);
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.1)';
@@ -571,6 +567,16 @@ const GeneratedRecord: React.FC = () => {
setPreviewVisible(false);
};
// 获取item的资源ID(优先使用generatedResourceId,否则使用id
const getItemResourceId = (item: any): string => {
return item.generatedResourceId || item.id;
};
// 判断item是否有generatedResourceId
const hasGeneratedResourceId = (item: any): boolean => {
return Boolean(item.generatedResourceId);
};
// 多选相关函数
const handleToggleSelect = (itemId: string) => {
setSelectedItems(prev => {
@@ -586,7 +592,7 @@ const GeneratedRecord: React.FC = () => {
const handleSelectAll = () => {
const allItemIds = recordlist.flatMap((group: any) =>
group.items.map((item: any) => item.id)
group.items.map((item: any) => getItemResourceId(item))
);
if (selectedItems.size === allItemIds.length) {
setSelectedItems(new Set());
@@ -602,7 +608,6 @@ const GeneratedRecord: React.FC = () => {
}
setAccountIdList([]);
setAccountIdInput('');
setBatchUploadProgress([]);
setUploadConfigModalVisible(true);
};
@@ -660,6 +665,7 @@ const GeneratedRecord: React.FC = () => {
loadUploadHistory();
};
// 批量上传素材
const handleStartBatchUpload = async () => {
if (!selectedOauthItems) {
message.warning('请先选择授权账户');
@@ -675,23 +681,46 @@ const GeneratedRecord: React.FC = () => {
advertiser_ids: string[];
resource_ids: string[];
oauth_id: string;
type: string;
source_model: string;
}[] = [];
const advertiserIds = accountIdList.map(account => account.accountId);
const type = filterType === 'project' ? 'generation_record' : 'chat_task';
// 创建itemId到item对象的映射
const itemMap = new Map<string, any>();
recordlist.forEach((group: any) => {
group.items.forEach((item: any) => {
const resourceId = getItemResourceId(item);
itemMap.set(resourceId, item);
});
});
for (const itemId of selectedItems) {
const item = itemMap.get(itemId);
// 根据item是否有generatedResourceId来决定source_model
let sourceModel: string;
if (item && hasGeneratedResourceId(item)) {
sourceModel = 'generated_resources';
} else {
sourceModel = filterType === 'project' ? 'generation_records' : 'chat_generation_tasks';
}
tasks.push({
advertiser_ids: advertiserIds,
resource_ids: [itemId],
oauth_id: selectedOauthItems.value,
type,
source_model: sourceModel,
});
}
// console.log(tasks);
await asyncBatchUploadMaterial({ tasks });
message.success(`已提交 ${tasks.length} 个上传任务,后台异步处理中`);
setIsSelectionMode(false);
setSelectedItems(new Set());
// 关闭弹窗并清理状态
setUploadConfigModalVisible(false);
setAccountIdList([]);
setAccountIdInput('');
setSelectedOauthItems(undefined);
setMaterialFileNames(new Map());
setUnifiedFileName('');
} catch (error: any) {
console.error('批量上传失败:', error);
message.error(error.message || '批量上传失败');
@@ -700,6 +729,73 @@ const GeneratedRecord: React.FC = () => {
}
};
// 更新文件名函数
const handleUpdateFileName = async (sourceId: string, newFileName: string) => {
if (!newFileName.trim()) return;
try {
const response = await updateFilename({
filenames: [{ source_id: sourceId, file_name: newFileName }],
});
// 更新 recordlist 中的文件名,使用 API 返回的 new_file_name
const result = response?.results?.find((r: any) => r.source_id === sourceId);
const actualFileName = result?.new_file_name || newFileName;
setRecordList(prevList => {
return prevList.map(group => ({
...group,
items: group.items.map((item: any) => {
const resourceId = getItemResourceId(item);
if (resourceId === sourceId) {
return { ...item, fileName: actualFileName };
}
return item;
}),
}));
});
message.success('文件名更新成功');
} catch (error: any) {
console.error('文件名更新失败:', error);
message.error(error.message || '文件名更新失败');
}
};
// 批量更新文件名函数
const handleBatchUpdateFileName = async (sourceIds: string[], newFileName: string) => {
if (!newFileName.trim() || sourceIds.length === 0) return;
try {
const filenames = sourceIds.map(sourceId => ({
source_id: sourceId,
file_name: newFileName,
}));
const response = await updateFilename({ filenames });
// 批量更新 recordlist 中的文件名,使用 API 返回的 new_file_name
const resultsMap = new Map<string, string>();
response?.results?.forEach((r: any) => {
if (r.success && r.new_file_name) {
resultsMap.set(r.source_id, r.new_file_name);
}
});
setRecordList(prevList => {
const sourceIdSet = new Set(sourceIds);
return prevList.map(group => ({
...group,
items: group.items.map((item: any) => {
const resourceId = getItemResourceId(item);
if (sourceIdSet.has(resourceId)) {
const actualFileName = resultsMap.get(resourceId) || newFileName;
return { ...item, fileName: actualFileName };
}
return item;
}),
}));
});
const successCount = response?.success_count || 0;
message.success(`已更新 ${successCount} 个文件名`);
} catch (error: any) {
console.error('文件名更新失败:', error);
message.error(error.message || '文件名更新失败');
}
};
// 日期选择器变化处理函数
const handleDateChange = (dateString: string) => {
setSelectedDate(dateString);
@@ -891,7 +987,7 @@ const GeneratedRecord: React.FC = () => {
borderRadius: 8,
background: '#f8f9fc',
border: '1px solid #e2e8f0',
color: '#64748b',
color: '#222222ff',
fontWeight: 600,
}}
>
@@ -919,8 +1015,9 @@ const GeneratedRecord: React.FC = () => {
disabled={uploading || selectedItems.size === 0}
style={{
borderRadius: 8,
background: 'linear-gradient(135deg, #10b981, #059669)',
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
border: 'none',
color: '#fff',
fontWeight: 600,
}}
>
@@ -934,7 +1031,7 @@ const GeneratedRecord: React.FC = () => {
onClick={() => setIsSelectionMode(true)}
style={{
borderRadius: 8,
background: 'linear-gradient(135deg, #10b981, #059669)',
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
border: 'none',
fontWeight: 600,
}}
@@ -949,6 +1046,7 @@ const GeneratedRecord: React.FC = () => {
{/* Second row filter: 视频 / 图片 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
marginBottom: 24,
@@ -1017,7 +1115,7 @@ const GeneratedRecord: React.FC = () => {
onClick={handleOpenUploadHistory}
style={{
borderRadius: 8,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
border: 'none',
color: '#ffffff',
fontWeight: 600,
@@ -1066,11 +1164,11 @@ const GeneratedRecord: React.FC = () => {
}}>
{group.items.map((item: any) => (
<LazyMedia
key={item.id}
key={getItemResourceId(item)}
item={item}
mediaType={filterMedia}
onClick={() => isSelectionMode ? handleToggleSelect(item.id) : handlePreview(item)}
isSelected={selectedItems.has(item.id)}
onClick={() => isSelectionMode ? handleToggleSelect(getItemResourceId(item)) : handlePreview(item)}
isSelected={selectedItems.has(getItemResourceId(item))}
onToggleSelect={handleToggleSelect}
isSelectionMode={isSelectionMode}
/>
@@ -1129,12 +1227,133 @@ const GeneratedRecord: React.FC = () => {
setAccountIdList([]);
setAccountIdInput('');
setSelectedOauthItems(undefined);
setBatchUploadProgress([]);
setMaterialFileNames(new Map());
setUnifiedFileName('');
}}
footer={null}
width={900}
mask={{ closable: false }}
>
<div style={{ padding: '16px 0' }}>
<Typography.Text strong style={{ fontSize: 14, color: '#475569', marginBottom: 8, display: 'block' }}>
({selectedItems.size})
</Typography.Text>
<div style={{ marginBottom: 16 }}>
<div style={{
display: 'flex',
gap: 8,
marginBottom: 12,
alignItems: 'center',
}}>
<Typography.Text style={{ fontSize: 12, color: '#64748b' }}></Typography.Text>
<Input
value={unifiedFileName}
onChange={(e) => setUnifiedFileName(e.target.value)}
placeholder="输入名称后点击应用"
style={{ flex: 1, borderRadius: 8 }}
size="small"
/>
<Button
type="primary"
size="small"
onClick={() => {
if (unifiedFileName.trim() && selectedItems.size > 0) {
handleBatchUpdateFileName(Array.from(selectedItems), unifiedFileName);
}
}}
disabled={!unifiedFileName.trim() || selectedItems.size === 0}
style={{ borderRadius: 8 }}
>
</Button>
</div>
<div style={{
maxHeight: 300,
overflow: 'auto',
border: '1px solid #f0f0f0',
borderRadius: 8,
padding: 12,
}}>
{(() => {
const itemMap = new Map<string, any>();
recordlist.forEach((group: any) => {
group.items.forEach((item: any) => {
const resourceId = getItemResourceId(item);
itemMap.set(resourceId, item);
});
});
return Array.from(selectedItems).map((itemId) => {
const item = itemMap.get(itemId);
return (
<div
key={itemId}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 0',
borderBottom: '1px solid #f5f5f5',
}}
>
<div style={{
width: 60,
height: 40,
borderRadius: 4,
backgroundColor: '#f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
flexShrink: 0,
}}>
{(() => {
const coverField = filterMedia === 'video' ? item?.videoCoverUrl : item?.imageUrl;
if (!coverField) {
return <Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}></Typography.Text>;
}
const baseUrl = import.meta.env.VITE_API_BASE || "http://localhost:8000";
const cleanPath = coverField.startsWith('/') ? coverField.slice(1) : coverField;
const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const coverUrl = `${cleanBase}/static/${cleanPath}&w=300&q=50`;
return (
<img
src={coverUrl}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
);
})()}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Typography.Text style={{ fontSize: 12, color: '#1e293b' }}>
{item?.fileName || `素材 ${item.id}`}
</Typography.Text>
</div>
<Input
value={materialFileNames.get(itemId) || item?.fileName || ''}
onChange={(e) => {
const newName = e.target.value;
const newNames = new Map(materialFileNames);
newNames.set(itemId, newName);
setMaterialFileNames(newNames);
// 防抖调用API
if (updateFilenameDebounceRef.current) {
clearTimeout(updateFilenameDebounceRef.current);
}
updateFilenameDebounceRef.current = setTimeout(() => {
handleUpdateFileName(itemId, newName);
}, 800);
}}
placeholder="输入新名称"
style={{ width: 200, borderRadius: 4 }}
size="small"
/>
</div>
);
});
})()}
</div>
</div>
<Typography.Text strong style={{ fontSize: 14, color: '#475569', marginBottom: 8, display: 'block' }}>
</Typography.Text>
@@ -1281,9 +1500,6 @@ const GeneratedRecord: React.FC = () => {
<div style={{
display: 'flex',
gap: 12,
marginTop: 16,
paddingTop: 16,
borderTop: '1px solid #f0f0f0',
justifyContent: 'flex-end',
}}>
<Button
@@ -1292,7 +1508,8 @@ const GeneratedRecord: React.FC = () => {
setAccountIdList([]);
setAccountIdInput('');
setSelectedOauthItems(undefined);
setBatchUploadProgress([]);
setMaterialFileNames(new Map());
setUnifiedFileName('');
}}
style={{ borderRadius: 8 }}
>
@@ -1317,8 +1534,9 @@ const GeneratedRecord: React.FC = () => {
open={uploadHistoryModalVisible}
onCancel={() => setUploadHistoryModalVisible(false)}
footer={null}
width={800}
width={ 800 }
style={{ borderRadius: 8 }}
mask={{ closable: false }}
>
<div style={{ marginBottom: 16 }}>
<Select
@@ -1346,23 +1564,36 @@ const GeneratedRecord: React.FC = () => {
dataSource={uploadHistoryList}
columns={[
{
title: '任务ID',
dataIndex: 'id',
key: 'id',
width: 150,
},
{
title: '资源ID',
dataIndex: 'resource_id',
key: 'resource_id',
width: 150,
title: '素材名称',
dataIndex: 'fileName',
key: 'fileName',
width: 200,
},
{
title: '账户ID',
dataIndex: 'advertiser_id',
key: 'advertiser_id',
width: 120,
dataIndex: 'advertiserId',
key: 'advertiserId',
width: 180,
},
// {
// title: '状态',
// dataIndex: 'status',
// key: 'status',
// width: 100,
// render: (status: number, record: any) => {
// const statusColorMap: Record<number, string> = {
// 1: '#f59e0b',
// 2: '#6366f1',
// 3: '#10b981',
// 4: '#ef4444',
// };
// return (
// <Tag color={statusColorMap[status] || '#64748b'} style={{ borderRadius: 4 }}>
// {record.status_text || status}
// </Tag>
// );
// },
// },
{
title: '状态',
dataIndex: 'status',
@@ -1388,6 +1619,18 @@ const GeneratedRecord: React.FC = () => {
);
},
},
{
title: '备注',
dataIndex: 'note',
key: 'note',
width: 250,
ellipsis: true,
render: (note: string) => (
<span style={{ color: '#94a3b8' }}>
{note || '-'}
</span>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
@@ -1395,15 +1638,10 @@ const GeneratedRecord: React.FC = () => {
width: 180,
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '更新时间',
dataIndex: 'updated_at',
key: 'updated_at',
width: 180,
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
},
]}
loading={uploadHistoryLoading}
scroll={{ x: 'max-content' }}
pagination={{
current: uploadHistoryPage,
pageSize: uploadHistoryPageSize,
@@ -1412,7 +1650,7 @@ const GeneratedRecord: React.FC = () => {
showTotal: (total) => `${total} 条记录`,
onChange: handleUploadHistoryPageChange,
}}
rowKey={(record, index) => record.id || record.resource_id || index}
rowKey={(record, index) => record.task_id || record.resource_id || index}
size="small"
/>
</Modal>
@@ -1729,7 +1967,7 @@ const GeneratedRecord: React.FC = () => {
</Button>
</div>
<div>
{/* <div>
<Button
style={{ width: '100%', borderRadius: 8, marginTop: 20, color: '#4c49cc' }}
@@ -1737,9 +1975,7 @@ const GeneratedRecord: React.FC = () => {
推送媒体后台
</Button>
</div>
</div> */}
</div>
</div>
</div>