素材云批量上传
This commit is contained in:
@@ -105,8 +105,6 @@ const App = () => {
|
|||||||
<Route path="removelens/:creatID/removeinfo" element={<RemoveInfo />} />
|
<Route path="removelens/:creatID/removeinfo" element={<RemoveInfo />} />
|
||||||
<Route path="removelens/:creatID/removefenbu" element={<RemoveRw />} />
|
<Route path="removelens/:creatID/removefenbu" element={<RemoveRw />} />
|
||||||
{/* <Route path="removelens/:creatID/removefenbu" element={<RemoveFenbu />} /> */}
|
{/* <Route path="removelens/:creatID/removefenbu" element={<RemoveFenbu />} /> */}
|
||||||
|
|
||||||
|
|
||||||
<Route path="generated" element={<GeneratedRecord />} />
|
<Route path="generated" element={<GeneratedRecord />} />
|
||||||
<Route path="pretest" element={<PreTest />} />
|
<Route path="pretest" element={<PreTest />} />
|
||||||
<Route path="authorization" element={<AuthorizationPage />} />
|
<Route path="authorization" element={<AuthorizationPage />} />
|
||||||
@@ -120,4 +118,4 @@ const App = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -16,6 +16,27 @@ const OPEN_TYPE_MAP: Record<number, string> = {
|
|||||||
10: '腾讯营销K3',
|
10: '腾讯营销K3',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PORT_TYPE_MAP: Record<number, string> = {
|
||||||
|
1: '巨量',
|
||||||
|
2: '磁力',
|
||||||
|
3: '巨量星图',
|
||||||
|
4: '服务单',
|
||||||
|
5: '腾讯',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间 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}`;
|
||||||
|
};
|
||||||
|
|
||||||
interface AuthorizationData {
|
interface AuthorizationData {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -136,84 +157,109 @@ const AuthorizationPage: React.FC = () => {
|
|||||||
setSelectedOpenType(undefined);
|
setSelectedOpenType(undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// account_id
|
|
||||||
// :
|
|
||||||
// "1836691333531724"
|
|
||||||
// account_name
|
|
||||||
// :
|
|
||||||
// "BP-1836691333531724"
|
|
||||||
// account_role
|
|
||||||
// :
|
|
||||||
// "CUSTOMER_ADMIN"
|
|
||||||
// account_userid
|
|
||||||
// :
|
|
||||||
// null
|
|
||||||
// account_username
|
|
||||||
// :
|
|
||||||
// "a***3@minzhong.cn"
|
|
||||||
// appid
|
|
||||||
// :
|
|
||||||
// "1866261617455433"
|
|
||||||
// created_at
|
|
||||||
// :
|
|
||||||
// "2026-06-15T10:00:22.734565Z"
|
|
||||||
// id
|
|
||||||
// :
|
|
||||||
// "0019ecab9b8bc57d964"
|
|
||||||
// material_auth_status
|
|
||||||
// :
|
|
||||||
// true
|
|
||||||
// open_type
|
|
||||||
// :
|
|
||||||
// 2
|
|
||||||
// port_type
|
|
||||||
// :
|
|
||||||
// 1
|
|
||||||
// updated_at
|
|
||||||
// :
|
|
||||||
// "2026-06-17T06:41:33.012018Z"
|
|
||||||
// user_id
|
|
||||||
// :
|
|
||||||
// "0019e96d23e57a808e2"
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: '序号',
|
title: 'ID',
|
||||||
dataIndex: 'index',
|
|
||||||
key: 'index',
|
|
||||||
width: 80,
|
|
||||||
render: (text: number) => <span style={{ color: '#94a3b8' }}>{text}</span>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '授权ID',
|
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
key: 'id',
|
key: 'id',
|
||||||
ellipsis: true,
|
},
|
||||||
render: (text: string) => (
|
{
|
||||||
<span style={{ fontWeight: 500, color: '#1e293b' }}>{text}</span>
|
title: '授权账户ID',
|
||||||
|
dataIndex: 'accountId',
|
||||||
|
key: 'accountId',
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户名称',
|
||||||
|
dataIndex: 'accountName',
|
||||||
|
key: 'accountName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户角色',
|
||||||
|
dataIndex: 'accountRole',
|
||||||
|
key: 'accountRole',
|
||||||
|
render: (role: string) => {
|
||||||
|
const roleMap: Record<string, string> = {
|
||||||
|
ADVERTISER: '客户',
|
||||||
|
CUSTOMER_ADMIN: '普通版工作台-管理员',
|
||||||
|
CUSTOMER_OPERATOR: '普通版工作台-协作者',
|
||||||
|
AGENT: '代理商',
|
||||||
|
CHILD_AGENT: '二级代理商',
|
||||||
|
PLATFORM_ROLE_STAR: '星图账户',
|
||||||
|
PLATFORM_ROLE_SHOP_ACCOUNT: '抖音店铺账户',
|
||||||
|
PLATFORM_ROLE_QIANCHUAN_AGENT: '千川代理商',
|
||||||
|
PLATFORM_ROLE_STAR_AGENT: '星图代理商',
|
||||||
|
PLATFORM_ROLE_AWEME: '抖音号',
|
||||||
|
PLATFORM_ROLE_STAR_MCN: '星图MCN机构',
|
||||||
|
PLATFORM_ROLE_STAR_ISV: '星图服务商',
|
||||||
|
AGENT_SYSTEM_ACCOUNT: '代理商系统账户',
|
||||||
|
PLATFORM_ROLE_LOCAL_AGENT: '本地推代理商',
|
||||||
|
PLATFORM_ROLE_YUNTU_BRAND_ISV_ADMIN: '云图品牌服务商管理员',
|
||||||
|
PLATFORM_ROLE_LIFE: '抖音来客账户',
|
||||||
|
PLATFORM_ROLE_ENTERPRISE_BP_ADMIN: '升级版工作台管理员',
|
||||||
|
PLATFORM_ROLE_ENTERPRISE_BP_OPERATOR: '升级版工作台协作者',
|
||||||
|
};
|
||||||
|
return roleMap[role] || role;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户用户ID',
|
||||||
|
dataIndex: 'accountUserid',
|
||||||
|
key: 'accountUserid',
|
||||||
|
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户用户名',
|
||||||
|
dataIndex: 'accountUsername',
|
||||||
|
key: 'accountUsername',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权应用ID',
|
||||||
|
dataIndex: 'appid',
|
||||||
|
key: 'appid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否敏感物料授权',
|
||||||
|
dataIndex: 'materialAuthStatus',
|
||||||
|
key: 'materialAuthStatus',
|
||||||
|
width: 100,
|
||||||
|
render: (text: boolean) => (
|
||||||
|
<Tag color={text ? 'green' : 'default'}>
|
||||||
|
{text ? '是' : '否'}
|
||||||
|
</Tag>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '开户方式',
|
title: '开户方式',
|
||||||
dataIndex: 'open_type',
|
dataIndex: 'openType',
|
||||||
key: 'open_type',
|
key: 'openType',
|
||||||
width: 140,
|
|
||||||
render: (text: number) => <span style={{ color: '#1e293b' }}>{OPEN_TYPE_MAP[text] || text}</span>,
|
render: (text: number) => <span style={{ color: '#1e293b' }}>{OPEN_TYPE_MAP[text] || text}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '授权状态',
|
title: '平台端口',
|
||||||
dataIndex: 'status',
|
dataIndex: 'portType',
|
||||||
key: 'status',
|
key: 'portType',
|
||||||
width: 140,
|
ellipsis: true,
|
||||||
render: (text: string) => renderStatus(text),
|
render: (text: number) => <span style={{ color: '#1e293b' }}>{PORT_TYPE_MAP[text] || text}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户id',
|
||||||
|
dataIndex: 'userId',
|
||||||
|
key: 'userId',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '描述',
|
title: '创建时间',
|
||||||
dataIndex: 'description',
|
dataIndex: 'createdAt',
|
||||||
key: 'description',
|
key: 'createdAt',
|
||||||
ellipsis: true,
|
width: 160,
|
||||||
render: (text: string) => <span style={{ color: '#64748b' }}>{text}</span>,
|
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>,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -224,7 +270,7 @@ const AuthorizationPage: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 12, minHeight: '94vh', background: '#f8fafc', borderRadius: 12 }}>
|
<div style={{ minHeight: '94vh' }}>
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ lineHeight: '28px', fontSize: 24, fontWeight: 600, color: '#1e293b', marginBottom: 8, marginTop: 0 }}>
|
<h1 style={{ lineHeight: '28px', fontSize: 24, fontWeight: 600, color: '#1e293b', marginBottom: 8, marginTop: 0 }}>
|
||||||
授权管理
|
授权管理
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react';
|
import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react';
|
||||||
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Upload, Modal, Progress } from 'antd';
|
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Upload, Modal, Progress, Table } from 'antd';
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
FilterOutlined,
|
FilterOutlined,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { gethistory, gethistoryItems } from '../api';
|
import { gethistory, gethistoryItems, getDefaultPreTest, getOAuthList } from '../api';
|
||||||
|
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -39,6 +39,34 @@ const GeneratedRecord: React.FC = () => {
|
|||||||
const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({});
|
const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({});
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
|
// 上传配置弹窗相关状态
|
||||||
|
const [uploadConfigModalVisible, setUploadConfigModalVisible] = useState(false);
|
||||||
|
const [preTestTemplates, setPreTestTemplates] = useState<any[]>([
|
||||||
|
{ id: 'template_001', name: '前测模板A' },
|
||||||
|
{ id: 'template_002', name: '前测模板B' },
|
||||||
|
{ id: 'template_003', name: '前测模板C' },
|
||||||
|
]);
|
||||||
|
const [preTestLoading, setPreTestLoading] = useState(false);
|
||||||
|
const [accountIdList, setAccountIdList] = useState<{
|
||||||
|
accountId: string;
|
||||||
|
authStatus: 'pending' | 'authorized' | 'failed';
|
||||||
|
}[]>([]);
|
||||||
|
const [accountIdInput, setAccountIdInput] = useState('');
|
||||||
|
|
||||||
|
const [oauthList, setOauthList] = useState<any[]>([]);
|
||||||
|
const [oauthLoading, setOauthLoading] = useState(false);
|
||||||
|
const [oauthTotal, setOauthTotal] = useState(0);
|
||||||
|
const [selectedOauthItems, setSelectedOauthItems] = useState<string[]>([]);
|
||||||
|
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 [isSelectionMode, setIsSelectionMode] = useState(false);
|
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||||
@@ -619,23 +647,49 @@ const GeneratedRecord: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBatchUploadSelected = async () => {
|
const handleBatchUploadSelected = () => {
|
||||||
if (selectedItems.size === 0) {
|
if (selectedItems.size === 0) {
|
||||||
message.warning('请先选择要上传的媒体');
|
message.warning('请先选择要上传的媒体');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setAccountIdList([]);
|
||||||
|
setAccountIdInput('');
|
||||||
|
setBatchUploadProgress([]);
|
||||||
|
setUploadConfigModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadOAuthList = async (page: number, pageSize: number) => {
|
||||||
|
setOauthLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getOAuthList({ page, page_size: pageSize });
|
||||||
|
const data = res?.data || res;
|
||||||
|
console.log(res);
|
||||||
|
setOauthList(data|| []);
|
||||||
|
setOauthTotal(res.pagination.total || 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载授权列表失败:', error);
|
||||||
|
setOauthList([]);
|
||||||
|
setOauthTotal(0);
|
||||||
|
} finally {
|
||||||
|
setOauthLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartBatchUpload = async () => {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
const uploadProgressMap: { [key: string]: number } = {};
|
const uploadProgressMap: { [key: string]: { status: 'pending' | 'uploading' | 'success' | 'error'; message: string } } = {};
|
||||||
|
|
||||||
|
for (const itemId of selectedItems) {
|
||||||
|
uploadProgressMap[itemId] = { status: 'pending', message: '' };
|
||||||
|
}
|
||||||
|
setBatchUploadProgress(Object.entries(uploadProgressMap).map(([itemId, value]) => ({ itemId, ...value })));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 遍历所有选中的项
|
|
||||||
for (const itemId of selectedItems) {
|
for (const itemId of selectedItems) {
|
||||||
let item: any = null;
|
let item: any = null;
|
||||||
let mediaUrl = '';
|
let mediaUrl = '';
|
||||||
let mediaType = '';
|
let mediaType = '';
|
||||||
|
|
||||||
// 找到对应的 item
|
|
||||||
for (const group of recordlist) {
|
for (const group of recordlist) {
|
||||||
const found = group.items.find((i: any) => i.id === itemId);
|
const found = group.items.find((i: any) => i.id === itemId);
|
||||||
if (found) {
|
if (found) {
|
||||||
@@ -646,7 +700,6 @@ const GeneratedRecord: React.FC = () => {
|
|||||||
|
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
|
|
||||||
// 根据媒体类型获取 URL
|
|
||||||
if (filterMedia === 'video' && item.videoUrl) {
|
if (filterMedia === 'video' && item.videoUrl) {
|
||||||
mediaUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl}`;
|
mediaUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl}`;
|
||||||
mediaType = 'video';
|
mediaType = 'video';
|
||||||
@@ -657,12 +710,10 @@ const GeneratedRecord: React.FC = () => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化进度
|
uploadProgressMap[itemId] = { status: 'uploading', message: '正在上传...' };
|
||||||
uploadProgressMap[itemId] = 0;
|
setBatchUploadProgress(Object.entries(uploadProgressMap).map(([itemId, value]) => ({ itemId, ...value })));
|
||||||
setUploadProgress({ ...uploadProgressMap });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 下载媒体文件
|
|
||||||
const response = await fetch(mediaUrl);
|
const response = await fetch(mediaUrl);
|
||||||
if (!response.ok) throw new Error('下载失败');
|
if (!response.ok) throw new Error('下载失败');
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
@@ -670,37 +721,39 @@ const GeneratedRecord: React.FC = () => {
|
|||||||
type: mediaType === 'video' ? 'video/mp4' : 'image/jpeg'
|
type: mediaType === 'video' ? 'video/mp4' : 'image/jpeg'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 上传到后台接口
|
for (const accountItem of accountIdList) {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('file', file);
|
form.append('file', file);
|
||||||
form.append('media_type', mediaType);
|
form.append('media_type', mediaType);
|
||||||
form.append('original_id', itemId);
|
form.append('original_id', itemId);
|
||||||
|
form.append('account_id', accountItem.accountId);
|
||||||
|
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const uploadResponse = await fetch(
|
const uploadResponse = await fetch(
|
||||||
`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}/api/generation-records/batch-upload`,
|
`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}/api/generation-records/batch-upload`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
body: form,
|
body: form,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await uploadResponse.json();
|
||||||
|
if (!uploadResponse.ok) {
|
||||||
|
throw new Error(result.message || '上传失败');
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
if (!uploadResponse.ok) throw new Error('上传失败');
|
uploadProgressMap[itemId] = { status: 'success', message: '上传成功' };
|
||||||
|
} catch (error: any) {
|
||||||
// 更新进度
|
|
||||||
uploadProgressMap[itemId] = 100;
|
|
||||||
setUploadProgress({ ...uploadProgressMap });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`上传失败: ${itemId}`, error);
|
console.error(`上传失败: ${itemId}`, error);
|
||||||
uploadProgressMap[itemId] = -1; // 标记失败
|
uploadProgressMap[itemId] = { status: 'error', message: error.message || '上传失败' };
|
||||||
setUploadProgress({ ...uploadProgressMap });
|
|
||||||
}
|
}
|
||||||
|
setBatchUploadProgress(Object.entries(uploadProgressMap).map(([itemId, value]) => ({ itemId, ...value })));
|
||||||
}
|
}
|
||||||
|
|
||||||
const successCount = Object.values(uploadProgressMap).filter(p => p === 100).length;
|
const successCount = Object.values(uploadProgressMap).filter(p => p.status === 'success').length;
|
||||||
const failCount = Object.values(uploadProgressMap).filter(p => p === -1).length;
|
const failCount = Object.values(uploadProgressMap).filter(p => p.status === 'error').length;
|
||||||
|
|
||||||
if (failCount === 0) {
|
if (failCount === 0) {
|
||||||
message.success(`成功上传 ${successCount} 个文件`);
|
message.success(`成功上传 ${successCount} 个文件`);
|
||||||
@@ -708,11 +761,8 @@ const GeneratedRecord: React.FC = () => {
|
|||||||
message.warning(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`);
|
message.warning(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退出选择模式
|
|
||||||
setIsSelectionMode(false);
|
setIsSelectionMode(false);
|
||||||
setSelectedItems(new Set());
|
setSelectedItems(new Set());
|
||||||
setUploadProgress({});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('批量上传失败');
|
message.error('批量上传失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -751,6 +801,22 @@ const GeneratedRecord: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}, [filterType, filterMedia, Pagebreak.page]);
|
}, [filterType, filterMedia, Pagebreak.page]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPreTestLoading(true);
|
||||||
|
getDefaultPreTest().then((res: any) => {
|
||||||
|
const data = res?.data || res;
|
||||||
|
setPreTestTemplates(Array.isArray(data) ? data : []);
|
||||||
|
}).catch(() => {
|
||||||
|
setPreTestTemplates([
|
||||||
|
{ id: 'template_001', name: '前测模板A' },
|
||||||
|
{ id: 'template_002', name: '前测模板B' },
|
||||||
|
{ id: 'template_003', name: '前测模板C' },
|
||||||
|
]);
|
||||||
|
}).finally(() => {
|
||||||
|
setPreTestLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 加载更多
|
// 加载更多
|
||||||
const handleLoadMore = () => {
|
const handleLoadMore = () => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
@@ -1208,6 +1274,322 @@ const GeneratedRecord: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 上传配置弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title="批量上传配置"
|
||||||
|
open={uploadConfigModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setUploadConfigModalVisible(false);
|
||||||
|
setAccountIdList([]);
|
||||||
|
setAccountIdInput('');
|
||||||
|
setSelectedOauthItems([]);
|
||||||
|
setBatchUploadProgress([]);
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
width={900}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '16px 0' }}>
|
||||||
|
<Typography.Text strong style={{ fontSize: 14, color: '#475569', marginBottom: 8, display: 'block' }}>
|
||||||
|
选择授权账户(可多选)
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
value={selectedOauthItems}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedOauthItems(value as string[]);
|
||||||
|
const selectedAccounts = oauthList
|
||||||
|
.filter(item => value.includes(String(item.id)))
|
||||||
|
.map(item => ({ accountId: String(item.accountId), authStatus: 'authorized' as const }));
|
||||||
|
|
||||||
|
const textAccounts = accountIdInput.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line.length > 0)
|
||||||
|
.map(id => ({ accountId: id, authStatus: 'pending' as const }));
|
||||||
|
|
||||||
|
const mergedAccounts = [...selectedAccounts, ...textAccounts];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const finalAccounts = mergedAccounts.filter(a => {
|
||||||
|
if (seen.has(a.accountId)) return false;
|
||||||
|
seen.add(a.accountId);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
setAccountIdList(finalAccounts);
|
||||||
|
}}
|
||||||
|
placeholder="点击选择授权账户"
|
||||||
|
style={{ width: '100%', marginBottom: 16, borderRadius: 8 }}
|
||||||
|
popupRender={() => (
|
||||||
|
<div style={{ padding: 8, width: 800, maxHeight: 500, overflow: 'auto' }}>
|
||||||
|
<Table
|
||||||
|
dataSource={oauthList}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '授权账户ID',
|
||||||
|
dataIndex: 'accountId',
|
||||||
|
key: 'accountId',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户名称',
|
||||||
|
dataIndex: 'accountName',
|
||||||
|
key: 'accountName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户角色',
|
||||||
|
dataIndex: 'accountRole',
|
||||||
|
key: 'accountRole',
|
||||||
|
width: 200,
|
||||||
|
render: (role: string) => {
|
||||||
|
const roleMap: Record<string, string> = {
|
||||||
|
ADVERTISER: '客户',
|
||||||
|
CUSTOMER_ADMIN: '普通版工作台-管理员',
|
||||||
|
CUSTOMER_OPERATOR: '普通版工作台-协作者',
|
||||||
|
AGENT: '代理商',
|
||||||
|
CHILD_AGENT: '二级代理商',
|
||||||
|
PLATFORM_ROLE_STAR: '星图账户',
|
||||||
|
PLATFORM_ROLE_SHOP_ACCOUNT: '抖音店铺账户',
|
||||||
|
PLATFORM_ROLE_QIANCHUAN_AGENT: '千川代理商',
|
||||||
|
PLATFORM_ROLE_STAR_AGENT: '星图代理商',
|
||||||
|
PLATFORM_ROLE_AWEME: '抖音号',
|
||||||
|
PLATFORM_ROLE_STAR_MCN: '星图MCN机构',
|
||||||
|
PLATFORM_ROLE_STAR_ISV: '星图服务商',
|
||||||
|
AGENT_SYSTEM_ACCOUNT: '代理商系统账户',
|
||||||
|
PLATFORM_ROLE_LOCAL_AGENT: '本地推代理商',
|
||||||
|
PLATFORM_ROLE_YUNTU_BRAND_ISV_ADMIN: '云图品牌服务商管理员',
|
||||||
|
PLATFORM_ROLE_LIFE: '抖音来客账户',
|
||||||
|
PLATFORM_ROLE_ENTERPRISE_BP_ADMIN: '升级版工作台管理员',
|
||||||
|
PLATFORM_ROLE_ENTERPRISE_BP_OPERATOR: '升级版工作台协作者',
|
||||||
|
};
|
||||||
|
return roleMap[role] || role;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户用户名',
|
||||||
|
dataIndex: 'accountUsername',
|
||||||
|
key: 'accountUsername',
|
||||||
|
width: 120,
|
||||||
|
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
loading={oauthLoading}
|
||||||
|
pagination={{
|
||||||
|
current: oauthPage,
|
||||||
|
pageSize: oauthPageSize,
|
||||||
|
total: oauthTotal,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条记录`,
|
||||||
|
onChange: (page, size) => {
|
||||||
|
setOauthPage(page);
|
||||||
|
setOauthPageSize(size);
|
||||||
|
loadOAuthList(page, size);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
onRow={(record) => ({
|
||||||
|
onClick: () => {
|
||||||
|
const id = String(record.id);
|
||||||
|
setSelectedOauthItems(prev => {
|
||||||
|
if (prev.includes(id)) {
|
||||||
|
return prev.filter(item => item !== id);
|
||||||
|
}
|
||||||
|
return [...prev, id];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: selectedOauthItems.includes(String(record.id)) ? '#e6f7ff' : undefined,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
open={oauthSelectOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setOauthSelectOpen(open);
|
||||||
|
if (open) {
|
||||||
|
loadOAuthList(1, oauthPageSize);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text strong style={{ fontSize: 14, color: '#475569', marginBottom: 8, display: 'block', marginTop: 16 }}>
|
||||||
|
粘贴账户ID(每行一个)
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<Input.TextArea
|
||||||
|
value={accountIdInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setAccountIdInput(value);
|
||||||
|
|
||||||
|
const ids = value.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line.length > 0);
|
||||||
|
|
||||||
|
const uniqueIds = [...new Set(ids)];
|
||||||
|
const textAccounts = uniqueIds.map(id => ({ accountId: id, authStatus: 'pending' as const }));
|
||||||
|
|
||||||
|
const oauthAccounts = oauthList
|
||||||
|
.filter(item => selectedOauthItems.includes(String(item.id)))
|
||||||
|
.map(item => ({ accountId: String(item.accountId), authStatus: 'authorized' as const }));
|
||||||
|
|
||||||
|
const mergedAccounts = [...oauthAccounts, ...textAccounts];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const finalAccounts = mergedAccounts.filter(a => {
|
||||||
|
if (seen.has(a.accountId)) return false;
|
||||||
|
seen.add(a.accountId);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
setAccountIdList(finalAccounts);
|
||||||
|
}}
|
||||||
|
placeholder="粘贴账户ID,每行一个,例如:
|
||||||
|
10001
|
||||||
|
10002
|
||||||
|
10003"
|
||||||
|
rows={4}
|
||||||
|
style={{ borderRadius: 8, marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{accountIdList.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography.Text style={{ fontSize: 13, color: '#64748b', marginBottom: 8, display: 'block' }}>
|
||||||
|
已导入 {accountIdList.length} 个账户
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
dataSource={accountIdList.map((item, index) => ({ ...item, key: index }))}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '账户ID',
|
||||||
|
dataIndex: 'accountId',
|
||||||
|
key: 'accountId',
|
||||||
|
width: '70%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权状态',
|
||||||
|
dataIndex: 'authStatus',
|
||||||
|
key: 'authStatus',
|
||||||
|
width: '30%',
|
||||||
|
render: (status: 'pending' | 'authorized' | 'failed') => {
|
||||||
|
if (status === 'authorized') {
|
||||||
|
return <Tag color="success" style={{ borderRadius: 4 }}>已授权</Tag>;
|
||||||
|
}
|
||||||
|
if (status === 'failed') {
|
||||||
|
return <Tag color="error" style={{ borderRadius: 4 }}>授权失败</Tag>;
|
||||||
|
}
|
||||||
|
return <Tag color="default" style={{ borderRadius: 4 }}>待授权</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
maxHeight: 300,
|
||||||
|
overflow: 'auto',
|
||||||
|
border: '1px solid #e8e8e8',
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
onClick={() => {
|
||||||
|
setAccountIdList([]);
|
||||||
|
setAccountIdInput('');
|
||||||
|
setSelectedOauthItems([]);
|
||||||
|
}}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
>
|
||||||
|
清空账户列表
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 上传进度区域 */}
|
||||||
|
{batchUploadProgress.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 16,
|
||||||
|
padding: 12,
|
||||||
|
border: '1px solid #e8e8e8',
|
||||||
|
borderRadius: 8,
|
||||||
|
maxHeight: 200,
|
||||||
|
overflowY: 'auto',
|
||||||
|
}}>
|
||||||
|
<Typography.Text strong style={{ fontSize: 13, color: '#475569', marginBottom: 8, display: 'block' }}>
|
||||||
|
上传进度 ({batchUploadProgress.filter(p => p.status === 'success').length}/{batchUploadProgress.length})
|
||||||
|
</Typography.Text>
|
||||||
|
{batchUploadProgress.map((progress) => (
|
||||||
|
<div
|
||||||
|
key={progress.itemId}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
padding: 8,
|
||||||
|
marginBottom: 6,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: progress.status === 'error' ? '#fef2f2' : progress.status === 'success' ? '#f0fdf4' : '#f9fafb',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: 16, height: 16, borderRadius: 50, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
{progress.status === 'success' && <span style={{ color: '#10b981', fontSize: 12 }}>✓</span>}
|
||||||
|
{progress.status === 'error' && <span style={{ color: '#ef4444', fontSize: 12 }}>✗</span>}
|
||||||
|
{progress.status === 'uploading' && <span style={{ color: '#6366f1', fontSize: 12 }}>●</span>}
|
||||||
|
{progress.status === 'pending' && <span style={{ color: '#9ca3af', fontSize: 12 }}>○</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 12, color: '#374151' }}>
|
||||||
|
{progress.itemId}
|
||||||
|
</div>
|
||||||
|
{progress.message && (
|
||||||
|
<div style={{ fontSize: 11, color: progress.status === 'error' ? '#ef4444' : '#64748b', marginTop: 2 }}>
|
||||||
|
{progress.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 12,
|
||||||
|
marginTop: 16,
|
||||||
|
paddingTop: 16,
|
||||||
|
borderTop: '1px solid #f0f0f0',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setUploadConfigModalVisible(false);
|
||||||
|
setAccountIdList([]);
|
||||||
|
setAccountIdInput('');
|
||||||
|
setSelectedOauthItems([]);
|
||||||
|
setBatchUploadProgress([]);
|
||||||
|
}}
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleStartBatchUpload}
|
||||||
|
loading={uploading}
|
||||||
|
disabled={uploading || accountIdList.length === 0}
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
>
|
||||||
|
{uploading ? '上传中...' : '开始上传'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* 预览弹窗 */}
|
{/* 预览弹窗 */}
|
||||||
{previewVisible && previewItem && (
|
{previewVisible && previewItem && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ const PreTest: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ minHeight: 'calc(100vh - 80px)', background: '#f1f5f9', padding: 12, borderRadius: 12 }}>
|
<div style={{ minHeight: 'calc(100vh - 80px)' }}>
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ lineHeight: '32px', fontSize: 26, fontWeight: 700, color: '#1e293b', marginBottom: 4, marginTop: 0 }}>
|
<h1 style={{ lineHeight: '32px', fontSize: 26, fontWeight: 700, color: '#1e293b', marginBottom: 4, marginTop: 0 }}>
|
||||||
前测管理
|
前测管理
|
||||||
|
|||||||
Reference in New Issue
Block a user