diff --git a/video-gen-app/src/pages/RemoveInfo.tsx b/video-gen-app/src/pages/RemoveInfo.tsx
new file mode 100644
index 00000000..ec24e62f
--- /dev/null
+++ b/video-gen-app/src/pages/RemoveInfo.tsx
@@ -0,0 +1,419 @@
+import React, { useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { Button, Table, Tag, Drawer, Input, Upload, message } from 'antd';
+import { ArrowLeftOutlined, PlayCircleOutlined, XOutlined, PlusOutlined, UploadOutlined } from '@ant-design/icons';
+import type { UploadFile } from 'antd';
+
+const { TextArea } = Input;
+
+function RemoveInfo() {
+ const { creatID } = useParams<{ creatID: string }>();
+ const navigate = useNavigate();
+ const [drawerVisible, setDrawerVisible] = useState(false);
+ const [currentSegment, setCurrentSegment] = useState
(null);
+ const [productName, setProductName] = useState('');
+ const [productSellingPoint, setProductSellingPoint] = useState('');
+ const [productImage, setProductImage] = useState('');
+ const [detailImage, setDetailImage] = useState('');
+
+ const handleGenerate = (segmentId: number) => {
+ setCurrentSegment(segmentId);
+ setDrawerVisible(true);
+ };
+
+ const handleCloseDrawer = () => {
+ setDrawerVisible(false);
+ setCurrentSegment(null);
+ setProductName('');
+ setProductSellingPoint('');
+ setProductImage('');
+ setDetailImage('');
+ };
+
+ const handleProductImageChange: any = (info: any) => {
+ if (info.fileList.length > 0) {
+ const file = info.fileList[0];
+ if (file.originFileObj) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setProductImage(e.target?.result as string);
+ };
+ reader.readAsDataURL(file.originFileObj);
+ }
+ } else {
+ setProductImage('');
+ }
+ };
+
+ const handleDetailImageChange: any = (info: any) => {
+ if (info.fileList.length > 0) {
+ const file = info.fileList[0];
+ if (file.originFileObj) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setDetailImage(e.target?.result as string);
+ };
+ reader.readAsDataURL(file.originFileObj);
+ }
+ } else {
+ setDetailImage('');
+ }
+ };
+
+ const handleManualGenerate = () => {
+ // 必填校验
+ if (!productImage) {
+ message.warning('请上传产品图');
+ return;
+ }
+ if (!productName.trim()) {
+ message.warning('请输入产品名称');
+ return;
+ }
+ if (!productSellingPoint.trim()) {
+ message.warning('请输入产品卖点');
+ return;
+ }
+
+ // 输出内容
+ console.log('手动生成 - 片段', currentSegment);
+ console.log('产品图:', productImage);
+ console.log('细节图:', detailImage);
+ console.log('产品名称:', productName);
+ console.log('产品卖点:', productSellingPoint);
+
+ message.success(`手动生成成功!片段: ${currentSegment}`);
+ };
+
+ const mockData = {
+ productName: '返回',
+ uploadTime: '2026-06-09 09:01:16',
+ sellingPoints: ['一键匹配', '连麦聊天'],
+ audience: '123123',
+ audienceAnalysis: '123123',
+ videoUrl: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=320&h=180&fit=crop',
+ segments: [
+ {
+ key: '1',
+ id: 1,
+ timeRange: '00:00 - 00:03',
+ thumbnail: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=120&h=80&fit=crop',
+ content: '11111',
+ lines: 'qqqqqqqqqqq',
+ contentStrategy: '展示礼盒'
+ },
+ {
+ key: '2',
+ id: 2,
+ timeRange: '00:03 - 00:06',
+ thumbnail: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=120&h=80&fit=crop',
+ content: '1231231231',
+ lines: 'qqqqqqqqqqq',
+ contentStrategy: '开箱展示'
+ },
+ {
+ key: '3',
+ id: 3,
+ timeRange: '00:06 - 00:09',
+ thumbnail: 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=120&h=80&fit=crop',
+ content: '123123',
+ lines: 'qqqqqqqqqqq',
+ contentStrategy: '取出产品'
+ },
+ ]
+ };
+
+ const columns = [
+ {
+ title: '片段',
+ width: 100,
+ render: (text: any, record: any) => (
+
+
片段{record.id}
+
{record.timeRange}
+
+ )
+ },
+ {
+ title: '片段视频',
+ width: 120,
+ render: (text: any, record: any) => (
+
+

+
+
+ )
+ },
+ {
+ title: '画面内容',
+ width: 250,
+
+ render: (text: any, record: any) => (
+
+ {record.content}
+
+ )
+ },
+ {
+ title: '台词',
+ width: 250,
+
+ render: (text: any, record: any) => (
+
+ {record.lines}
+
+ )
+ },
+ {
+ title: '内容策略',
+ width: 120,
+ align: 'left' as const,
+ render: (text: any, record: any) => (
+
+ {record.contentStrategy || '-'}
+
+ )
+ },
+ {
+ title: '素材',
+ width: 140,
+ align: 'center' as const,
+ render: (text: any, record: any) => (
+
+
+ 等待生成
+
+
+
+ )
+ }
+ ];
+
+ return (
+
+
+
+
+ }
+ onClick={() => navigate(-1)}
+ style={{ fontSize: 16, color: '#666' }}
+ />
+ {mockData.productName}
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
视频总结
+
+ 上传时间: {mockData.uploadTime}
+
+
+
+
+ 产品名称:
+ {mockData.productName}
+
+
+
卖点词:
+
+ {mockData.sellingPoints.map((point, index) => (
+
+ {point}
+
+ ))}
+
+
+
+ 受众群体:
+ {mockData.audience}
+
+
+ 受众分析:
+ {mockData.audienceAnalysis}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 智能视频复刻
+ -片段{currentSegment}
+
+ }
+ onClick={handleCloseDrawer}
+ style={{ padding: 0 }}
+ />
+
+ }
+ placement="right"
+ closable={false}
+ onClose={handleCloseDrawer}
+ open={drawerVisible}
+ width={480}
+ bodyStyle={{ padding: '24px' }}
+ >
+
+
+
+
+
+ {!productImage && (
+
+ )}
+
+
+ {!detailImage && (
+
+ )}
+
+
+
+
+
+
+ setProductName(e.target.value)}
+ placeholder="请输入产品名称"
+ style={{ height: 48, borderRadius: 8 }}
+ maxLength={10}
+ showCount
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default RemoveInfo;
diff --git a/video-gen-app/src/pages/RemoveLens.tsx b/video-gen-app/src/pages/RemoveLens.tsx
index 4e555bc1..df60f6ba 100644
--- a/video-gen-app/src/pages/RemoveLens.tsx
+++ b/video-gen-app/src/pages/RemoveLens.tsx
@@ -1,613 +1,718 @@
-import { useState, useRef, useCallback } from 'react';
-
-interface FrameData {
- url: string;
- time: number;
- selected: boolean;
-}
+import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
+import { Button, Modal, Input, Table, Upload, Popconfirm } from 'antd';
+import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
export default function VideoFrameExtractor() {
- const [videoFile, setVideoFile] = useState
(null);
+ const navigate = useNavigate();
+
const [videoUrl, setVideoUrl] = useState('');
- const [frames, setFrames] = useState([]);
- const [isExtracting, setIsExtracting] = useState(false);
- const [extractProgress, setExtractProgress] = useState(0);
- const [selectedFrames, setSelectedFrames] = useState([]);
const [error, setError] = useState('');
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const [videoDuration, setVideoDuration] = useState(0);
+ const [segmentStart, setSegmentStart] = useState(0);
+ const [segmentEnd, setSegmentEnd] = useState(0);
+ const [isSegmentSelected, setIsSegmentSelected] = useState(false);
+
+ const [dragging, setDragging] = useState<'start' | 'end' | null>(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [framePreviews, setFramePreviews] = useState([]);
+
+ const [tableData] = useState([
+ {
+ id: 1,
+ image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=product%20image%20red%20gift%20box%20with%20hearts&image_size=square',
+ originalName: '进圈',
+ productName: '他趣',
+ status: '视频成功',
+ createTime: '2026-05-14 17:49:20',
+ },
+ {
+ id: 2,
+ image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=luxury%20perfume%20bottle%20golden%20elegant&image_size=square',
+ originalName: '香水',
+ productName: '面霜',
+ status: '视频提示词成功',
+ createTime: '2026-05-08 08:57:46',
+ },
+ ]);
const videoRef = useRef(null);
+ const timelineRef = useRef(null);
const canvasRef = useRef(null);
+ const hiddenVideoRef = useRef(null);
const cleanupResources = useCallback(() => {
if (videoUrl) {
URL.revokeObjectURL(videoUrl);
}
- frames.forEach(frame => {
- if (frame.url.startsWith('blob:')) {
- URL.revokeObjectURL(frame.url);
- }
- });
- setFrames([]);
setVideoUrl('');
- setVideoFile(null);
setError('');
- setSelectedFrames([]);
- }, [videoUrl, frames]);
-
- const handleFileChange = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file) return;
+ setVideoDuration(0);
+ setSegmentStart(0);
+ setSegmentEnd(0);
+ setIsSegmentSelected(false);
+ setIsPlaying(false);
+ setCurrentTime(0);
+ setFramePreviews([]);
+ }, [videoUrl]);
+ const handleFileChange = (file: File) => {
if (!file.type.startsWith('video/')) {
setError('请选择视频文件');
- return;
+ return false;
}
if (file.size > 100 * 1024 * 1024) {
setError('视频文件大小不能超过 100MB');
- return;
+ return false;
}
cleanupResources();
- setVideoFile(file);
const url = URL.createObjectURL(file);
setVideoUrl(url);
setError('');
+ return false;
};
- const handleDrop = (e: React.DragEvent) => {
- e.preventDefault();
- const file = e.dataTransfer.files?.[0];
- if (!file) return;
-
- if (!file.type.startsWith('video/')) {
- setError('请选择视频文件');
- return;
- }
-
- if (file.size > 100 * 1024 * 1024) {
- setError('视频文件大小不能超过 100MB');
- return;
- }
-
- cleanupResources();
-
- setVideoFile(file);
- const url = URL.createObjectURL(file);
- setVideoUrl(url);
- setError('');
- };
-
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault();
- };
-
- /**
- * 视频帧提取核心函数 - 拆镜功能的主要实现
- *
- * 功能说明:
- * - 从上传的视频中按时间间隔提取帧画面
- * - 每秒提取一帧,确保覆盖整个视频时长
- * - 默认选中前4秒的帧(4帧),用于后续创作
- *
- * 技术实现:
- * - 使用 HTML5 Video API 进行视频帧捕获
- * - 通过 Canvas API 将视频帧转换为图片
- * - 使用 Promise + 事件监听确保帧数据就绪
- * - 添加超时机制防止死锁
- */
- const extractFrames = useCallback(async () => {
- // 前置检查:确保视频和画布元素已就绪
- if (!videoRef.current || !canvasRef.current) return;
-
- // 初始化提取状态
- setIsExtracting(true); // 标记正在提取中
- setExtractProgress(1); // 重置进度为0%
- setSelectedFrames([]); // 清空已选帧列表
-
- // 获取视频和画布引用
- const video = videoRef.current;
+ const generateFramePreviews = useCallback(async () => {
+ if (!canvasRef.current || videoDuration <= 0 || !videoUrl) return;
+
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
- if (!ctx) return; // Canvas 2D上下文获取失败则退出
+ if (!ctx) return;
- // 计算提取参数
- const duration = video.duration; // 视频总时长(秒)
- const frameList: FrameData[] = []; // 存储提取的帧数据
- const extractInterval = 1; // 提取间隔:每1秒抽一帧
- const totalFrames = Math.max(1, Math.floor(duration / extractInterval)); // 计算总帧数
-
- try {
- // 遍历视频时间轴,按间隔提取帧
- for (let time = 0; time < duration; time += extractInterval) {
- // 设置视频当前播放位置到目标时间点
- video.currentTime = time;
-
- // 等待帧数据就绪(异步等待)
- await new Promise((resolve) => {
- /**
- * 检查视频就绪状态
- * readyState >= 2 表示当前帧数据已加载完成
- * readyState 值说明:
- * - 0 = HAVE_NOTHING: 无数据
- * - 1 = HAVE_METADATA: 仅元数据
- * - 2 = HAVE_CURRENT_DATA: 当前帧数据可用
- * - 3 = HAVE_FUTURE_DATA: 当前帧和后续帧可用
- * - 4 = HAVE_ENOUGH_DATA: 所有数据可用
- */
- const checkReadyState = () => {
- if (video.readyState >= 2) {
- processFrame();
- return true;
- }
- return false;
- };
-
- /**
- * 处理单帧提取的核心函数
- */
- const processFrame = () => {
- // 清理事件监听器,防止内存泄漏
- video.removeEventListener('seeked', onSeeked);
- video.removeEventListener('loadeddata', onLoadedData);
- clearTimeout(timeoutId); // 清除超时计时器
-
- // 设置画布尺寸为视频帧尺寸
- canvas.width = video.videoWidth;
- canvas.height = video.videoHeight;
-
- // 将视频当前帧绘制到画布
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
-
- // 将画布转换为 Base64 格式的图片 URL(JPEG格式,质量0.9)
- const frameUrl = canvas.toDataURL('image/jpeg', 0.9);
-
- // 将帧数据添加到列表
- frameList.push({
- url: frameUrl, // 帧图片URL
- time: Math.round(time * 10) / 10, // 帧对应的时间点(保留一位小数)
- selected: false, // 默认未选中
- });
-
- // 更新提取进度
- setExtractProgress(Math.round((frameList.length / totalFrames) * 100));
-
- // 完成当前帧提取,继续下一轮
- resolve();
- };
-
- /**
- * seeked 事件处理:视频定位完成后触发
- */
- const onSeeked = () => {
- checkReadyState();
- };
-
- /**
- * loadeddata 事件处理:帧数据加载完成后触发
- * 作为 seeked 的备选方案,确保兼容性
- */
- const onLoadedData = () => {
- processFrame();
- };
-
- /**
- * 超时机制:3秒超时防止无限等待
- * 如果3秒内帧数据仍未就绪,跳过当前帧继续下一帧
- */
- const timeoutId = setTimeout(() => {
- video.removeEventListener('seeked', onSeeked);
- video.removeEventListener('loadeddata', onLoadedData);
- console.warn(`Timeout waiting for frame at ${time}s`);
- resolve(); // 跳过当前帧,继续处理下一帧
- }, 3000);
-
- // 先同步检查状态,如果未就绪则注册事件监听器等待
- if (!checkReadyState()) {
- video.addEventListener('seeked', onSeeked);
- video.addEventListener('loadeddata', onLoadedData);
- }
- });
- }
-
- // 提取完成后,默认选中前4秒的帧(约8帧)
- const defaultSelected = Math.min(8, frameList.length);
- const defaultIndices: number[] = [];
- const updatedFrames = frameList.map((frame, index) => {
- if (index < defaultSelected) {
- defaultIndices.push(index);
- return { ...frame, selected: true }; // 标记为已选中
- }
- return frame;
+ // 创建隐藏的视频元素用于生成帧预览
+ if (!hiddenVideoRef.current) {
+ hiddenVideoRef.current = document.createElement('video');
+ hiddenVideoRef.current.style.display = 'none';
+ hiddenVideoRef.current.crossOrigin = 'anonymous';
+ document.body.appendChild(hiddenVideoRef.current);
+ }
+
+ const hiddenVideo = hiddenVideoRef.current;
+ hiddenVideo.src = videoUrl;
+
+ await new Promise((resolve) => {
+ hiddenVideo.addEventListener('loadedmetadata', () => resolve(), { once: true });
+ });
+
+ // 按秒数生成帧,每秒 1 帧,最多 15 帧
+ const numFrames = Math.min(Math.ceil(videoDuration), 15);
+ const previews: string[] = [];
+
+ for (let i = 0; i < numFrames; i++) {
+ const time = ((i + 0.5) / numFrames) * videoDuration;
+
+ await new Promise((resolve) => {
+ const handleSeeked = () => {
+ hiddenVideo.removeEventListener('seeked', handleSeeked);
+ setTimeout(resolve, 80);
+ };
+ hiddenVideo.addEventListener('seeked', handleSeeked);
+ hiddenVideo.currentTime = time;
});
+
+ canvas.width = 120;
+ canvas.height = 80;
+ ctx.drawImage(hiddenVideo, 0, 0, canvas.width, canvas.height);
+ previews.push(canvas.toDataURL('image/jpeg', 0.8));
+ }
+
+ setFramePreviews(previews);
+ }, [videoDuration, videoUrl]);
- // 更新状态:保存提取的帧列表和默认选中的帧索引
- setFrames(updatedFrames);
- setSelectedFrames(defaultIndices);
- } catch (err) {
- // 提取过程中发生错误
- setError('拆镜过程中发生错误');
- console.error('Extract frames error:', err);
- } finally {
- // 无论成功或失败,都标记提取完成
- setIsExtracting(false);
- setExtractProgress(100);
+ const handleVideoLoaded = useCallback(() => {
+ if (videoRef.current) {
+ const duration = videoRef.current.duration;
+ setVideoDuration(duration);
+ setSegmentStart(0);
+ setSegmentEnd(Math.min(15, duration));
+ setIsSegmentSelected(true);
}
}, []);
- const toggleFrameSelection = (index: number) => {
- setSelectedFrames(prev => {
- if (prev.includes(index)) {
- return prev.filter(i => i !== index);
- }
- return [...prev, index];
- });
+ useEffect(() => {
+ if (videoDuration > 0) {
+ generateFramePreviews();
+ }
+ }, [videoDuration, generateFramePreviews]);
- setFrames(prev =>
- prev.map((frame, i) =>
- i === index ? { ...frame, selected: !frame.selected } : frame
- )
- );
- };
+ const handleVideoTimeUpdate = useCallback(() => {
+ if (videoRef.current) {
+ setCurrentTime(videoRef.current.currentTime);
+ }
+ }, []);
- const formatTime = (seconds: number) => {
+ const togglePlay = useCallback(() => {
+ if (!videoRef.current) return;
+ if (videoRef.current.paused) {
+ videoRef.current.play();
+ setIsPlaying(true);
+ } else {
+ videoRef.current.pause();
+ setIsPlaying(false);
+ }
+ }, []);
+
+ const formatTime = useCallback((seconds: number) => {
const mins = Math.floor(seconds / 60);
- const secs = Math.round(seconds % 60);
- return `${mins}:${String(secs).padStart(2, '0')}`;
- };
+ const secs = Math.floor(seconds % 60);
+ const cs = Math.floor((seconds % 1) * 100);
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}:${String(cs).padStart(2, '0')}`;
+ }, []);
- const handleRemoveVideo = () => {
- cleanupResources();
- };
+ const formatTimeShort = useCallback((seconds: number) => {
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
+ }, []);
+
+ const getTimeFromEvent = useCallback((clientX: number): number | null => {
+ if (!timelineRef.current || videoDuration === 0) return null;
+ const rect = timelineRef.current.getBoundingClientRect();
+ const x = Math.max(0, Math.min(rect.width, clientX - rect.left));
+ const percentage = x / rect.width;
+ return percentage * videoDuration;
+ }, [videoDuration]);
+
+ const handleTimelineMouseDown = useCallback((
+ e: React.MouseEvent,
+ handle: 'start' | 'end'
+ ) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setDragging(handle);
+ }, []);
+
+ const handleTimelineClick = useCallback((e: React.MouseEvent) => {
+ e.stopPropagation();
+ const time = getTimeFromEvent(e.clientX);
+ if (time !== null && videoRef.current) {
+ videoRef.current.currentTime = time;
+ }
+ }, [getTimeFromEvent]);
+
+ useEffect(() => {
+ if (!dragging) return;
+
+ const handleMouseMove = (e: MouseEvent) => {
+ const time = getTimeFromEvent(e.clientX);
+ if (time === null) return;
+
+ if (dragging === 'start') {
+ setSegmentStart(Math.max(0, Math.min(time, segmentEnd - 0.3)));
+ } else {
+ setSegmentEnd(Math.max(segmentStart + 0.3, Math.min(time, videoDuration)));
+ }
+ };
+
+ const handleMouseUp = () => {
+ setDragging(null);
+ };
+
+ window.addEventListener('mousemove', handleMouseMove);
+ window.addEventListener('mouseup', handleMouseUp);
+
+ return () => {
+ window.removeEventListener('mousemove', handleMouseMove);
+ window.removeEventListener('mouseup', handleMouseUp);
+ };
+ }, [dragging, segmentStart, segmentEnd, videoDuration, getTimeFromEvent]);
+
+ const segmentDuration = useMemo(() => segmentEnd - segmentStart, [segmentEnd, segmentStart]);
+ const segmentValid = useMemo(() => segmentDuration >= 4 && segmentDuration <= 15, [segmentDuration]);
+
+ const startPercent = useMemo(() => videoDuration > 0 ? (segmentStart / videoDuration) * 100 : 0, [segmentStart, videoDuration]);
+ const endPercent = useMemo(() => videoDuration > 0 ? (segmentEnd / videoDuration) * 100 : 100, [segmentEnd, videoDuration]);
+ const rangeWidth = useMemo(() => endPercent - startPercent, [endPercent, startPercent]);
return (
-
-
- {/* 标题区域 */}
+
+ }
+ style={{
+ borderRadius: 6,
+ fontSize: 16,
+ fontWeight: 600,
+ color: '#656efa'
+ }}
+ onClick={() => setIsModalOpen(true)}
+ >
+ 创作记录
+
+
+
+
- {/* 错误提示 */}
- {error && (
-
- ⚠️ {error}
-
- )}
-
- {/* 上传/视频区域 */}
-
- {/* 删除按钮 */}
- {videoUrl && (
-
+
+ {error && (
+
+ {error}
+
)}
- {/* 未上传状态 */}
{!videoUrl && (
-
-
+
-
-
+
+
支持 MP4、MOV 格式视频,最大支持 100MB 的文件,支持上传 3 分钟内的视频,可处理 4-15 秒片段
-
+
)}
- {/* 已上传状态 - 视频预览 */}
- {videoUrl && !frames.length && (
-
-
-
- 时长: {formatTime(videoRef.current?.duration || 0)}
-
-
- )}
-
- {/* 拆镜结果 */}
- {frames.length > 0 && (
-
- {/* 视频预览 */}
-
-
-
-
- {/* 帧选择区域 */}
-
+
-
+
-
- 00:00:00 / 00:00:{String(Math.round(videoRef.current?.duration || 0)).padStart(2, '0')}
-
+
+
+
+
- {/* 帧缩略图 */}
-
- {frames.map((frame, index) => (
-
toggleFrameSelection(index)}
- style={{
- flexShrink: 0,
- width: 60,
- height: 45,
- border: frame.selected ? '2px solid #6366f1' : '1px solid #e5e7eb',
- borderRadius: 6,
- overflow: 'hidden',
- cursor: 'pointer',
- position: 'relative'
- }}
- >
-

- {frame.selected && (
-
+
+
+
+ {formatTime(currentTime)} / {formatTime(videoDuration)}
+
+
+
+
+
+
+
+ {framePreviews.map((frame, index) => (
+
+ {frame && (
+

+ )}
+
+ ))}
+
+
+
handleTimelineMouseDown(e, 'start')}
+ onClick={(e) => e.stopPropagation()}
+ style={{
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: `calc(${startPercent}% - 10px)`,
+ width: 20,
+ background: 'linear-gradient(180deg, #6366f1 0%, #4f46e5 100%)',
+ borderRadius: 6,
+ cursor: 'ew-resize',
+ boxShadow: dragging === 'start'
+ ? '0 4px 16px rgba(99, 102, 241, 0.4)'
+ : '0 2px 8px rgba(99, 102, 241, 0.3)',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 10
+ }}
+ >
+
+
+
+
handleTimelineMouseDown(e, 'end')}
+ onClick={(e) => e.stopPropagation()}
+ style={{
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: `calc(${endPercent}% - 10px)`,
+ width: 20,
+ background: 'linear-gradient(180deg, #ec4899 0%, #db2777 100%)',
+ borderRadius: 6,
+ cursor: 'ew-resize',
+ boxShadow: dragging === 'end'
+ ? '0 4px 16px rgba(236, 72, 153, 0.4)'
+ : '0 2px 8px rgba(236, 72, 153, 0.3)',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 10
+ }}
+ >
+
+
+
+
+
+ 已选取 {formatTimeShort(segmentDuration)}
+ {!segmentValid && (
+
+ (建议 4-15 秒)
+
)}
- ))}
-
-
-
-
- 已选取 {selectedFrames.length} 帧
-
-
+
+ )}
)}
- {/* 进度条 */}
- {isExtracting && (
-
-
- 正在拆解...
- {extractProgress}%
-
-
-
- )}
+ 预计消耗 10 个积分
+
+
+
-
- {/* 操作按钮 */}
-
-
-
-
- {/* 创作记录 */}
- {/*
*/}
- {/* 隐藏画布 */}
+
+
setIsModalOpen(false)}
+ width={800}
+ footer={null}
+ >
+
+
+
+
+
+ (
+
+ ),
+ },
+ {
+ title: '原产品名称',
+ dataIndex: 'originalName',
+ key: 'originalName',
+ },
+ {
+ title: '产品名称',
+ dataIndex: 'productName',
+ key: 'productName',
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'createTime',
+ key: 'createTime',
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: (_, record) => (
+
+ ),
+ },
+ ]}
+ dataSource={tableData}
+ rowKey="id"
+ pagination={false}
+ />
+
);
}
\ No newline at end of file
diff --git a/video-gen-app/src/pages/admin/AdminCreditRatios.tsx b/video-gen-app/src/pages/admin/AdminCreditRatios.tsx
deleted file mode 100644
index 0bf3beaf..00000000
--- a/video-gen-app/src/pages/admin/AdminCreditRatios.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import React, { useState } from 'react';
-import {
- Button, Card, Form, InputNumber, message, Modal, Popconfirm, Select, Space, Table, Tag, Typography,
-} from 'antd';
-import {
- CalculatorOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
-} from '@ant-design/icons';
-
-interface CreditRatio {
- id: string;
- modelName: string;
- resolution: string;
- ratio: number;
- baseCredits: number;
- perSecondCredits: number;
-}
-
-const MOCK_RATIOS: CreditRatio[] = [
- { id: 'cr-1', modelName: 'GPT-4o', resolution: '720p', ratio: 1.0, baseCredits: 60, perSecondCredits: 2 },
- { id: 'cr-2', modelName: 'GPT-4o', resolution: '1080p', ratio: 1.5, baseCredits: 90, perSecondCredits: 3 },
- { id: 'cr-3', modelName: 'GPT-4o', resolution: '4K', ratio: 2.5, baseCredits: 150, perSecondCredits: 5 },
- { id: 'cr-4', modelName: 'DeepSeek-V3', resolution: '720p', ratio: 0.8, baseCredits: 48, perSecondCredits: 2 },
- { id: 'cr-5', modelName: 'DeepSeek-V3', resolution: '1080p', ratio: 1.2, baseCredits: 72, perSecondCredits: 3 },
- { id: 'cr-6', modelName: 'DeepSeek-V3', resolution: '4K', ratio: 2.0, baseCredits: 120, perSecondCredits: 4 },
- { id: 'cr-7', modelName: '通用', resolution: '720p', ratio: 1.0, baseCredits: 60, perSecondCredits: 2 },
- { id: 'cr-8', modelName: '通用', resolution: '1080p', ratio: 1.5, baseCredits: 90, perSecondCredits: 3 },
- { id: 'cr-9', modelName: '通用', resolution: '4K', ratio: 2.5, baseCredits: 150, perSecondCredits: 5 },
-];
-
-const AdminCreditRatios: React.FC = () => {
- const [ratios, setRatios] = useState(MOCK_RATIOS);
- const [modal, setModal] = useState<{ open: boolean; ratio: CreditRatio | null }>({ open: false, ratio: null });
- const [form] = Form.useForm();
-
- const handleSave = async () => {
- try {
- const values = await form.validateFields();
- if (modal.ratio) {
- setRatios(prev => prev.map(r => r.id === modal.ratio!.id ? { ...r, ...values } : r));
- message.success('已更新');
- } else {
- setRatios(prev => [...prev, { id: `cr-${Date.now()}`, ...values }]);
- message.success('已添加');
- }
- setModal({ open: false, ratio: null });
- form.resetFields();
- } catch { /* validation */ }
- };
-
- const handleDelete = (id: string) => {
- setRatios(prev => prev.filter(r => r.id !== id));
- message.success('已删除');
- };
-
- const openEdit = (ratio?: CreditRatio) => {
- setModal({ open: true, ratio: ratio || null });
- if (ratio) form.setFieldsValue(ratio);
- else { form.resetFields(); form.setFieldsValue({ ratio: 1.0, baseCredits: 60, perSecondCredits: 2 }); }
- };
-
- const columns = [
- {
- title: '模型', dataIndex: 'modelName', width: 150,
- render: (v: string) => {v},
- },
- {
- title: '分辨率', dataIndex: 'resolution', width: 100,
- render: (v: string) => {
- const colors: Record = { '720p': 'default', '1080p': 'blue', '4K': 'gold' };
- return {v};
- },
- },
- {
- title: '倍率', dataIndex: 'ratio', width: 100, sorter: (a: CreditRatio, b: CreditRatio) => a.ratio - b.ratio,
- render: (v: number) => (
- = 2 ? '#ef4444' : v >= 1.5 ? '#f59e0b' : '#10b981' }}>
- x{v}
-
- ),
- },
- {
- title: '基础积分', dataIndex: 'baseCredits', width: 100,
- render: (v: number) => {v} 积分,
- },
- {
- title: '每秒积分', dataIndex: 'perSecondCredits', width: 100,
- render: (v: number) => {v} 积分/秒,
- },
- {
- title: '示例计算 (15秒)', key: 'example', width: 120,
- render: (_: any, r: CreditRatio) => {
- const total = Math.round((r.baseCredits + r.perSecondCredits * 15) * r.ratio);
- return {total} 积分;
- },
- },
- {
- title: '操作', key: 'action', width: 150, fixed: 'right' as const,
- render: (_: any, r: CreditRatio) => (
-
- } onClick={() => openEdit(r)}>编辑
- handleDelete(r.id)}>
- }>删除
-
-
- ),
- },
- ];
-
- return (
-
-
-
-
-
- 积分比例配置
- {ratios.length} 条规则
-
- } onClick={() => openEdit()} style={{ borderRadius: 8 }}>
- 添加比例
-
-
-
-
- 积分计算公式:(基础积分 + 每秒积分 x 视频时长) x 模型倍率
-
-
-
-
-
-
{modal.ratio ? '编辑比例' : '添加比例'}}
- open={modal.open}
- onOk={handleSave}
- onCancel={() => { setModal({ open: false, ratio: null }); form.resetFields(); }}
- okText="保存" cancelText="取消" width={480}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default AdminCreditRatios;
diff --git a/video-gen-app/src/pages/admin/AdminCreditRecords.tsx b/video-gen-app/src/pages/admin/AdminCreditRecords.tsx
deleted file mode 100644
index 85bcb708..00000000
--- a/video-gen-app/src/pages/admin/AdminCreditRecords.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button, Card, DatePicker, Select, Space, Table, Tag, Typography,
-} from 'antd';
-import {
- WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, SearchOutlined,
-} from '@ant-design/icons';
-
-interface CreditRecord {
- id: string;
- username: string;
- type: 'recharge' | 'consume';
- amount: number;
- balanceAfter: number;
- description: string;
- createdAt: string;
-}
-
-const MOCK_RECORDS: CreditRecord[] = [
- { id: 'cr-1', username: 'videomaker', type: 'recharge', amount: 3000, balanceAfter: 3000, description: '会员充值赠送', createdAt: '2026-04-28 10:00:00' },
- { id: 'cr-2', username: 'videomaker', type: 'consume', amount: -120, balanceAfter: 2880, description: '提示词优化 - 电商广告视频', createdAt: '2026-04-29 14:22:00' },
- { id: 'cr-3', username: 'designer', type: 'recharge', amount: 2000, balanceAfter: 2000, description: '进阶包充值', createdAt: '2026-04-29 16:00:00' },
- { id: 'cr-4', username: 'videomaker', type: 'consume', amount: -80, balanceAfter: 2800, description: '提示词优化 - 教育课程视频', createdAt: '2026-04-30 09:15:00' },
- { id: 'cr-5', username: 'designer', type: 'consume', amount: -100, balanceAfter: 1900, description: '提示词优化 - 品牌故事视频', createdAt: '2026-05-01 11:30:00' },
- { id: 'cr-6', username: 'marketer', type: 'recharge', amount: 5000, balanceAfter: 5000, description: '专业包充值', createdAt: '2026-05-02 08:00:00' },
- { id: 'cr-7', username: 'videomaker', type: 'recharge', amount: 500, balanceAfter: 3300, description: '活动赠送积分', createdAt: '2026-05-02 11:00:00' },
- { id: 'cr-8', username: 'marketer', type: 'consume', amount: -120, balanceAfter: 4880, description: '提示词优化 - 产品宣传视频', createdAt: '2026-05-03 15:20:00' },
- { id: 'cr-9', username: 'editor', type: 'recharge', amount: 1500, balanceAfter: 1500, description: '体验包充值', createdAt: '2026-05-04 10:00:00' },
- { id: 'cr-10', username: 'designer', type: 'consume', amount: -200, balanceAfter: 1700, description: '提示词优化 - 游戏预告片', createdAt: '2026-05-05 14:45:00' },
-];
-
-const AdminCreditRecords: React.FC = () => {
- const [records, setRecords] = useState(MOCK_RECORDS);
- const [typeFilter, setTypeFilter] = useState('');
- const [loading, setLoading] = useState(false);
-
- const filtered = typeFilter ? records.filter(r => r.type === typeFilter) : records;
-
- const totalRecharge = records.filter(r => r.type === 'recharge').reduce((s, r) => s + r.amount, 0);
- const totalConsume = records.filter(r => r.type === 'consume').reduce((s, r) => s + Math.abs(r.amount), 0);
-
- const columns = [
- {
- title: '用户', dataIndex: 'username', width: 120,
- render: (v: string) => {v},
- },
- {
- title: '类型', dataIndex: 'type', width: 100,
- render: (v: string) => (
- : }>
- {v === 'recharge' ? '充值' : '消费'}
-
- ),
- filters: [
- { text: '充值', value: 'recharge' },
- { text: '消费', value: 'consume' },
- ],
- onFilter: (value: any, record: CreditRecord) => record.type === value,
- },
- {
- title: '变动积分', dataIndex: 'amount', width: 120, sorter: (a: CreditRecord, b: CreditRecord) => a.amount - b.amount,
- render: (v: number) => (
- 0 ? '#10b981' : '#ef4444', fontSize: 15 }}>
- {v > 0 ? '+' : ''}{v.toLocaleString()}
-
- ),
- },
- {
- title: '变动后余额', dataIndex: 'balanceAfter', width: 120,
- render: (v: number) => {v.toLocaleString()},
- },
- {
- title: '说明', dataIndex: 'description', ellipsis: true,
- },
- {
- title: '时间', dataIndex: 'createdAt', width: 160,
- render: (v: string) => {v},
- },
- ];
-
- return (
-
- {/* Summary Cards */}
-
-
-
-
-
-
总充值
-
+{totalRecharge.toLocaleString()}
-
-
-
-
-
-
-
-
总消费
-
-{totalConsume.toLocaleString()}
-
-
-
-
-
-
-
-
交易笔数
-
{records.length}
-
-
-
-
-
-
- `共 ${t} 条记录` }}
- scroll={{ x: 800 }}
- />
-
-
- );
-};
-
-export default AdminCreditRecords;
diff --git a/video-gen-app/src/pages/admin/AdminDashboard.tsx b/video-gen-app/src/pages/admin/AdminDashboard.tsx
deleted file mode 100644
index acf23e54..00000000
--- a/video-gen-app/src/pages/admin/AdminDashboard.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Card, Col, Row, Statistic, Typography, Table, Tag } from 'antd';
-import {
- UserOutlined,
- ProjectOutlined,
- PlayCircleOutlined,
- DollarOutlined,
- ThunderboltOutlined,
- ArrowUpOutlined,
-} from '@ant-design/icons';
-import { getAdminStats } from '../../api';
-import type { AdminStats } from '../../types';
-
-const AdminDashboard: React.FC = () => {
- const [stats, setStats] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- const load = async () => {
- setLoading(true);
- const data = await getAdminStats();
- setStats(data);
- setLoading(false);
- };
- load();
- }, []);
-
- const statCards = stats ? [
- { title: '总用户数', value: stats.totalUsers, icon: , color: '#6366f1', bg: 'rgba(99,102,241,0.08)' },
- { title: '总项目数', value: stats.totalProjects, icon: , color: '#06b6d4', bg: 'rgba(6,182,212,0.08)' },
- { title: '总生成次数', value: stats.totalGenerations, icon: , color: '#10b981', bg: 'rgba(16,185,129,0.08)' },
- { title: '总收入(元)', value: stats.totalRevenue, icon: , color: '#f59e0b', bg: 'rgba(245,158,11,0.08)', prefix: '¥' },
- { title: '今日消耗积分', value: stats.creditsConsumedToday, icon: , color: '#ef4444', bg: 'rgba(239,68,68,0.08)' },
- ] : [];
-
- return (
-
- {/* Stats Cards */}
-
- {statCards.map((s, i) => (
-
-
-
-
- {s.icon}
-
-
-
{s.title}
-
- {s.prefix}{typeof s.value === 'number' ? s.value.toLocaleString() : s.value}
-
-
-
-
-
- ))}
-
-
- {/* Quick Info */}
-
-
-
-
- {[
- { label: '平台名称', value: 'VideoGen.AI' },
- { label: 'API版本', value: 'v1.0.0' },
- { label: '数据库', value: 'SQLite (本地开发)' },
- { label: 'LLM模式', value: 'Mock (模拟)' },
- { label: '视频引擎', value: 'Seedance 2.0' },
- ].map(item => (
-
- {item.label}
- {item.value}
-
- ))}
-
-
-
-
-
-
- {[
- { name: '体验包', credits: 500, price: 49, color: '#f59e0b' },
- { name: '进阶包', credits: 2000, price: 168, color: '#6366f1', hot: true },
- { name: '专业包', credits: 5000, price: 388, color: '#06b6d4' },
- { name: '企业包', credits: 20000, price: 1280, color: '#10b981' },
- ].map(p => (
-
-
-
-
{p.name}
- {p.hot &&
热门}
-
-
- ¥{p.price}
- {p.credits.toLocaleString()}积分
-
-
- ))}
-
-
-
-
-
- );
-};
-
-export default AdminDashboard;
diff --git a/video-gen-app/src/pages/admin/AdminIndustries.tsx b/video-gen-app/src/pages/admin/AdminIndustries.tsx
deleted file mode 100644
index e03e3200..00000000
--- a/video-gen-app/src/pages/admin/AdminIndustries.tsx
+++ /dev/null
@@ -1,336 +0,0 @@
-import React, { useState } from 'react';
-import {
- Button, Card, Form, Input, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
-} from 'antd';
-import {
- AppstoreOutlined, PlusOutlined, EditOutlined, DeleteOutlined, MinusCircleOutlined,
-} from '@ant-design/icons';
-
-interface OptionGroup {
- name: string;
- options: string[];
-}
-
-interface IndustryItem {
- id: string;
- key: string;
- label: string;
- description: string;
- skills: string[];
- optionGroups: OptionGroup[];
- isActive: boolean;
- sortOrder: number;
-}
-
-const MOCK_INDUSTRIES: IndustryItem[] = [
- {
- id: 'ind-1', key: 'ecommerce', label: '电商', description: '电商直播、产品展示、促销活动',
- skills: ['你是一位专业的电商视频文案专家,擅长将产品卖点转化为视觉语言,注重画面节奏和消费者心理'],
- optionGroups: [
- { name: '视频风格', options: ['实拍展示', '3D动画', '混剪快闪', '沉浸体验'] },
- { name: '目标受众', options: ['年轻女性', '家庭用户', '商务人士', '学生群体'] },
- ],
- isActive: true, sortOrder: 1,
- },
- {
- id: 'ind-2', key: 'education', label: '教育', description: '在线课程、知识付费、培训',
- skills: ['你是一位专业的教育视频策划专家,擅长将复杂知识点转化为生动易懂的视觉叙事'],
- optionGroups: [
- { name: '课程类型', options: ['知识讲解', '操作演示', '故事叙事', '互动问答'] },
- ],
- isActive: true, sortOrder: 2,
- },
- {
- id: 'ind-3', key: 'gaming', label: '游戏', description: '游戏预告、赛事宣传、角色展示',
- skills: ['你是一位专业的游戏视频创意专家,擅长打造震撼视觉体验和沉浸式叙事'],
- optionGroups: [
- { name: '游戏类型', options: ['RPG', 'FPS', 'MOBA', '休闲'] },
- { name: '视频类型', options: ['预告片', '宣传片', '教程', '赛事回顾'] },
- ],
- isActive: true, sortOrder: 3,
- },
- {
- id: 'ind-4', key: 'medical', label: '医疗', description: '医疗健康、药品宣传、科普',
- skills: ['你是一位专业的医疗健康视频文案专家,擅长将医学知识转化为通俗易懂的视觉内容'],
- optionGroups: [],
- isActive: true, sortOrder: 4,
- },
- {
- id: 'ind-5', key: 'finance', label: '金融', description: '理财产品、保险、银行服务',
- skills: ['你是一位专业的金融视频文案专家,擅长将复杂的金融产品转化为易于理解的视觉表达'],
- optionGroups: [],
- isActive: true, sortOrder: 5,
- },
- {
- id: 'ind-6', key: 'realestate', label: '房产', description: '楼盘展示、户型介绍、周边配套',
- skills: ['你是一位专业的房产视频策划专家,擅长通过镜头语言展现空间美感和生活场景'],
- optionGroups: [
- { name: '展示方式', options: ['航拍全景', '室内漫游', '样板间', '周边实景'] },
- ],
- isActive: true, sortOrder: 6,
- },
- {
- id: 'ind-7', key: 'food', label: '餐饮', description: '美食制作、餐厅宣传、食材展示',
- skills: ['你是一位专业的美食视频创意专家,擅长用镜头捕捉食物的色香味,营造食欲感'],
- optionGroups: [
- { name: '拍摄风格', options: ['特写慢放', '制作过程', '美食探店', '食材溯源'] },
- ],
- isActive: true, sortOrder: 7,
- },
- {
- id: 'ind-8', key: 'travel', label: '旅游', description: '景点宣传、酒店推荐、旅行攻略',
- skills: ['你是一位专业的旅游视频文案专家,擅长用镜头语言展现目的地魅力和旅行体验'],
- optionGroups: [
- { name: '内容形式', options: ['Vlog', '攻略指南', '风景大片', '人文记录'] },
- ],
- isActive: true, sortOrder: 8,
- },
- {
- id: 'ind-9', key: 'tech', label: '科技', description: '科技产品、SaaS服务、AI应用',
- skills: ['你是一位专业的科技视频策划专家,擅长将技术概念转化为直观的视觉演示'],
- optionGroups: [
- { name: '演示方式', options: ['产品演示', '对比评测', '概念解析', '场景模拟'] },
- ],
- isActive: true, sortOrder: 9,
- },
- {
- id: 'ind-10', key: 'other', label: '其他', description: '通用行业',
- skills: ['你是一位专业的视频导演和文案专家,擅长将主题转化为富有感染力的视觉叙事'],
- optionGroups: [],
- isActive: true, sortOrder: 10,
- },
-];
-
-const AdminIndustries: React.FC = () => {
- const [industries, setIndustries] = useState(MOCK_INDUSTRIES);
- const [saving, setSaving] = useState(false);
- const [modal, setModal] = useState<{ open: boolean; item: IndustryItem | null }>({ open: false, item: null });
- const [form] = Form.useForm();
-
- const handleSave = async () => {
- try {
- const values = await form.validateFields();
- setSaving(true);
- const skills = values.skills_wentutujie?.trim() ? [values.skills_wentutujie.trim()] : [];
- const optionGroups: OptionGroup[] = (values.optionGroups || [])
- .filter((g: any) => g?.name?.trim())
- .map((g: any) => ({
- name: g.name.trim(),
- options: (g.options || []).filter((o: string) => o?.trim()),
- }))
- .filter((g: OptionGroup) => g.options.length > 0);
-
- if (modal.item) {
- setIndustries(prev => prev.map(i => i.id === modal.item!.id ? { ...i, ...values, skills, optionGroups } : i));
- message.success('已更新');
- } else {
- const newItem: IndustryItem = {
- id: `ind-${Date.now()}`,
- key: values.key,
- label: values.label,
- description: values.description || '',
- skills,
- optionGroups,
- isActive: values.isActive !== false,
- sortOrder: industries.length + 1,
- };
- setIndustries(prev => [...prev, newItem]);
- message.success('已添加');
- }
- setModal({ open: false, item: null });
- form.resetFields();
- } catch (e: any) {
- if (e?.errorFields) return;
- message.error(e?.message || '保存失败');
- } finally {
- setSaving(false);
- }
- };
-
- const handleDelete = (id: string) => {
- setIndustries(prev => prev.filter(i => i.id !== id));
- message.success('已删除');
- };
-
- const openEdit = (item?: IndustryItem) => {
- setModal({ open: true, item: item || null });
- if (item) {
- form.setFieldsValue({
- key: item.key,
- label: item.label,
- description: item.description,
- skills_wentutujie: item.skills[0] || '',
- optionGroups: item.optionGroups.length > 0 ? item.optionGroups : [{ name: '', options: [] }],
- isActive: item.isActive,
- });
- } else {
- form.resetFields();
- form.setFieldsValue({ isActive: true, optionGroups: [{ name: '', options: [] }] });
- }
- };
-
- const columns = [
- {
- title: '行业', key: 'industry', width: 160,
- render: (_: any, r: IndustryItem) => (
-
- ),
- },
- {
- title: '描述', dataIndex: 'description', ellipsis: true,
- },
- {
- title: '选项配置', key: 'optionGroups', width: 260,
- render: (_: any, r: IndustryItem) => {
- if (!r.optionGroups || r.optionGroups.length === 0) {
- return 未配置;
- }
- return (
-
- {r.optionGroups.map((g, i) => (
-
- {g.name}
-
- {g.options.slice(0, 3).join('、')}{g.options.length > 3 ? `...${g.options.length}项` : ''}
-
-
- ))}
-
- );
- },
- },
- {
- title: '文图理解提示词', dataIndex: 'skills', width: 200,
- render: (skills: string[]) => (
-
- {skills[0] || '-'}
-
- ),
- },
- {
- title: '状态', dataIndex: 'isActive', width: 80,
- render: (v: boolean) => {v ? '启用' : '停用'},
- },
- {
- title: '操作', key: 'action', width: 150, fixed: 'right' as const,
- render: (_: any, r: IndustryItem) => (
-
- } onClick={() => openEdit(r)}>编辑
- handleDelete(r.id)}>
- }>删除
-
-
- ),
- },
- ];
-
- return (
-
-
-
-
-
- 行业与技能配置
- {industries.length} 个行业
-
-
} onClick={() => openEdit()} style={{ borderRadius: 8 }}>
- 添加行业
-
-
-
-
-
-
-
{modal.item ? '编辑行业' : '添加行业'}}
- open={modal.open}
- onOk={handleSave}
- onCancel={() => { setModal({ open: false, item: null }); form.resetFields(); }}
- okText="保存" cancelText="取消" width={640}
- confirmLoading={saving}
- >
-
-
-
-
-
-
-
- {/* Option Groups */}
-
- 行业选项配置
-
- 添加选项组,每组包含名称和多个选项,前台将显示为下拉选择
-
-
-
- {(fields, { add, remove }) => (
-
- {fields.map(({ key, name, ...restField }) => (
-
-
-
-
-
-
-
-
-
-
remove(name)}
- style={{ color: '#ef4444', fontSize: 16, marginTop: 34, cursor: 'pointer', flexShrink: 0 }}
- />
-
- ))}
-
-
- )}
-
-
-
-
-
-
-
-
- );
-};
-
-export default AdminIndustries;
diff --git a/video-gen-app/src/pages/admin/AdminLayout.tsx b/video-gen-app/src/pages/admin/AdminLayout.tsx
deleted file mode 100644
index 2dcc27d5..00000000
--- a/video-gen-app/src/pages/admin/AdminLayout.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import React, { useState } from 'react';
-import { Layout, Menu, Avatar, Typography, Space, Dropdown, Spin } from 'antd';
-import {
- DashboardOutlined,
- UserOutlined,
- RobotOutlined,
- SettingOutlined,
- BellOutlined,
- ThunderboltOutlined,
- LogoutOutlined,
- LeftOutlined,
- RightOutlined,
- WalletOutlined,
- CalculatorOutlined,
- DollarOutlined,
- AppstoreOutlined,
- PlayCircleOutlined,
-} from '@ant-design/icons';
-import { Outlet, useNavigate, useLocation, Navigate } from 'react-router-dom';
-import { useAuthStore } from '../../store/useAuthStore';
-
-const { Sider, Content } = Layout;
-
-const AdminLayout: React.FC = () => {
- const navigate = useNavigate();
- const location = useLocation();
- const { user, loading, logout } = useAuthStore();
- const [collapsed, setCollapsed] = useState(false);
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (!user) {
- return ;
- }
-
- const menuItems = [
- { key: '/admin', icon: , label: '数据概览' },
- { key: '/admin/users', icon: , label: '用户管理' },
- { key: '/admin/credit-records', icon: , label: '交易流水' },
- { key: '/admin/models', icon: , label: '模型配置' },
- { key: '/admin/credit-ratios', icon: , label: '积分比例' },
- { key: '/admin/video-engines', icon: , label: '视频引擎' },
- { key: '/admin/industries', icon: , label: '行业配置' },
- { key: '/admin/payment', icon: , label: '支付配置' },
- { key: '/admin/settings', icon: , label: '系统设置' },
- { key: '/admin/notifications', icon: , label: '消息推送' },
- ];
-
- const selectedKey = location.pathname;
-
- return (
-
-
- {/* Logo */}
-
-
-
-
- {!collapsed && (
-
- 管理后台
-
- )}
-
-
- {/* Menu */}
-
-
-
- {/* Header */}
-
-
- {menuItems.find(m => m.key === selectedKey)?.label || '管理后台'}
-
-
-
- {user?.username}
-
-
-
-
- {/* Content */}
-
-
-
-
-
- );
-};
-
-export default AdminLayout;
diff --git a/video-gen-app/src/pages/admin/AdminLoginPage.tsx b/video-gen-app/src/pages/admin/AdminLoginPage.tsx
deleted file mode 100644
index 1bf73026..00000000
--- a/video-gen-app/src/pages/admin/AdminLoginPage.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { useState } from 'react';
-import { Button, Card, Form, Input, message, Typography } from 'antd';
-import { UserOutlined, LockOutlined, ThunderboltOutlined } from '@ant-design/icons';
-import { useNavigate } from 'react-router-dom';
-import { useAuthStore } from '../../store/useAuthStore';
-
-const AdminLoginPage = () => {
- const navigate = useNavigate();
- const { login } = useAuthStore();
- const [loading, setLoading] = useState(false);
-
- const handleLogin = async (values: { username: string; password: string }) => {
- setLoading(true);
- try {
- await login(values.username, values.password);
- message.success('登录成功');
- navigate('/admin');
- } catch {
- message.error('登录失败');
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
- {/* Logo */}
-
-
-
-
-
- VideoGen.AI
-
-
管理后台
-
-
-
- } />
-
-
- } />
-
-
-
-
-
-
-
-
- 演示账号: admin / admin123
-
-
-
-
- );
-};
-
-export default AdminLoginPage;
diff --git a/video-gen-app/src/pages/admin/AdminModels.tsx b/video-gen-app/src/pages/admin/AdminModels.tsx
deleted file mode 100644
index cba8d8b9..00000000
--- a/video-gen-app/src/pages/admin/AdminModels.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
-} from 'antd';
-import {
- RobotOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
-} from '@ant-design/icons';
-import { getModelConfigs, saveModelConfig, deleteModelConfig } from '../../api';
-import type { ModelConfig } from '../../types';
-
-const AdminModels: React.FC = () => {
- const [models, setModels] = useState([]);
- const [loading, setLoading] = useState(true);
- const [modal, setModal] = useState<{ open: boolean; model: ModelConfig | null }>({ open: false, model: null });
- const [form] = Form.useForm();
-
- const load = async () => {
- setLoading(true);
- const data = await getModelConfigs();
- setModels(data);
- setLoading(false);
- };
-
- useEffect(() => { load(); }, []);
-
- const handleSave = async () => {
- try {
- const values = await form.validateFields();
- await saveModelConfig({
- ...modal.model,
- ...values,
- id: modal.model?.id,
- });
- message.success(modal.model?.id ? '模型配置已更新' : '模型配置已添加');
- setModal({ open: false, model: null });
- form.resetFields();
- load();
- } catch { /* validation */ }
- };
-
- const handleDelete = async (id: string) => {
- await deleteModelConfig(id);
- message.success('模型配置已删除');
- load();
- };
-
- const openEdit = (model?: ModelConfig) => {
- setModal({ open: true, model: model || null });
- if (model) {
- form.setFieldsValue(model);
- } else {
- form.resetFields();
- form.setFieldsValue({
- provider: 'sdk',
- weight: 1,
- maxTokens: 4096,
- temperature: 0.7,
- isActive: true,
- priority: 0,
- });
- }
- };
-
- const columns = [
- {
- title: '模型名称', dataIndex: 'name', width: 150,
- render: (v: string, r: ModelConfig) => (
-
-
-
-
-
-
- ),
- },
- {
- title: '提供商', dataIndex: 'provider', width: 140,
- render: (v: string) => {
- const labelMap: Record = {
- sdk: 'SDK模式',
- openai_compatible: 'OpenAI兼容',
- mock: 'Mock模式',
- };
- return {labelMap[v] || v};
- },
- },
- {
- title: 'API地址', dataIndex: 'apiBase', width: 200,
- render: (v: string) => (
-
- {v || '-'}
-
- ),
- },
- {
- title: '权重', dataIndex: 'weight', width: 80, sorter: (a: ModelConfig, b: ModelConfig) => a.weight - b.weight,
- },
- {
- title: 'Max Tokens', dataIndex: 'maxTokens', width: 100,
- },
- {
- title: 'Temperature', dataIndex: 'temperature', width: 100,
- render: (v: number) => v.toFixed(1),
- },
- {
- title: '状态', dataIndex: 'isActive', width: 80,
- render: (v: boolean) => (
- {v ? '启用' : '停用'}
- ),
- },
- {
- title: '操作', key: 'action', width: 150, fixed: 'right' as const,
- render: (_: any, r: ModelConfig) => (
-
- }
- onClick={() => openEdit(r)}>
- 编辑
-
- handleDelete(r.id)}>
- }>删除
-
-
- ),
- },
- ];
-
- return (
-
- );
-};
-
-export default AdminModels;
diff --git a/video-gen-app/src/pages/admin/AdminNotificationManager.tsx b/video-gen-app/src/pages/admin/AdminNotificationManager.tsx
deleted file mode 100644
index e6b64021..00000000
--- a/video-gen-app/src/pages/admin/AdminNotificationManager.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button, Card, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message,
-} from 'antd';
-import {
- BellOutlined, PlusOutlined, DeleteOutlined, SendOutlined,
-} from '@ant-design/icons';
-
-interface NotificationRecord {
- id: string;
- title: string;
- content: string;
- type: string;
- target: string;
- createdAt: string;
-}
-
-const MOCK_NOTIFICATIONS: NotificationRecord[] = [
- { id: 'n-1', title: '系统上线通知', content: 'VideoGen.AI 平台正式上线!', type: 'system', target: '全部用户', createdAt: '2026-05-01 09:00:00' },
- { id: 'n-2', title: '积分充值优惠', content: '限时活动:充值进阶包额外赠送200积分', type: 'credit', target: '全部用户', createdAt: '2026-05-03 10:00:00' },
- { id: 'n-3', title: '账户审核通过', content: '您的账户已通过实名审核', type: 'system', target: 'videomaker', createdAt: '2026-05-05 14:00:00' },
-];
-
-const MOCK_USERS = [
- { id: 'u-001', username: 'videomaker' },
- { id: 'u-002', username: 'designer' },
- { id: 'u-003', username: 'marketer' },
- { id: 'u-004', username: 'editor' },
-];
-
-const AdminNotificationManager: React.FC = () => {
- const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS);
- const [modalOpen, setModalOpen] = useState(false);
- const [form] = Form.useForm();
-
- const handleSend = async () => {
- try {
- const values = await form.validateFields();
- const newRecord: NotificationRecord = {
- id: `n-${Date.now()}`,
- title: values.title,
- content: values.content,
- type: values.type,
- target: values.target_user_id
- ? MOCK_USERS.find(u => u.id === values.target_user_id)?.username || '指定用户'
- : '全部用户',
- createdAt: new Date().toLocaleString('zh-CN'),
- };
- setNotifications(prev => [newRecord, ...prev]);
- message.success('消息已发送');
- setModalOpen(false);
- form.resetFields();
- } catch { /* validation */ }
- };
-
- const handleDelete = (id: string) => {
- setNotifications(prev => prev.filter(n => n.id !== id));
- message.success('已删除');
- };
-
- const getTypeColor = (type: string) => {
- switch (type) {
- case 'system': return 'blue';
- case 'credit': return 'orange';
- case 'promo': return 'purple';
- default: return 'default';
- }
- };
-
- const columns = [
- {
- title: '标题', dataIndex: 'title', width: 200,
- render: (v: string) => {v},
- },
- {
- title: '内容', dataIndex: 'content', ellipsis: true,
- },
- {
- title: '类型', dataIndex: 'type', width: 80,
- render: (v: string) => {
- const labels: Record = { system: '系统', credit: '积分', promo: '活动' };
- return {labels[v] || v};
- },
- },
- {
- title: '发送目标', dataIndex: 'target', width: 120,
- render: (v: string) => (
- {v}
- ),
- },
- {
- title: '发送时间', dataIndex: 'createdAt', width: 160,
- },
- {
- title: '操作', key: 'action', width: 80,
- render: (_: any, r: NotificationRecord) => (
- handleDelete(r.id)}>
- }>删除
-
- ),
- },
- ];
-
- return (
-
-
-
-
-
- 消息推送管理
- {notifications.length} 条消息
-
- } onClick={() => setModalOpen(true)}
- style={{ borderRadius: 8 }}>
- 发送新消息
-
-
-
- `共 ${t} 条消息` }}
- scroll={{ x: 800 }}
- />
-
-
- {/* Send Notification Modal */}
- 发送消息}
- open={modalOpen}
- onOk={handleSend}
- onCancel={() => { setModalOpen(false); form.resetFields(); }}
- okText="发送" cancelText="取消" width={520}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default AdminNotificationManager;
diff --git a/video-gen-app/src/pages/admin/AdminNotifications.tsx b/video-gen-app/src/pages/admin/AdminNotifications.tsx
deleted file mode 100644
index 1ebd4980..00000000
--- a/video-gen-app/src/pages/admin/AdminNotifications.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button, Card, Empty, Space, Tag, Typography,
-} from 'antd';
-import {
- BellOutlined, CheckOutlined, InfoCircleOutlined, CreditCardOutlined, ExclamationCircleOutlined,
-} from '@ant-design/icons';
-import { getNotifications } from '../../api';
-import type { AdminNotification } from '../../types';
-
-const AdminNotifications: React.FC = () => {
- const [notifications, setNotifications] = useState([]);
- const [loading, setLoading] = useState(true);
-
- const load = async () => {
- setLoading(true);
- const data = await getNotifications();
- setNotifications(data);
- setLoading(false);
- };
-
- useEffect(() => { load(); }, []);
-
- const getTypeIcon = (type: string) => {
- switch (type) {
- case 'system': return ;
- case 'credit': return ;
- default: return ;
- }
- };
-
- const getTypeLabel = (type: string) => {
- switch (type) {
- case 'system': return 系统;
- case 'credit': return 积分;
- default: return 其他;
- }
- };
-
- return (
-
-
-
-
-
- 消息通知
- {notifications.filter(n => !n.isRead).length} 条未读
-
-
-
- {notifications.length === 0 ? (
-
- ) : (
-
- {notifications.map(n => (
-
-
-
- {getTypeIcon(n.type)}
-
-
-
-
- {n.title}
-
- {getTypeLabel(n.type)}
- {!n.isRead && (
- 未读
- )}
-
-
- {n.content}
-
-
- {n.createdAt}
-
-
- {!n.isRead && (
-
}
- style={{ flexShrink: 0, color: '#6366f1' }}
- onClick={() => {
- setNotifications(prev =>
- prev.map(item => item.id === n.id ? { ...item, isRead: true } : item)
- );
- }}>
- 标记已读
-
- )}
-
-
- ))}
-
- )}
-
-
- );
-};
-
-export default AdminNotifications;
diff --git a/video-gen-app/src/pages/admin/AdminPaymentConfig.tsx b/video-gen-app/src/pages/admin/AdminPaymentConfig.tsx
deleted file mode 100644
index 4965cabb..00000000
--- a/video-gen-app/src/pages/admin/AdminPaymentConfig.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import React, { useState } from 'react';
-import {
- Button, Card, Form, Input, message, Switch, Typography, Divider,
-} from 'antd';
-import {
- SaveOutlined, WechatOutlined, AlipayCircleOutlined, DollarOutlined,
-} from '@ant-design/icons';
-
-interface PaymentSetting {
- key: string;
- value: string;
- label: string;
- description: string;
- secret?: boolean;
-}
-
-const AdminPaymentConfig: React.FC = () => {
- const [saving, setSaving] = useState(false);
- const [wechatEnabled, setWechatEnabled] = useState(false);
- const [alipayEnabled, setAlipayEnabled] = useState(false);
- const [form] = Form.useForm();
-
- const handleSave = async () => {
- try {
- const values = await form.validateFields();
- setSaving(true);
- await new Promise(r => setTimeout(r, 500));
- message.success('支付配置已保存');
- setSaving(false);
- } catch { setSaving(false); }
- };
-
- return (
-
- {/* WeChat Pay */}
-
-
-
-
-
- 微信支付
- 微信商户号支付配置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Alipay */}
-
-
-
-
-
- 支付宝
- 支付宝应用支付配置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Recharge Packages */}
-
充值套餐}>
- {[
- { name: '体验包', credits: 500, price: 49, color: '#f59e0b' },
- { name: '进阶包', credits: 2000, price: 168, color: '#6366f1' },
- { name: '专业包', credits: 5000, price: 388, color: '#06b6d4' },
- { name: '企业包', credits: 20000, price: 1280, color: '#10b981' },
- ].map(p => (
-
-
-
- ¥{p.price}
- {p.credits.toLocaleString()} 积分
- ({(p.price / p.credits * 100).toFixed(1)}元/百积分)
-
-
- ))}
-
-
-
- } onClick={handleSave} loading={saving}
- size="large" style={{ borderRadius: 8, minWidth: 140 }}>
- 保存配置
-
-
-
- );
-};
-
-export default AdminPaymentConfig;
diff --git a/video-gen-app/src/pages/admin/AdminSettings.tsx b/video-gen-app/src/pages/admin/AdminSettings.tsx
deleted file mode 100644
index 7ee42630..00000000
--- a/video-gen-app/src/pages/admin/AdminSettings.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button, Card, Form, Input, message, Space, Typography,
-} from 'antd';
-import {
- SettingOutlined, SaveOutlined,
-} from '@ant-design/icons';
-import { getSystemConfigs, updateSystemConfig } from '../../api';
-import type { SystemConfig } from '../../types';
-
-const AdminSettings: React.FC = () => {
- const [configs, setConfigs] = useState([]);
- const [loading, setLoading] = useState(true);
- const [saving, setSaving] = useState(false);
- const [form] = Form.useForm();
-
- useEffect(() => {
- const load = async () => {
- setLoading(true);
- const data = await getSystemConfigs();
- setConfigs(data);
- const formValues: Record = {};
- data.forEach(c => { formValues[c.key] = c.value; });
- form.setFieldsValue(formValues);
- setLoading(false);
- };
- load();
- }, []);
-
- const handleSave = async () => {
- try {
- const values = await form.validateFields();
- setSaving(true);
- for (const config of configs) {
- const newVal = values[config.key];
- if (newVal !== config.value) {
- await updateSystemConfig(config.id, newVal);
- }
- }
- message.success('系统配置已保存');
- const data = await getSystemConfigs();
- setConfigs(data);
- setSaving(false);
- } catch {
- setSaving(false);
- }
- };
-
- const groupedConfigs: Record = {
- '站点信息': configs.filter(c => c.key.startsWith('site_')),
- 'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
- };
-
- const getFieldDescription = (config: SystemConfig): string => {
- const descMap: Record = {
- site_name: '平台显示名称,将展示在页面标题和导航栏',
- site_logo: '平台Logo图片URL,建议尺寸 200x40px',
- seo_title: '搜索引擎结果中显示的标题',
- seo_description: '搜索引擎结果中显示的描述文字,建议150字以内',
- seo_keywords: '用逗号分隔的关键词列表',
- };
- return descMap[config.key] || config.description || '';
- };
-
- const getFieldComponent = (config: SystemConfig) => {
- if (config.key === 'seo_description') {
- return ;
- }
- if (config.key === 'seo_keywords') {
- return ;
- }
- return ;
- };
-
- if (loading) {
- return ;
- }
-
- return (
-
-
-
-
-
-
-
- 系统设置
- 管理站点基础信息和SEO配置
-
-
-
-
-
-
-
- } onClick={handleSave} loading={saving}
- size="large" style={{ borderRadius: 8, minWidth: 140 }}>
- 保存配置
-
-
-
- );
-};
-
-export default AdminSettings;
diff --git a/video-gen-app/src/pages/admin/AdminUsers.tsx b/video-gen-app/src/pages/admin/AdminUsers.tsx
deleted file mode 100644
index 04ea6986..00000000
--- a/video-gen-app/src/pages/admin/AdminUsers.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Space, Switch, Table, Tag, Typography,
-} from 'antd';
-import {
- UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined,
-} from '@ant-design/icons';
-import { getAdminUsers, adjustCredits, toggleUserStatus } from '../../api';
-import type { AdminUser } from '../../types';
-
-const AdminUsers: React.FC = () => {
- const [users, setUsers] = useState([]);
- const [loading, setLoading] = useState(true);
- const [search, setSearch] = useState('');
- const [creditModal, setCreditModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null });
- const [form] = Form.useForm();
-
- const load = async () => {
- setLoading(true);
- const data = await getAdminUsers(search || undefined);
- setUsers(data);
- setLoading(false);
- };
-
- useEffect(() => { load(); }, []);
-
- const handleSearch = () => load();
-
- const handleAdjustCredits = async () => {
- try {
- const values = await form.validateFields();
- const { user } = creditModal;
- if (!user) return;
- await adjustCredits(user.id, values.amount, values.reason);
- message.success(`已${values.amount > 0 ? '增加' : '扣除'} ${Math.abs(values.amount)} 积分`);
- setCreditModal({ open: false, user: null });
- form.resetFields();
- load();
- } catch { /* validation */ }
- };
-
- const handleToggleStatus = async (user: AdminUser) => {
- await toggleUserStatus(user.id, !user.isActive);
- message.success(user.isActive ? '已禁用该用户' : '已启用该用户');
- load();
- };
-
- const columns = [
- {
- title: '用户', key: 'user', width: 200,
- render: (_: any, r: AdminUser) => (
-
-
- {r.username.charAt(0).toUpperCase()}
-
-
-
- {r.username}
- {r.isAdmin && 管理员}
-
-
{r.email}
-
-
- ),
- },
- {
- title: '积分余额', dataIndex: 'credits', width: 120, sorter: (a: AdminUser, b: AdminUser) => a.credits - b.credits,
- render: (v: number) => (
- 0 ? '#10b981' : '#ef4444', fontSize: 15 }}>
- {v.toLocaleString()}
-
- ),
- },
- {
- title: '手机号', dataIndex: 'phone', width: 130,
- render: (v: string) => {v || '-'},
- },
- {
- title: '状态', dataIndex: 'isActive', width: 80,
- render: (v: boolean) => (
- {v ? '正常' : '禁用'}
- ),
- },
- {
- title: '注册时间', dataIndex: 'createdAt', width: 120,
- render: (v: string) => {v},
- },
- {
- title: '最后登录', dataIndex: 'lastLoginAt', width: 140,
- render: (v: string) => {v || '-'},
- },
- {
- title: '操作', key: 'action', width: 200, fixed: 'right' as const,
- render: (_: any, r: AdminUser) => (
-
- }
- onClick={() => { setCreditModal({ open: true, user: r }); form.resetFields(); }}>
- 调整积分
-
- {!r.isAdmin && (
- handleToggleStatus(r)}
- >
- : }>
- {r.isActive ? '禁用' : '启用'}
-
-
- )}
-
- ),
- },
- ];
-
- return (
-
-
- {/* Search bar */}
-
- }
- value={search}
- onChange={e => setSearch(e.target.value)}
- onPressEnter={handleSearch}
- style={{ width: 280, borderRadius: 8 }}
- allowClear
- />
-
-
-
- `共 ${t} 个用户` }}
- scroll={{ x: 900 }}
- />
-
-
- {/* Adjust Credits Modal */}
- 调整积分 - {creditModal.user?.username}}
- open={creditModal.open}
- onOk={handleAdjustCredits}
- onCancel={() => { setCreditModal({ open: false, user: null }); form.resetFields(); }}
- okText="确认" cancelText="取消" width={440}
- >
-
- 当前积分:
-
- {creditModal.user?.credits.toLocaleString()}
-
-
-
- `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
- />
-
-
-
-
-
-
-
- );
-};
-
-export default AdminUsers;
diff --git a/video-gen-app/src/pages/admin/AdminVideoEngines.tsx b/video-gen-app/src/pages/admin/AdminVideoEngines.tsx
deleted file mode 100644
index 83e1dff2..00000000
--- a/video-gen-app/src/pages/admin/AdminVideoEngines.tsx
+++ /dev/null
@@ -1,214 +0,0 @@
-import React, { useState } from 'react';
-import {
- Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
-} from 'antd';
-import {
- PlayCircleOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
-} from '@ant-design/icons';
-
-interface VideoEngine {
- id: string;
- name: string;
- provider: string;
- apiBase: string;
- apiKey: string;
- modelName: string;
- supportedRatios: string[];
- supportedResolutions: string[];
- maxDuration: number;
- isActive: boolean;
- priority: number;
-}
-
-const MOCK_ENGINES: VideoEngine[] = [
- {
- id: 've-1', name: 'Seedance 2.0', provider: 'seedance',
- apiBase: 'https://ark.cn-beijing.volces.com/api/v3',
- apiKey: 'sk-****', modelName: 'seedance-2.0',
- supportedRatios: ['16:9', '9:16', '1:1', '4:3'],
- supportedResolutions: ['720p', '1080p', '4K'],
- maxDuration: 60, isActive: true, priority: 1,
- },
-];
-
-const AdminVideoEngines: React.FC = () => {
- const [engines, setEngines] = useState(MOCK_ENGINES);
- const [modal, setModal] = useState<{ open: boolean; engine: VideoEngine | null }>({ open: false, engine: null });
- const [form] = Form.useForm();
-
- const handleSave = async () => {
- try {
- const values = await form.validateFields();
- if (modal.engine) {
- setEngines(prev => prev.map(e => e.id === modal.engine!.id ? { ...e, ...values } : e));
- message.success('已更新');
- } else {
- const newEngine: VideoEngine = {
- id: `ve-${Date.now()}`,
- ...values,
- };
- setEngines(prev => [...prev, newEngine]);
- message.success('已添加');
- }
- setModal({ open: false, engine: null });
- form.resetFields();
- } catch { /* validation */ }
- };
-
- const handleDelete = (id: string) => {
- setEngines(prev => prev.filter(e => e.id !== id));
- message.success('已删除');
- };
-
- const openEdit = (engine?: VideoEngine) => {
- setModal({ open: true, engine: engine || null });
- if (engine) {
- form.setFieldsValue(engine);
- } else {
- form.resetFields();
- form.setFieldsValue({
- isActive: true, priority: 0, maxDuration: 60,
- supportedRatios: ['16:9', '9:16', '1:1'],
- supportedResolutions: ['720p', '1080p'],
- });
- }
- };
-
- const columns = [
- {
- title: '引擎名称', key: 'name', width: 180,
- render: (_: any, r: VideoEngine) => (
-
-
-
-
{r.name}
-
{r.provider}
-
-
- ),
- },
- {
- title: 'API地址', dataIndex: 'apiBase', width: 250,
- render: (v: string) => {v},
- },
- {
- title: '支持比例', dataIndex: 'supportedRatios', width: 180,
- render: (ratios: string[]) => ratios.map(r => {r}),
- },
- {
- title: '支持分辨率', dataIndex: 'supportedResolutions', width: 150,
- render: (res: string[]) => res.map(r => {r}),
- },
- {
- title: '最大时长', dataIndex: 'maxDuration', width: 80,
- render: (v: number) => `${v}s`,
- },
- {
- title: '状态', dataIndex: 'isActive', width: 80,
- render: (v: boolean) => {v ? '启用' : '停用'},
- },
- {
- title: '操作', key: 'action', width: 150, fixed: 'right' as const,
- render: (_: any, r: VideoEngine) => (
-
- } onClick={() => openEdit(r)}>编辑
- handleDelete(r.id)}>
- }>删除
-
-
- ),
- },
- ];
-
- return (
-
-
-
-
-
- 视频引擎配置
- {engines.length} 个引擎
-
-
} onClick={() => openEdit()} style={{ borderRadius: 8 }}>
- 添加引擎
-
-
-
-
-
-
-
{modal.engine ? '编辑引擎' : '添加引擎'}}
- open={modal.open}
- onOk={handleSave}
- onCancel={() => { setModal({ open: false, engine: null }); form.resetFields(); }}
- okText="保存" cancelText="取消" width={560}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default AdminVideoEngines;