feat: generate video

This commit is contained in:
2025-07-05 14:26:57 +08:00
parent d50815fb96
commit 7b5b73fb1a
4 changed files with 111 additions and 12 deletions
+57 -11
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
ArrowLeft,
@@ -12,7 +12,8 @@ import {
Clipboard,
ShieldAlert,
Loader2,
Star
Star,
Video
} from 'lucide-react';
import { format, differenceInMinutes } from 'date-fns';
import TimelineSlider from './components/TimelineSlider';
@@ -111,6 +112,9 @@ export default function HostDetail() {
const [expandedCredentials, setExpandedCredentials] = useState<string[]>([]);
const [revealedPasswords, setRevealedPasswords] = useState<string[]>([]);
// 生成视频相关状态
const [generatingVideo, setGeneratingVideo] = useState(false);
// 日历和滑块状态
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const [timeRange, setTimeRange] = useState({ min: 0, max: 0 });
@@ -314,6 +318,32 @@ export default function HostDetail() {
}
};
// 生成视频
const generateVideo = async () => {
if (!showDetailTimeline || !timeRange.min || !timeRange.max) {
alert('请先选择时间范围');
return;
}
try {
setGeneratingVideo(true);
const startTime = Math.floor(timeRange.min / 1000);
const endTime = Math.floor(timeRange.max / 1000);
const videoUrl = `/api/generate/video?hostname=${encodeURIComponent(hostname)}&startTime=${startTime}&endTime=${endTime}`;
// 在新标签页中打开视频
window.open(videoUrl, '_blank');
} catch (error) {
console.error('生成视频失败:', error);
alert('生成视频失败,请稍后重试');
} finally {
setGeneratingVideo(false);
}
};
// 组织凭据数据按用户分组
const credentialsByUser = useMemo<UserGroup[]>(() => {
const userMap = new Map<string, Credential[]>();
@@ -786,15 +816,31 @@ export default function HostDetail() {
<div className="mb-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-medium text-gray-900 dark:text-white">时间点详情</h2>
<button
onClick={() => {
const selectedSec = Math.floor(hourlySliderValue / 3600000) * 3600;
fetchHourlyRecords(selectedSec, selectedSec + 3600);
}}
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800"
>
<RefreshCcw className={`h-5 w-5 text-gray-600 dark:text-gray-400 ${loadingRecords ? 'animate-spin' : ''}`} />
</button>
<div className="flex items-center gap-2">
<button
onClick={generateVideo}
disabled={generatingVideo}
className="flex items-center gap-2 px-3 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white text-sm rounded focus:outline-none transition-colors"
title="生成视频"
>
{generatingVideo ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Video className="h-4 w-4" />
)}
{generatingVideo ? '生成中...' : '生成视频'}
</button>
<button
onClick={() => {
const selectedSec = Math.floor(hourlySliderValue / 3600000) * 3600;
fetchHourlyRecords(selectedSec, selectedSec + 3600);
}}
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800"
title="刷新"
>
<RefreshCcw className={`h-5 w-5 text-gray-600 dark:text-gray-400 ${loadingRecords ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{records.length > 0 ? (