对接素材列表解决
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user