feat: generate video
This commit is contained in:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user