diff --git a/video-gen-admin/.env b/video-gen-admin/.env index 40b79d76..4e72a253 100644 --- a/video-gen-admin/.env +++ b/video-gen-admin/.env @@ -1,5 +1,5 @@ -#VITE_API_BASE=http://localhost:8000 -VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn +VITE_API_BASE=http://192.168.120.17:8000 +# VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn VITE_USE_MOCK=false # Encryption disabled for dev — enable in production VITE_ENCRYPTION_KEY= \ No newline at end of file diff --git a/video-gen-admin/src/App.tsx b/video-gen-admin/src/App.tsx index 1c08d379..7168ed1c 100644 --- a/video-gen-admin/src/App.tsx +++ b/video-gen-admin/src/App.tsx @@ -3,8 +3,11 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { ConfigProvider, App as AntApp, Spin } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import AdminLayout from './pages/AdminLayout'; +import AdminAuthoriz from './pages/AdminAuthoriz'; +import AdminConsume from './pages/AdminConsume'; import AdminLoginPage from './pages/AdminLoginPage'; import AdminDashboard from './pages/AdminDashboard'; +import AdminPlatform from './pages/AdminPlatform'; import AdminUsers from './pages/AdminUsers'; import AdminModels from './pages/AdminModels'; import AdminSettings from './pages/AdminSettings'; @@ -28,6 +31,7 @@ import AdminShotReplications from './pages/AdminShotReplications'; import AdminShotTaskSetDetail from './pages/AdminShotTaskSetDetail'; import AdminReplicationProjectDetail from './pages/AdminReplicationProjectDetail'; import AdminVideoPromptSchemaConfig from './pages/AdminVideoPromptSchemaConfig'; + import { useAdminStore } from './store'; const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { @@ -96,6 +100,9 @@ const App = () => { } /> } /> } /> + } /> + } /> + } /> } /> diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index a1f0c8fa..2296e9c2 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -540,3 +540,29 @@ export async function importVideoPromptSchemaConfig(payload: VideoPromptSchemaCo export async function previewVideoPromptSchemaConfig(payload: VideoPromptSchemaPreviewPayload): Promise { return api.post('/admin/video-prompt-schema-config/preview', payload); } + +// 获取授权链接 +export interface RequestOAuthParams { + open_type: number; +} +export async function requestOAuth(params: RequestOAuthParams): Promise { + return api.post(`/user-oauth/request_oauth`, params); +} + +// 授权列表 +export interface OAuthListParams { + account_userid?: string; + open_type?: number; + account_id?: string; + page?: number; + page_size?: number; +} +export async function getOAuthList(params: OAuthListParams): Promise { + const query = new URLSearchParams(); + if (params.account_userid) query.set('account_userid', params.account_userid); + if (params.open_type !== undefined) query.set('open_type', String(params.open_type)); + if (params.account_id) query.set('account_id', params.account_id); + 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(`/user-oauth/oauth_list?${query.toString()}`); +} diff --git a/video-gen-admin/src/pages/AdminAuthoriz.tsx b/video-gen-admin/src/pages/AdminAuthoriz.tsx new file mode 100644 index 00000000..8ff2ccbe --- /dev/null +++ b/video-gen-admin/src/pages/AdminAuthoriz.tsx @@ -0,0 +1,362 @@ +import React, { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { Button, Card, Space, Table, Tag, Modal, Select, App, Input, Typography } from 'antd'; +import { PlusOutlined, LockOutlined } from '@ant-design/icons'; +import { getOAuthList, requestOAuth } from '../api'; + +const OPEN_TYPE_MAP: Record = { + 1: '千川', + 2: '广告', + 3: '本地推', + 4: '星图', + 5: '快手代理商', + 6: '巨量星图', + 7: '巨量服务单', + 8: '腾讯服务单', + 9: '腾讯营销K2', + 10: '腾讯营销K3', +}; + +const PORT_TYPE_MAP: Record = { + 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 { + id: string; + status: string; + description: string; + account_userid?: string; + open_type?: number; + account_id?: string; +} + +const AuthorizationPage: React.FC = () => { + const { message } = App.useApp(); + const [authorizations, setAuthorizations] = useState([]); + const [loading, setLoading] = useState(false); + const [listLoading, setListLoading] = useState(false); + const [showModal, setShowModal] = useState(false); + const [selectedOpenType, setSelectedOpenType] = useState(undefined); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); + const [searchParams, setSearchParams] = useState({ + account_userid: '', + open_type: undefined as number | undefined, + account_id: '', + }); + + useEffect(() => { + loadOAuthList(); + }, []); + + const loadOAuthList = async (page = 1, pageSize = 10, params = searchParams) => { + setListLoading(true); + try { + const response = await getOAuthList({ + page, + page_size: pageSize, + account_userid: params.account_userid || undefined, + open_type: params.open_type, + account_id: params.account_id || undefined, + }); + if (response) { + if (response.data) { + setAuthorizations(response.data.data || response.data); + } + if (response.pagination) { + setTotal(response.pagination.total || 0); + setCurrentPage(response.pagination.page || 1); + setPageSize(response.pagination.pageSize || 10); + } + } else { + setAuthorizations(response || []); + } + } catch (error) { + message.error('获取授权列表失败'); + } finally { + setListLoading(false); + } + }; + + const handleAuthorize = () => { + setShowModal(true); + }; + + const handleConfirm = async () => { + if (!selectedOpenType) { + message.warning('请选择开户方式'); + return; + } + setLoading(true); + try { + const response = await requestOAuth({ open_type: selectedOpenType }); + if (response.authUrl) { + window.open(response.authUrl, '_blank'); + } else { + message.error('获取授权链接失败'); + } + } catch (error) { + message.error('请求授权失败'); + } finally { + setLoading(false); + setShowModal(false); + setSelectedOpenType(undefined); + } + }; + const columns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + }, + { + title: '授权账户ID', + dataIndex: 'accountId', + key: 'accountId', + width: 160, + }, + { + title: '授权账户名称', + dataIndex: 'accountName', + key: 'accountName', + }, + { + title: '授权账户角色', + dataIndex: 'accountRole', + key: 'accountRole', + render: (role: string) => { + const roleMap: Record = { + 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) => {text || '-'}, + }, + { + title: '授权账户用户名', + dataIndex: 'accountUsername', + key: 'accountUsername', + }, + { + title: '授权应用ID', + dataIndex: 'appid', + key: 'appid', + }, + { + title: '是否敏感物料授权', + dataIndex: 'materialAuthStatus', + key: 'materialAuthStatus', + width: 100, + render: (text: boolean) => ( + + {text ? '是' : '否'} + + ), + }, + { + title: '开户方式', + dataIndex: 'openType', + key: 'openType', + render: (text: number) => {OPEN_TYPE_MAP[text] || text}, + }, + { + title: '平台端口', + dataIndex: 'portType', + key: 'portType', + ellipsis: true, + render: (text: number) => {PORT_TYPE_MAP[text] || text}, + }, + { + title: '用户id', + dataIndex: 'userId', + key: 'userId', + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + width: 160, + render: (text: string) => {formatDateTime(text)}, + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + key: 'updatedAt', + width: 160, + render: (text: string) => {formatDateTime(text)}, + }, + { + title: '操作', + key: 'action', + width: 120, + render: (_: unknown, record: AuthorizationData) => ( + + 查看消耗 + + ), + }, + ]; + + const tableData = authorizations.map((item, index) => ({ + ...item, + index: index + 1, + key: item.id, + })); + + return ( +
+ +
+ + + 授权管理 + + +
+
+ setSearchParams(prev => ({ ...prev, account_userid: e.target.value }))} + style={{ width: 180 }} + onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }} + /> + setSearchParams(prev => ({ ...prev, account_id: e.target.value }))} + style={{ width: 180 }} + onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }} + /> + + +
+ `共 ${t} 条记录`, + onChange: (page, size) => { + setCurrentPage(page); + setPageSize(size); + loadOAuthList(page, size); + }, + size: 'small', + }} + /> + + + { + setShowModal(false); + setSelectedOpenType(undefined); + }} + okText="确认授权" + cancelText="取消" + confirmLoading={loading} + > +
`共 ${t} 条记录`, + size: 'small', + }} + /> + + + { + localStorage.setItem('consumeColumns', JSON.stringify(selectedColumns)); + message.success('表头设置已保存'); + setShowModal(false); + }} + onCancel={() => { + setShowModal(false); + setSearchText(''); + }} + okText="确定" + cancelText="取消" + width={800} + bodyStyle={{ padding: 16 }} + > +
+
+ setSearchText(e.target.value)} + style={{ marginBottom: 8 }} + /> + + 输入指标名称进行搜索 + +
+
+ 任务详情指标 +
+ 0} + indeterminate={selectedColumns.length > 0 && selectedColumns.length < columnsData.length} + onChange={(e) => { + if (e.target.checked) { + setSelectedColumns(columnsData.map(item => item.name)); + } else { + setSelectedColumns([]); + } + }} + style={{ marginBottom: 12 }} + > + 全选 + +
+ {columnsData + .filter(item => + item.description.toLowerCase().includes(searchText.toLowerCase()) || + item.name.toLowerCase().includes(searchText.toLowerCase()) + ) + .map(item => ( + { + if (e.target.checked) { + setSelectedColumns(prev => [...prev, item.name]); + } else { + setSelectedColumns(prev => prev.filter(col => col !== item.name)); + } + }} + > + {item.description} + + ))} +
+
+
+
+
+ + 已添加({selectedColumns.length}) + + + + + +
+
+ {selectedColumns.length === 0 ? ( + + 暂无已选指标 + + ) : ( +
+ {selectedColumns.map((colName, index) => { + const col = columnsData.find(c => c.name === colName); + return ( +
{ + e.dataTransfer.setData('index', String(index)); + }} + onDragOver={(e) => { + e.preventDefault(); + }} + onDrop={(e) => { + const fromIndex = parseInt(e.dataTransfer.getData('index')); + const toIndex = index; + if (fromIndex !== toIndex) { + const newSelected = [...selectedColumns]; + const [removed] = newSelected.splice(fromIndex, 1); + newSelected.splice(toIndex, 0, removed); + setSelectedColumns(newSelected); + } + }} + > + {col?.description || colName} + + {index + 1} + +
+ ); + })} +
+ )} +
+
+
+
+ + ); +}; + +export default ConsumePage; diff --git a/video-gen-admin/src/pages/Adminplatform.tsx b/video-gen-admin/src/pages/Adminplatform.tsx new file mode 100644 index 00000000..1b1494df --- /dev/null +++ b/video-gen-admin/src/pages/Adminplatform.tsx @@ -0,0 +1,157 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, Card, Space, Table, Tag, Typography, message, Modal, Input, Upload, +} from 'antd'; +import { + HistoryOutlined, ReloadOutlined, PlusOutlined, UploadOutlined, +} from '@ant-design/icons'; +import { getOperationLogs } from '../api'; +import { formatDate } from '../utils/formatDate'; + +interface OperationLog { + id: string; + userId: string; + username: string; + action: string; + method: string; + path: string; + detail?: string; + ip?: string; + createdAt: string; +} + +const METHOD_COLORS: Record = { POST: 'green', PUT: 'blue', DELETE: 'red' }; + +const AdminPlatform: React.FC = () => { + const [logs, setLogs] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + const [showModal, setShowModal] = useState(false); + const [formData, setFormData] = useState({ title: '', description: '', image: '' }); + + const load = async (p?: number) => { + setLoading(true); + try { + const res = await getOperationLogs(p || page); + setLogs(res.items || []); + setTotal(res.total || 0); + } catch { + message.error('加载平台管理失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { load(); }, []); + + const columns = [ + { + title: '操作人', dataIndex: 'username', width: 120, + render: (v: string) => {v}, + }, + { + title: '操作', dataIndex: 'action', width: 160, + render: (v: string) => {v}, + }, + { + title: '方法', dataIndex: 'method', width: 80, + render: (v: string) => {v}, + }, + { + title: '路径', dataIndex: 'path', width: 220, ellipsis: true, + render: (v: string) => {v}, + }, + { + title: '时间', dataIndex: 'createdAt', width: 160, + render: (v: string) => {formatDate(v)}, + }, + ]; + + return ( +
+ +
+ + + 平台管理 + + + + + +
+
`共 ${t} 条记录`, + onChange: (p) => { setPage(p); load(p); }, + }} + scroll={{ x: 800 }} + /> + + + { + message.success('新增成功'); + setShowModal(false); + setFormData({ title: '', description: '', image: '' }); + load(); + }} + onCancel={() => { + setShowModal(false); + setFormData({ title: '', description: '', image: '' }); + }} + okText="确认" + cancelText="取消" + > +
+
+ 标题 + setFormData(prev => ({ ...prev, title: e.target.value }))} + /> +
+
+ 描述 + setFormData(prev => ({ ...prev, description: e.target.value }))} + rows={4} + /> +
+
+ 封面图片 + { + if (info.file.status === 'done') { + setFormData(prev => ({ ...prev, image: info.file.response?.url || '' })); + } + }} + > +
+ +
上传图片
+
+
+
+
+
+ + ); +}; + +export default AdminPlatform; diff --git a/video-gen-app/.env b/video-gen-app/.env index ea3cda13..302d39cd 100644 --- a/video-gen-app/.env +++ b/video-gen-app/.env @@ -1,5 +1,5 @@ -#VITE_API_BASE=http://localhost:8000 -VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn +VITE_API_BASE=http://192.168.120.17:8000 +# VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn VITE_USE_MOCK=false # Encryption disabled for dev — enable in production VITE_ENCRYPTION_KEY= diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index b15ffb10..79ff978e 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -577,4 +577,35 @@ export async function getArea(params?: GetAreaParams): Promise { if (params?.parent_code) query.set('parent_code', params.parent_code); return api.get(`/pre-test-template/getArea?${query.toString()}`); } -// + +// ── Upload Material ─────────────────────────────────────── + +export interface AsyncBatchUploadTask { + advertiser_ids: string[]; + resource_ids: string[]; + oauth_id: string; +} + +export interface AsyncBatchUploadParams { + tasks: AsyncBatchUploadTask[]; +} + +export async function asyncBatchUploadMaterial(params: AsyncBatchUploadParams): Promise { + return api.post('/upload-material/async-batch-upload', params); +} + +// 获取上传历史 +// /api/upload-material/upload-history +export interface UploadHistoryParams { + status?: string; + page?: number; + pageSize?: number; +} +export async function getUploadHistory(params: UploadHistoryParams): Promise { + const searchParams = new URLSearchParams(); + if (params.status) searchParams.set('status', params.status); + if (params.page) searchParams.set('page', String(params.page)); + if (params.pageSize) searchParams.set('page_size', String(params.pageSize)); + const query = searchParams.toString(); + return api.get(`/upload-material/upload-history${query ? `?${query}` : ''}`); +} diff --git a/video-gen-app/src/pages/AuthorizationPage.tsx b/video-gen-app/src/pages/AuthorizationPage.tsx index c274477a..570919fd 100644 --- a/video-gen-app/src/pages/AuthorizationPage.tsx +++ b/video-gen-app/src/pages/AuthorizationPage.tsx @@ -65,7 +65,6 @@ const AuthorizationPage: React.FC = () => { useEffect(() => { loadOAuthList(); - handleCallback(); }, []); const loadOAuthList = async (page = 1, pageSize = 10, params = searchParams) => { @@ -97,22 +96,6 @@ const AuthorizationPage: React.FC = () => { } }; - const handleCallback = async () => { - const params = new URLSearchParams(window.location.search); - const authCode = params.get('auth_code'); - const state = params.get('state'); - if (authCode && state) { - try { - await juliang_callback({ auth_code: authCode, state }); - message.success('授权成功'); - window.history.replaceState({}, document.title, window.location.pathname); - loadOAuthList(); - } catch (error) { - message.error('授权回调失败'); - } - } - }; - const handleAuthorize = () => { setShowModal(true); }; diff --git a/video-gen-app/src/pages/GeneratedRecord.tsx b/video-gen-app/src/pages/GeneratedRecord.tsx index eebc84ba..b3d172bd 100644 --- a/video-gen-app/src/pages/GeneratedRecord.tsx +++ b/video-gen-app/src/pages/GeneratedRecord.tsx @@ -12,9 +12,9 @@ import { XOutlined, ClockCircleOutlined, UploadOutlined, - PlusOutlined, + } from '@ant-design/icons'; -import { gethistory, gethistoryItems, getDefaultPreTest, getOAuthList } from '../api'; +import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, getUploadHistory } from '../api'; const { Search } = Input; const { Text } = Typography; @@ -35,30 +35,19 @@ const GeneratedRecord: React.FC = () => { const videoRef = React.createRef(); const [selectedDate, setSelectedDate] = useState(''); - // 批量上传相关状态 - const [uploadModalVisible, setUploadModalVisible] = useState(false); - const [uploadFiles, setUploadFiles] = useState([]); - const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({}); const [uploading, setUploading] = useState(false); // 上传配置弹窗相关状态 const [uploadConfigModalVisible, setUploadConfigModalVisible] = useState(false); - const [preTestTemplates, setPreTestTemplates] = useState([ - { 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([]); const [oauthLoading, setOauthLoading] = useState(false); const [oauthTotal, setOauthTotal] = useState(0); - const [selectedOauthItems, setSelectedOauthItems] = useState([]); + const [selectedOauthItems, setSelectedOauthItems] = useState<{ value: string; label: string } | undefined>(undefined); const [oauthPage, setOauthPage] = useState(1); const [oauthPageSize, setOauthPageSize] = useState(10); const [oauthSelectOpen, setOauthSelectOpen] = useState(false); @@ -69,6 +58,15 @@ const GeneratedRecord: React.FC = () => { message: string; }[]>([]); + // 上传任务历史弹窗相关状态 + const [uploadHistoryModalVisible, setUploadHistoryModalVisible] = useState(false); + const [uploadHistoryList, setUploadHistoryList] = useState([]); + const [uploadHistoryTotal, setUploadHistoryTotal] = useState(0); + const [uploadHistoryPage, setUploadHistoryPage] = useState(1); + const [uploadHistoryPageSize, setUploadHistoryPageSize] = useState(10); + const [uploadHistoryLoading, setUploadHistoryLoading] = useState(false); + const [uploadHistoryStatus, setUploadHistoryStatus] = useState(''); + // 多选相关状态 const [isSelectionMode, setIsSelectionMode] = useState(false); const [selectedItems, setSelectedItems] = useState>(new Set()); @@ -573,58 +571,6 @@ const GeneratedRecord: React.FC = () => { setPreviewVisible(false); }; - // 批量上传相关函数 - const handleUploadChange = (info: any) => { - // 过滤文件类型 - const validFiles = info.fileList.filter((file: any) => { - const type = file.type.toLowerCase(); - return type.startsWith('image/') || type.startsWith('video/'); - }); - - // 检查无效文件并提示 - const invalidFiles = info.fileList.filter((file: any) => { - const type = file.type.toLowerCase(); - return !type.startsWith('image/') && !type.startsWith('video/'); - }); - - if (invalidFiles.length > 0) { - message.warning(`已过滤 ${invalidFiles.length} 个无效文件,仅支持图片和视频`); - } - - setUploadFiles(validFiles); - }; - - const handleRemoveFile = (file: any) => { - setUploadFiles(prev => prev.filter(f => f.uid !== file.uid)); - }; - - const handleStartUpload = async () => { - if (uploadFiles.length === 0) { - message.warning('请先选择要上传的文件'); - return; - } - setUploading(true); - // 模拟批量上传过程 - for (let i = 0; i < uploadFiles.length; i++) { - const file = uploadFiles[i]; - setUploadProgress(prev => ({ ...prev, [file.uid]: 0 })); - - // 模拟上传进度 - for (let progress = 0; progress <= 100; progress += 10) { - await new Promise(resolve => setTimeout(resolve, 100)); - setUploadProgress(prev => ({ ...prev, [file.uid]: progress })); - } - } - // 上传完成 - await new Promise(resolve => setTimeout(resolve, 500)); - message.success(`成功上传 ${uploadFiles.length} 个文件`); - setUploading(false); - setUploadFiles([]); - setUploadModalVisible(false); - // 刷新页面数据 - setPagebreak(prev => ({ ...prev, page: 1 })); - }; - // 多选相关函数 const handleToggleSelect = (itemId: string) => { setSelectedItems(prev => { @@ -665,7 +611,6 @@ const GeneratedRecord: React.FC = () => { 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) { @@ -677,97 +622,79 @@ const GeneratedRecord: React.FC = () => { } }; - const handleStartBatchUpload = async () => { - setUploading(true); - 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 }))); - + const loadUploadHistory = async () => { + setUploadHistoryLoading(true); try { + const res = await getUploadHistory({ + status: uploadHistoryStatus || undefined, + page: uploadHistoryPage, + pageSize: uploadHistoryPageSize, + }); + const data = res?.data || res; + setUploadHistoryList(data || []); + setUploadHistoryTotal(res.pagination?.total || res.total || 0); + } catch (error) { + console.error('加载上传历史失败:', error); + setUploadHistoryList([]); + setUploadHistoryTotal(0); + } finally { + setUploadHistoryLoading(false); + } + }; + + const handleOpenUploadHistory = () => { + setUploadHistoryModalVisible(true); + setUploadHistoryPage(1); + setUploadHistoryStatus(''); + loadUploadHistory(); + }; + + const handleUploadHistorySearch = () => { + setUploadHistoryPage(1); + loadUploadHistory(); + }; + + const handleUploadHistoryPageChange = (page: number, pageSize: number) => { + setUploadHistoryPage(page); + setUploadHistoryPageSize(pageSize); + loadUploadHistory(); + }; + + const handleStartBatchUpload = async () => { + if (!selectedOauthItems) { + message.warning('请先选择授权账户'); + return; + } + if (selectedItems.size === 0) { + message.warning('请先选择要上传的媒体'); + return; + } + setUploading(true); + try { + const tasks: { + advertiser_ids: string[]; + resource_ids: string[]; + oauth_id: string; + type: string; + }[] = []; + const advertiserIds = accountIdList.map(account => account.accountId); + const type = filterType === 'project' ? 'generation_record' : 'chat_task'; for (const itemId of selectedItems) { - let item: any = null; - let mediaUrl = ''; - let mediaType = ''; - - for (const group of recordlist) { - const found = group.items.find((i: any) => i.id === itemId); - if (found) { - item = found; - break; - } - } - - if (!item) continue; - - if (filterMedia === 'video' && item.videoUrl) { - mediaUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl}`; - mediaType = 'video'; - } else if (filterMedia === 'image' && item.imageUrl) { - mediaUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${item.imageUrl}`; - mediaType = 'image'; - } else { - continue; - } - - uploadProgressMap[itemId] = { status: 'uploading', message: '正在上传...' }; - setBatchUploadProgress(Object.entries(uploadProgressMap).map(([itemId, value]) => ({ itemId, ...value }))); - - try { - const response = await fetch(mediaUrl); - if (!response.ok) throw new Error('下载失败'); - const blob = await response.blob(); - const file = new File([blob], item.title || `media_${itemId}`, { - type: mediaType === 'video' ? 'video/mp4' : 'image/jpeg' - }); - - for (const accountItem of accountIdList) { - const form = new FormData(); - form.append('file', file); - form.append('media_type', mediaType); - form.append('original_id', itemId); - form.append('account_id', accountItem.accountId); - - const token = localStorage.getItem('auth_token'); - const uploadResponse = await fetch( - `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}/api/generation-records/batch-upload`, - { - method: 'POST', - headers: token ? { Authorization: `Bearer ${token}` } : {}, - body: form, - } - ); - - const result = await uploadResponse.json(); - if (!uploadResponse.ok) { - throw new Error(result.message || '上传失败'); - } - } - - uploadProgressMap[itemId] = { status: 'success', message: '上传成功' }; - } catch (error: any) { - console.error(`上传失败: ${itemId}`, error); - uploadProgressMap[itemId] = { status: 'error', message: error.message || '上传失败' }; - } - setBatchUploadProgress(Object.entries(uploadProgressMap).map(([itemId, value]) => ({ itemId, ...value }))); + tasks.push({ + advertiser_ids: advertiserIds, + resource_ids: [itemId], + oauth_id: selectedOauthItems.value, + type, + }); } - - const successCount = Object.values(uploadProgressMap).filter(p => p.status === 'success').length; - const failCount = Object.values(uploadProgressMap).filter(p => p.status === 'error').length; - - if (failCount === 0) { - message.success(`成功上传 ${successCount} 个文件`); - } else { - message.warning(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`); - } - + // console.log(tasks); + await asyncBatchUploadMaterial({ tasks }); + message.success(`已提交 ${tasks.length} 个上传任务,后台异步处理中`); setIsSelectionMode(false); setSelectedItems(new Set()); - } catch (error) { - message.error('批量上传失败'); - console.error(error); + } catch (error: any) { + console.error('批量上传失败:', error); + message.error(error.message || '批量上传失败'); } finally { setUploading(false); } @@ -775,13 +702,7 @@ const GeneratedRecord: React.FC = () => { // 日期选择器变化处理函数 const handleDateChange = (dateString: string) => { - console.log(dateString); - - console.log('qweqwe'); - setSelectedDate(dateString); - - }; useEffect(() => { @@ -809,16 +730,11 @@ const GeneratedRecord: React.FC = () => { total: res.total, page: res.page, }]; - console.log('data', data); if (recordList && recordList[0].items.length > 0) { - setRecordList(recordList); - + setRecordList(recordList); }else{ setRecordList([]); } - - console.log('recordList', recordList); - }).catch((err) => { }).finally(() => { setLoading(false); @@ -835,15 +751,12 @@ const GeneratedRecord: React.FC = () => { data.forEach(group => { group.page = 1; }); - console.log('qweqweqwe', data); - // 如果是第一页,替换数据;否则追加数据 if (Pagebreak.page === 1) { setRecordList(data); } else { setRecordList(prev => [...prev, ...data]); } - setTotalnumber(res?.totalDays || 0); }).catch((err) => { if (Pagebreak.page === 1) { @@ -857,22 +770,6 @@ const GeneratedRecord: React.FC = () => { }, [filterType, filterMedia, Pagebreak.page, selectedDate]); - 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 = () => { if (loading) return; @@ -935,16 +832,6 @@ const GeneratedRecord: React.FC = () => { return (
- {/* Header */} - {/*
- - 生成历史 - - - 查看所有生成的视频和图片记录 - -
*/} - {/* 操作栏:筛选 + 上传按钮 */}
{ > 批量上传 + )}
@@ -1069,58 +957,84 @@ const GeneratedRecord: React.FC = () => { background: '#fff', border: '1px solid #f0f0f5', }}> - 媒体类型: - - - - handleDateChange(dateString || '')} - format="YYYY-MM-DD" - style={{ width: 160, borderRadius: 8, border: '1px solid #e2e8f0' }} - placeholder="选择日期" - /> - {selectedDate && ( +
+ 媒体类型: + + - )} - + + handleDateChange(dateString || '')} + format="YYYY-MM-DD" + style={{ width: 160, borderRadius: 8, border: '1px solid #e2e8f0' }} + placeholder="选择日期" + /> + {selectedDate && ( + + )} + +
+ {/* Content area */} @@ -1206,147 +1120,6 @@ const GeneratedRecord: React.FC = () => { )} - {/* 批量上传弹窗 */} - { - setUploadModalVisible(false); - setUploadFiles([]); - }} - footer={null} - width={600} - > -
- {/* 上传区域 */} - false} // 手动控制上传 - accept="image/*,video/*" - listType="picture-card" - onRemove={handleRemoveFile} - > -
- - 点击上传 -
-
- - {/* 上传列表和进度 */} - {uploadFiles.length > 0 && ( -
- - 已选择 {uploadFiles.length} 个文件 - -
- {uploadFiles.map((file) => ( -
-
- {file.type?.startsWith('image/') ? ( - - ) : file.type?.startsWith('video/') ? ( - - ) : ( - - )} -
-
-
- {file.name} -
- {uploadProgress[file.uid] !== undefined && ( - - )} -
-
- ))} -
-
- )} - - {/* 操作按钮 */} -
- - -
-
-
- {/* 上传配置弹窗 */} { setUploadConfigModalVisible(false); setAccountIdList([]); setAccountIdInput(''); - setSelectedOauthItems([]); + setSelectedOauthItems(undefined); setBatchUploadProgress([]); }} footer={null} @@ -1363,32 +1136,12 @@ const GeneratedRecord: React.FC = () => { >
- 选择授权账户(可多选) + 选择授权账户 -
({ ...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 已授权; - } - if (status === 'failed') { - return 授权失败; - } - return 待授权; - }, - }, - ]} - pagination={false} - size="small" - style={{ - maxHeight: 300, - overflow: 'auto', - border: '1px solid #e8e8e8', - borderRadius: 8, - }} - /> - - - - )} - - {/* 上传进度区域 */} - {batchUploadProgress.length > 0 && ( -
- - 上传进度 ({batchUploadProgress.filter(p => p.status === 'success').length}/{batchUploadProgress.length}) - - {batchUploadProgress.map((progress) => ( -
-
- {progress.status === 'success' && } - {progress.status === 'error' && } - {progress.status === 'uploading' && } - {progress.status === 'pending' && } -
-
-
- {progress.itemId} -
- {progress.message && ( -
- {progress.message} -
- )} -
-
- ))} -
- )} - {/* 操作按钮 */}
{ setUploadConfigModalVisible(false); setAccountIdList([]); setAccountIdInput(''); - setSelectedOauthItems([]); + setSelectedOauthItems(undefined); setBatchUploadProgress([]); }} style={{ borderRadius: 8 }} @@ -1663,6 +1311,112 @@ const GeneratedRecord: React.FC = () => {
+ {/* 上传任务历史弹窗 */} + setUploadHistoryModalVisible(false)} + footer={null} + width={800} + style={{ borderRadius: 8 }} + > +
+
{ + const statusMap: Record = { + '1': '待上传', + '2': '上传中', + '3': '上传成功', + '4': '上传失败', + }; + const statusColorMap: Record = { + '1': '#f59e0b', + '2': '#6366f1', + '3': '#10b981', + '4': '#ef4444', + }; + return ( + + {statusMap[status] || status} + + ); + }, + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + 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} + pagination={{ + current: uploadHistoryPage, + pageSize: uploadHistoryPageSize, + total: uploadHistoryTotal, + showSizeChanger: true, + showTotal: (total) => `共 ${total} 条记录`, + onChange: handleUploadHistoryPageChange, + }} + rowKey={(record, index) => record.id || record.resource_id || index} + size="small" + /> + + {/* 预览弹窗 */} {previewVisible && previewItem && (