对接素材列表解决

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
+2
View File
@@ -19,6 +19,7 @@ import RemoveLens from './pages/RemoveLens';
import GeneratedRecord from './pages/GeneratedRecord';
import PreTest from './pages/PreTest';
import AuthorizationPage from './pages/AuthorizationPage';
import MaterialListPage from './pages/MaterialListPage';
import RemoveInfo from './pages/RemoveInfo';
import RemoveRw from './pages/RemoveRw';
// import RemoveFenbu from './pages/RemoveFenbu';
@@ -108,6 +109,7 @@ const App = () => {
<Route path="generated" element={<GeneratedRecord />} />
<Route path="pretest" element={<PreTest />} />
<Route path="authorization" element={<AuthorizationPage />} />
<Route path="materials" element={<MaterialListPage />} />
<Route path="consume" element={<ConsumePage />} />
</Route>
<Route path="*" element={<Navigate to="/projects" replace />} />
+50
View File
@@ -616,3 +616,53 @@ export async function getUploadHistory(params: UploadHistoryParams): Promise<any
const query = searchParams.toString();
return api.get(`/upload-material/upload-history${query ? `?${query}` : ''}`);
}
export interface UploadFilenames {
source_id: string;
file_name: string;
}
export interface UpdateFilenameParams {
filenames: UploadFilenames[];
}
// 上传文件名
export async function updateFilename(params: UpdateFilenameParams): Promise<any> {
return api.post('/upload-material/batch-update-filename', params);
}
// 查询素材消耗列表
export interface MaterialConsumpListParams {
advertiser_id?: string;
consume_date?: [string, string];
page?: number;
page_size?: number;
}
export async function getMaterialConsumpList(params: MaterialConsumpListParams): Promise<any> {
const query = new URLSearchParams();
if (params.advertiser_id) query.set('advertiser_id', params.advertiser_id);
if (params.consume_date !== undefined) query.set('consume_date', params.consume_date[0] + ',' + params.consume_date[1]);
if (params.page !== undefined) query.set('page', String(params.page));
if (params.page_size !== undefined) query.set('page_size', String(params.page_size));
return api.get(`/material-consumption/list?${query.toString()}`);
}
// 查询素材消耗列表
export interface ResourcesMaterialListParams {
advertiser_id?: string;
material_id?: string;
upload_id?: string;
file_name?: string;
resource_type?: string; // image或者video
page?: number;
page_size?: number;
}
export async function getResourcesMaterialList(params: ResourcesMaterialListParams): Promise<any> {
const query = new URLSearchParams();
if (params.advertiser_id) query.set('advertiser_id', params.advertiser_id);
if (params.material_id) query.set('material_id', params.material_id);
if (params.upload_id) query.set('upload_id', params.upload_id);
if (params.file_name) query.set('file_name', params.file_name);
if (params.resource_type) query.set('resource_type', params.resource_type);
if (params.page !== undefined) query.set('page', String(params.page));
if (params.page_size !== undefined) query.set('page_size', String(params.page_size));
return api.get(`/resources-material/list?${query.toString()}`);
}
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Button, Table, Tag, Modal, Select, App, Input, Pagination, Typography } from 'antd';
import { PlusOutlined, LockOutlined } from '@ant-design/icons';
import { getOAuthList, juliang_callback, requestOAuth } from '../api';
import { getOAuthList, requestOAuth } from '../api';
const OPEN_TYPE_MAP: Record<number, string> = {
1: '千川',
+156 -55
View File
@@ -1,9 +1,18 @@
import React, { useState, useEffect } from 'react';
import { Table, Tag, Button, Pagination, Typography } from 'antd';
import { Table, Tag, Button, Pagination, Typography, DatePicker, App, Input } from 'antd';
import { ArrowLeftOutlined, DollarOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { getMaterialConsumpList } from '../api';
const { RangePicker } = DatePicker;
// 消耗类型配置
const consumptionTypeConfig: Record<string, { label: string; color: string }> = {
video: { label: '视频生成', color: 'blue' },
audio: { label: '音频转换', color: 'purple' },
image: { label: '图片处理', color: 'green' },
};
// 消耗记录数据类型
interface ConsumptionRecord {
id: string;
authorizationId: string;
@@ -13,62 +22,110 @@ interface ConsumptionRecord {
createdAt: string;
}
// 模拟消耗记录数据
const mockConsumptionRecords: ConsumptionRecord[] = [
{ id: 'C001', authorizationId: '1867060028363785', amount: 100, type: 'video', description: '视频生成消耗', createdAt: '2024-01-15 10:30:00' },
{ id: 'C002', authorizationId: '1867060028363785', amount: 50, type: 'audio', description: '音频转换消耗', createdAt: '2024-01-15 11:20:00' },
{ id: 'C003', authorizationId: '1867059808785418', amount: 200, type: 'video', description: '视频生成消耗', createdAt: '2024-01-14 14:45:00' },
{ id: 'C004', authorizationId: '1867060028363785', amount: 75, type: 'image', description: '图片处理消耗', createdAt: '2024-01-14 09:15:00' },
{ id: 'C005', authorizationId: '1867059757929740', amount: 150, type: 'video', description: '视频生成消耗', createdAt: '2024-01-13 16:00:00' },
];
// 消耗类型配置
const consumptionTypeConfig = {
video: { label: '视频生成', color: 'blue' },
audio: { label: '音频转换', color: 'purple' },
image: { label: '图片处理', color: 'green' },
};
// 表头配置
const columns = [
{ title: '序号', dataIndex: 'index', key: 'index', width: 80, fixed: 'left' as const, render: (text: number) => <span style={{ color: '#94a3b8' }}>{text}</span> },
{ title: '消耗ID', dataIndex: 'id', key: 'id', ellipsis: true, render: (text: string) => <span style={{ fontWeight: 500, color: '#1e293b' }}>{text}</span> },
{ title: '授权ID', dataIndex: 'authorizationId', key: 'authorizationId', ellipsis: true },
{
title: '消耗类型',
dataIndex: 'type',
key: 'type',
width: 120,
render: (text: string) => {
const config = consumptionTypeConfig[text as keyof typeof consumptionTypeConfig];
return <Tag color={config?.color}>{config?.label}</Tag>;
}
},
{
title: '消耗金额',
dataIndex: 'amount',
key: 'amount',
width: 120,
render: (text: number) => <span style={{ color: '#ef4444', fontWeight: 500 }}>{text} </span>
},
{ title: '消耗描述', dataIndex: 'description', key: 'description', ellipsis: true },
{ title: '消耗时间', dataIndex: 'createdAt', key: 'createdAt', width: 160, fixed: 'right' as const },
];
const ConsumePage: React.FC = () => {
const navigate = useNavigate();
const [consumptionRecords, setConsumptionRecords] = useState<ConsumptionRecord[]>(mockConsumptionRecords);
const { message } = App.useApp();
const [consumptionRecords, setConsumptionRecords] = useState<ConsumptionRecord[]>([]);
const [loading, setLoading] = useState(false);
const [listLoading, setListLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [searchParams, setSearchParams] = useState({
advertiser_id: '',
consume_date: undefined as [string, string] | undefined,
});
useEffect(() => {
setLoading(true);
// 模拟异步获取数据
setTimeout(() => {
setConsumptionRecords(mockConsumptionRecords);
setLoading(false);
}, 500);
loadConsumptionList();
}, []);
const loadConsumptionList = async (page = 1, pageSizeNum = 10, params = searchParams) => {
setListLoading(true);
try {
const response = await getMaterialConsumpList({
page,
page_size: pageSizeNum,
advertiser_id: params.advertiser_id || undefined,
consume_date: params.consume_date,
});
if (response) {
if (response.data) {
setConsumptionRecords(response.data.data || response.data);
setTotal(response.pagination?.total || 0);
setCurrentPage(response.pagination?.page || 1);
setPageSize(response.pagination?.pageSize || 10);
} else {
setConsumptionRecords(response.data || response || []);
setTotal(Array.isArray(response) ? response.length : 0);
}
} else {
setConsumptionRecords([]);
setTotal(0);
}
} catch (error) {
message.error('获取消耗列表失败');
console.error('获取消耗列表失败:', error);
} finally {
setListLoading(false);
}
};
// 表头配置
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
fixed: 'left' as const,
render: (text: number) => <span style={{ color: '#94a3b8' }}>{text}</span>,
},
{
title: '消耗ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
render: (text: string) => <span style={{ fontWeight: 500, color: '#1e293b' }}>{text}</span>,
},
{
title: '授权ID',
dataIndex: 'authorizationId',
key: 'authorizationId',
ellipsis: true,
},
{
title: '消耗类型',
dataIndex: 'type',
key: 'type',
width: 120,
render: (text: string) => {
const config = consumptionTypeConfig[text];
return <Tag color={config?.color}>{config?.label || text}</Tag>;
},
},
{
title: '消耗金额',
dataIndex: 'amount',
key: 'amount',
width: 120,
render: (text: number) => <span style={{ color: '#ef4444', fontWeight: 500 }}>{text} </span>,
},
{
title: '消耗描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{
title: '消耗时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
fixed: 'right' as const,
},
];
const tableData = consumptionRecords.map((item, index) => ({
...item,
index: index + 1,
@@ -83,16 +140,54 @@ const ConsumePage: React.FC = () => {
<div style={{ minHeight: '94vh' }}>
{/* 页面标题 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Button type="text" icon={<ArrowLeftOutlined />} onClick={handleBack} />
<DollarOutlined style={{ color: '#6366f1', fontSize: 16 }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
</div>
{/* 搜索筛选 */}
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
<Input
placeholder="授权ID"
value={searchParams.advertiser_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_id: e.target.value }))}
style={{ width: 200 }}
onPressEnter={() => { setCurrentPage(1); loadConsumptionList(1, pageSize); }}
/>
<RangePicker
value={searchParams.consume_date ? [undefined, undefined] : undefined}
onChange={(dates, dateStrings) => {
if (dates && dateStrings[0] && dateStrings[1]) {
setSearchParams(prev => ({ ...prev, consume_date: [dateStrings[0], dateStrings[1]] }));
} else {
setSearchParams(prev => ({ ...prev, consume_date: undefined }));
}
}}
style={{ width: 280 }}
/>
<Button
type="primary"
onClick={() => { setCurrentPage(1); loadConsumptionList(1, pageSize); }}
>
</Button>
<Button
onClick={() => {
setSearchParams({ advertiser_id: '', consume_date: undefined });
setCurrentPage(1);
loadConsumptionList(1, pageSize);
}}
>
</Button>
</div>
{/* 表格 */}
<div style={{ background: '#fff', borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }}>
<Table
dataSource={tableData}
columns={columns}
loading={loading}
loading={listLoading}
pagination={false}
rowKey="id"
bordered={false}
@@ -100,10 +195,16 @@ const ConsumePage: React.FC = () => {
/>
<div style={{ padding: '16px', textAlign: 'right' }}>
<Pagination
pageSize={10}
total={consumptionRecords.length}
current={currentPage}
pageSize={pageSize}
total={total}
showSizeChanger
showTotal={(total) => `${total} 条记录`}
onChange={(page, size) => {
setCurrentPage(page);
setPageSize(size);
loadConsumptionList(page, size);
}}
size="small"
/>
</div>
@@ -112,4 +213,4 @@ const ConsumePage: React.FC = () => {
);
};
export default ConsumePage;
export default ConsumePage;
+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>
@@ -0,0 +1,360 @@
import React, { useEffect, useState } from 'react';
import { Button, Table, Tag, Input, Pagination, Typography, Select, App, Image } from 'antd';
import { FolderOpenOutlined } from '@ant-design/icons';
import { getResourcesMaterialList } from '../api';
// 格式化时间 2026-06-12T03:47:28.542988Z -> 2026-06-12 03:47:28
const formatDateTime = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 安全拼接URL
const buildUrl = (path: string): string => {
if (!path) return '';
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
return `${cleanBase}/${cleanPath}`;
};
// 资源类型配置
const resourceTypeConfig: Record<string, { label: string; color: string }> = {
image: { label: '图片', color: 'green' },
video: { label: '视频', color: 'blue' },
};
// 状态配置
const statusConfig: Record<string, { label: string; color: string }> = {
'1': { label: '待上传', color: 'orange' },
'2': { label: '上传中', color: 'processing' },
'3': { label: '上传成功', color: 'success' },
'4': { label: '上传失败', color: 'error' },
};
interface MaterialResource {
fileName: string;
resourceUrl: string;
remoteUrl: string;
storageType: string;
storagePath: string;
fileSizeBytes: number;
sourceModel: string;
sourceModelModule: string;
sourceId: string;
engineId: string;
engineType: string;
provider: string;
modelName: string;
generatedAt: string;
resourceMonth: string;
createdAt: string;
}
interface MaterialData {
id: string;
oauth_idId: string;
advertiserId: string;
targetTable: string;
targetId: string;
materialId: string;
uploadId: string;
resourceType: string;
userId: string;
taskId: string;
note: string;
status: string;
preResult: string;
preTestTemplateId: string;
createdAt: string;
updatedAt: string;
resource: MaterialResource;
}
const MaterialListPage: React.FC = () => {
const { message } = App.useApp();
const [materials, setMaterials] = useState<MaterialData[]>([]);
const [listLoading, setListLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [searchParams, setSearchParams] = useState({
advertiser_id: '',
material_id: '',
upload_id: '',
file_name: '',
resource_type: undefined as string | undefined,
});
useEffect(() => {
loadMaterialList();
}, []);
const loadMaterialList = async (page = 1, pageSizeNum = 10, params = searchParams) => {
setListLoading(true);
try {
const response = await getResourcesMaterialList({
advertiser_id: params.advertiser_id || undefined,
material_id: params.material_id || undefined,
upload_id: params.upload_id || undefined,
file_name: params.file_name || undefined,
resource_type: params.resource_type,
page,
page_size: pageSizeNum,
});
if (response?.code === 0) {
setMaterials(response.data || []);
setTotal(response.total || 0);
} else {
setMaterials([]);
setTotal(0);
}
} catch (error) {
message.error('获取素材列表失败');
console.error('获取素材列表失败:', error);
} finally {
setListLoading(false);
}
};
const columns = [
// {
// title: 'ID',
// dataIndex: 'id',
// key: 'id',
// width: 100,
// },
{
title: '广告主ID',
dataIndex: 'advertiserId',
key: 'advertiserId',
width: 100,
},
{
title: '素材ID',
dataIndex: 'materialId',
key: 'materialId',
width: 120,
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
},
{
title: '上传平台Id',
dataIndex: 'uploadId',
key: 'uploadId',
width: 120,
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
},
{
title: '资源类型',
dataIndex: 'resourceType',
key: 'resourceType',
width: 100,
render: (text: string) => {
const config = resourceTypeConfig[text];
return <Tag color={config?.color}>{config?.label || text}</Tag>;
},
},
{
title: '预览',
key: 'preview',
width: 80,
render: (_: unknown, record: MaterialData) => {
if (record.resourceType == 'image') {
const imageUrl = record.resource?.resourceUrl ? buildUrl(record.resource.resourceUrl) : '';
if (imageUrl) {
return <Image width={40} height={40} src={imageUrl} style={{ objectFit: 'cover', borderRadius: 4 }} />;
}
}
return <Tag>-</Tag>;
},
},
{
title: '文件名',
dataIndex: ['resource', 'fileName'],
key: 'fileName',
width: 200,
ellipsis: true,
render: (text: string) => <span style={{ color: '#1e293b' }}>{text || '-'}</span>,
},
{
title: '来源模型',
dataIndex: ['resource', 'sourceModel'],
key: 'sourceModel',
width: 120,
ellipsis: true,
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
},
{
title: '供应商',
dataIndex: ['resource', 'provider'],
key: 'provider',
width: 100,
ellipsis: true,
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
},
{
title: '模型名称',
dataIndex: ['resource', 'modelName'],
key: 'modelName',
width: 120,
ellipsis: true,
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
},
{
title: '前测状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
// render: (text: string) => {
// const config = statusConfig[text];
// return <Tag color={config?.color}>{config?.label || text}</Tag>;
// },
},
{
title: '前测结果',
dataIndex: 'preResult',
key: 'preResult',
width: 120,
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
},
{
title: '备注',
dataIndex: 'note',
key: 'note',
width: 150,
ellipsis: true,
render: (text: string) => <span style={{ color: '#94a3b8' }}>{text || '-'}</span>,
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 120,
// ellipsis: true,
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
},
// {
// title: '更新时间',
// dataIndex: 'updatedAt',
// key: 'updatedAt',
// width: 160,
// render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
// },
];
const tableData = materials.map((item, index) => ({
...item,
index: index + 1,
key: item.id,
}));
return (
<div style={{ minHeight: '94vh' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<FolderOpenOutlined style={{ color: '#6366f1', fontSize: 16 }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
</div>
<div style={{ display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
<Input
placeholder="广告主ID"
value={searchParams.advertiser_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_id: e.target.value }))}
style={{ width: 160 }}
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
/>
<Input
placeholder="素材ID"
value={searchParams.material_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, material_id: e.target.value }))}
style={{ width: 160 }}
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
/>
<Input
placeholder="上传ID"
value={searchParams.upload_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, upload_id: e.target.value }))}
style={{ width: 160 }}
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
/>
<Input
placeholder="文件名"
value={searchParams.file_name}
onChange={(e) => setSearchParams(prev => ({ ...prev, file_name: e.target.value }))}
style={{ width: 140 }}
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
/>
<Select
placeholder="资源类型"
value={searchParams.resource_type}
onChange={(value) => setSearchParams(prev => ({ ...prev, resource_type: value }))}
style={{ width: 120 }}
allowClear
options={[
{ value: 'image', label: '图片' },
{ value: 'video', label: '视频' },
]}
/>
<Button
type="primary"
onClick={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
>
</Button>
<Button
onClick={() => {
setSearchParams({ advertiser_id: '', material_id: '', upload_id: '', file_name: '', resource_type: undefined });
setCurrentPage(1);
loadMaterialList(1, pageSize);
}}
>
</Button>
</div>
<div style={{ background: '#fff', borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }}>
<Table
dataSource={tableData}
columns={columns}
loading={listLoading}
pagination={false}
rowKey="id"
bordered={false}
scroll={{ x: 'max-content' }}
/>
<div style={{ padding: '16px', textAlign: 'right' }}>
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
showSizeChanger
showTotal={(total) => `${total} 条记录`}
onChange={(page, size) => {
setCurrentPage(page);
setPageSize(size);
loadMaterialList(page, size);
}}
size="small"
/>
</div>
</div>
</div>
);
};
export default MaterialListPage;