This commit is contained in:
root 2025-07-11 01:49:27 +08:00
parent 61782a77b3
commit bdc68bf733
7 changed files with 331 additions and 296 deletions

View File

@ -0,0 +1,44 @@
import React from 'react'
import { Sparkles, Zap, Video } from 'lucide-react'
const FeaturesHighlight: React.FC = () => {
const features = [
{
icon: Sparkles,
title: 'AI 智能剪辑',
description: '使用先进的 AI 技术,自动识别精彩片段,智能生成视频剪辑'
},
{
icon: Zap,
title: '高效处理',
description: '优化的渲染引擎,支持多种格式,快速导出高质量视频'
},
{
icon: Video,
title: '专业工具',
description: '丰富的编辑工具和特效,满足从入门到专业的各种需求'
}
]
return (
<div className="bg-gradient-to-r from-primary-500 to-primary-600 rounded-lg p-8 text-white">
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{features.map((feature, index) => {
const IconComponent = feature.icon
return (
<div key={index} className="text-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-white/20 rounded-lg mb-4">
<IconComponent size={24} />
</div>
<h3 className="font-semibold mb-2">{feature.title}</h3>
<p className="text-primary-100 text-sm">{feature.description}</p>
</div>
)
})}
</div>
</div>
)
}
export default FeaturesHighlight

View File

@ -0,0 +1,35 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { Project } from '../services/projectService'
import ProjectImage from './ProjectImage'
interface ProjectCardProps {
project: Project
formatTimeAgo: (dateString: string) => string
}
const ProjectCard: React.FC<ProjectCardProps> = ({ project, formatTimeAgo }) => {
return (
<Link
to={`/projects/${project.id}`}
className="card overflow-hidden hover:shadow-md transition-shadow cursor-pointer"
>
<div className="aspect-video overflow-hidden">
<ProjectImage
imagePath={project.product_image}
alt={project.name}
className="w-full h-full object-cover"
/>
</div>
<div className="p-4">
<h3 className="font-medium text-secondary-900 mb-1">{project.name}</h3>
<p className="text-sm text-secondary-600">{formatTimeAgo(project.updated_at)}</p>
{project.product_name && (
<p className="text-xs text-secondary-500 mt-1">{project.product_name}</p>
)}
</div>
</Link>
)
}
export default ProjectCard

View File

@ -0,0 +1,56 @@
import React, { useState, useEffect } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { Video } from 'lucide-react'
interface ProjectImageProps {
imagePath: string
alt: string
className: string
}
const ProjectImage: React.FC<ProjectImageProps> = ({ imagePath, alt, className }) => {
const [imageSrc, setImageSrc] = useState<string>('')
const [imageLoading, setImageLoading] = useState(true)
useEffect(() => {
const loadImage = async () => {
if (!imagePath) {
setImageLoading(false)
return
}
if (imagePath.startsWith('http') || imagePath.startsWith('data:')) {
setImageSrc(imagePath)
setImageLoading(false)
return
}
try {
const dataUrl = await invoke<string>('read_image_as_data_url', { filePath: imagePath })
setImageSrc(dataUrl)
} catch (error) {
console.error('Failed to read image:', error)
} finally {
setImageLoading(false)
}
}
loadImage()
}, [imagePath])
if (imageLoading) {
return <div className={`${className} bg-secondary-200 animate-pulse`}></div>
}
if (!imageSrc) {
return (
<div className={`${className} bg-secondary-200 flex items-center justify-center`}>
<Video className="text-secondary-400" size={48} />
</div>
)
}
return <img src={imageSrc} alt={alt} className={className} />
}
export default ProjectImage

View File

@ -0,0 +1,50 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { FolderOpen, Music, Zap, Sparkles, Database, LucideIcon } from 'lucide-react'
interface QuickAction {
icon: LucideIcon
label: string
description: string
path: string
}
const QuickActions: React.FC = () => {
const quickActions: QuickAction[] = [
{ icon: Sparkles, label: 'AI 视频生成', description: '使用 AI 将图片转换为动态视频', path: '/ai-video' },
{ icon: Music, label: '音频处理', description: '处理音频文件,添加效果', path: '/audio' },
{ icon: Zap, label: 'AI 自动剪辑', description: '使用 AI 自动生成视频剪辑', path: '/editor' },
{ icon: FolderOpen, label: '导入媒体', description: '导入视频、音频和图片文件', path: '/media' },
{ icon: Database, label: 'KV 存储测试', description: '测试 Cloudflare KV 键值存储功能', path: '/kv-test' },
]
return (
<div>
<h2 className="text-xl font-semibold text-secondary-900 mb-6"></h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{quickActions.map((action, index) => {
const IconComponent = action.icon
return (
<Link
key={index}
to={action.path}
className="card p-6 hover:shadow-md transition-shadow group"
>
<div className="flex items-start space-x-4">
<div className="p-3 bg-primary-100 rounded-lg group-hover:bg-primary-200 transition-colors">
<IconComponent className="text-primary-600" size={24} />
</div>
<div className="flex-1">
<h3 className="font-medium text-secondary-900 mb-1">{action.label}</h3>
<p className="text-sm text-secondary-600">{action.description}</p>
</div>
</div>
</Link>
)
})}
</div>
</div>
)
}
export default QuickActions

View File

@ -0,0 +1,113 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Plus, Video } from 'lucide-react'
import { ProjectService, Project } from '../services/projectService'
import ProjectCard from './ProjectCard'
const RecentProjects: React.FC = () => {
const [recentProjects, setRecentProjects] = useState<Project[]>([])
const [loading, setLoading] = useState(true)
// 加载最近的项目
useEffect(() => {
const loadRecentProjects = async () => {
try {
const response = await ProjectService.getAllProjects()
if (response.status && response.data) {
// 按更新时间排序取最近的3个项目
const sortedProjects = response.data
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
.slice(0, 3)
setRecentProjects(sortedProjects)
}
} catch (error) {
console.error('Failed to load recent projects:', error)
} finally {
setLoading(false)
}
}
loadRecentProjects()
}, [])
// 格式化时间显示
const formatTimeAgo = (dateString: string): string => {
const now = new Date()
const date = new Date(dateString)
const diffInMs = now.getTime() - date.getTime()
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60))
const diffInDays = Math.floor(diffInHours / 24)
if (diffInHours < 1) {
return '刚刚'
} else if (diffInHours < 24) {
return `${diffInHours} 小时前`
} else if (diffInDays < 7) {
return `${diffInDays} 天前`
} else {
return date.toLocaleDateString('zh-CN')
}
}
// 加载状态骨架屏
const LoadingSkeleton = () => (
<>
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="card overflow-hidden">
<div className="aspect-video bg-secondary-200 animate-pulse"></div>
<div className="p-4">
<div className="h-4 bg-secondary-200 rounded animate-pulse mb-2"></div>
<div className="h-3 bg-secondary-200 rounded animate-pulse w-2/3"></div>
</div>
</div>
))}
</>
)
// 空状态
const EmptyState = () => (
<div className="col-span-full text-center py-8">
<Video className="mx-auto text-secondary-400 mb-4" size={48} />
<p className="text-secondary-600 mb-4"></p>
<Link
to="/projects"
className="btn-primary inline-flex items-center"
>
<Plus size={16} className="mr-2" />
</Link>
</div>
)
return (
<div>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-secondary-900"></h2>
<Link
to="/projects"
className="text-primary-600 hover:text-primary-700 text-sm font-medium"
>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{loading ? (
<LoadingSkeleton />
) : recentProjects.length > 0 ? (
recentProjects.map((project) => (
<ProjectCard
key={project.id}
project={project}
formatTimeAgo={formatTimeAgo}
/>
))
) : (
<EmptyState />
)}
</div>
</div>
)
}
export default RecentProjects

View File

@ -0,0 +1,33 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { Plus } from 'lucide-react'
const WelcomeSection: React.FC = () => {
return (
<div className="text-center py-8">
<h1 className="text-4xl font-bold text-secondary-900 mb-4">
使 MixVideo V2
</h1>
<p className="text-lg text-secondary-600 mb-8">
</p>
<div className="flex items-center justify-center space-x-4">
<Link
to="/projects"
className="btn-primary flex items-center"
>
<Plus size={20} className="mr-2" />
</Link>
<Link
to="/templates"
className="btn-secondary"
>
</Link>
</div>
</div>
)
}
export default WelcomeSection

View File

@ -1,296 +0,0 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Plus, FolderOpen, Video, Music, Zap, Sparkles, Database } from 'lucide-react'
import { invoke } from '@tauri-apps/api/core'
import { ProjectService, Project } from '../services/projectService'
const HomePage: React.FC = () => {
const [recentProjects, setRecentProjects] = useState<Project[]>([])
const [loading, setLoading] = useState(true)
// 加载最近的项目
useEffect(() => {
const loadRecentProjects = async () => {
try {
const response = await ProjectService.getAllProjects()
if (response.status && response.data) {
// 按更新时间排序取最近的3个项目
const sortedProjects = response.data
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
.slice(0, 3)
setRecentProjects(sortedProjects)
}
} catch (error) {
console.error('Failed to load recent projects:', error)
} finally {
setLoading(false)
}
}
loadRecentProjects()
}, [])
// 简化的图片组件
const ProjectImage: React.FC<{ imagePath: string; alt: string; className: string }> = ({
imagePath, alt, className
}) => {
const [imageSrc, setImageSrc] = useState<string>('')
const [imageLoading, setImageLoading] = useState(true)
useEffect(() => {
const loadImage = async () => {
if (!imagePath) {
setImageLoading(false)
return
}
if (imagePath.startsWith('http') || imagePath.startsWith('data:')) {
setImageSrc(imagePath)
setImageLoading(false)
return
}
try {
const dataUrl = await invoke<string>('read_image_as_data_url', { filePath: imagePath })
setImageSrc(dataUrl)
} catch (error) {
console.error('Failed to read image:', error)
} finally {
setImageLoading(false)
}
}
loadImage()
}, [imagePath])
if (imageLoading) {
return <div className={`${className} bg-secondary-200 animate-pulse`}></div>
}
if (!imageSrc) {
return (
<div className={`${className} bg-secondary-200 flex items-center justify-center`}>
<Video className="text-secondary-400" size={48} />
</div>
)
}
return <img src={imageSrc} alt={alt} className={className} />
}
// 格式化时间显示
const formatTimeAgo = (dateString: string): string => {
const now = new Date()
const date = new Date(dateString)
const diffInMs = now.getTime() - date.getTime()
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60))
const diffInDays = Math.floor(diffInHours / 24)
if (diffInHours < 1) {
return '刚刚'
} else if (diffInHours < 24) {
return `${diffInHours} 小时前`
} else if (diffInDays < 7) {
return `${diffInDays} 天前`
} else {
return date.toLocaleDateString('zh-CN')
}
}
const quickActions = [
{ icon: Sparkles, label: 'AI 视频生成', description: '使用 AI 将图片转换为动态视频', path: '/ai-video' },
{ icon: Music, label: '音频处理', description: '处理音频文件,添加效果', path: '/audio' },
{ icon: Zap, label: 'AI 自动剪辑', description: '使用 AI 自动生成视频剪辑', path: '/editor' },
{ icon: FolderOpen, label: '导入媒体', description: '导入视频、音频和图片文件', path: '/media' },
{ icon: Database, label: 'KV 存储测试', description: '测试 Cloudflare KV 键值存储功能', path: '/kv-test' },
]
return (
<div className="p-6 space-y-8">
{/* Welcome Section */}
<div className="text-center py-8">
<h1 className="text-4xl font-bold text-secondary-900 mb-4">
使 MixVideo V2
</h1>
<p className="text-lg text-secondary-600 mb-8">
</p>
<div className="flex items-center space-x-4">
<Link
to="/projects"
className="btn-primary px-8 py-3 text-lg"
>
<Plus className="mr-2" size={20} />
</Link>
</div>
</div>
{/* Quick Actions */}
<div>
<h2 className="text-2xl font-semibold text-secondary-900 mb-4"></h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{quickActions.map((action, index) => {
const Icon = action.icon
return (
<Link
key={index}
to={action.path}
className="card p-6 hover:shadow-md transition-shadow cursor-pointer group block"
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center group-hover:bg-primary-200 transition-colors">
<Icon className="text-primary-600" size={24} />
</div>
<h3 className="font-medium text-secondary-900">{action.label}</h3>
<p className="text-sm text-secondary-600">{action.description}</p>
</div>
</Link>
)
})}
</div>
</div>
{/* Recent Projects */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-semibold text-secondary-900"></h2>
<Link to="/projects" className="btn-ghost px-4 py-2">
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{loading ? (
// 加载状态
Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="card overflow-hidden">
<div className="aspect-video bg-secondary-200 animate-pulse"></div>
<div className="p-4">
<div className="h-4 bg-secondary-200 rounded animate-pulse mb-2"></div>
<div className="h-3 bg-secondary-200 rounded animate-pulse w-2/3"></div>
</div>
</div>
))
) : recentProjects.length > 0 ? (
// 显示真实项目
recentProjects.map((project) => (
<Link
key={project.id}
to={`/projects/${project.id}`}
className="card overflow-hidden hover:shadow-md transition-shadow cursor-pointer"
>
<div className="aspect-video overflow-hidden">
<ProjectImage
imagePath={project.product_image}
alt={project.name}
className="w-full h-full object-cover"
/>
</div>
<div className="p-4">
<h3 className="font-medium text-secondary-900 mb-1">{project.name}</h3>
<p className="text-sm text-secondary-600">{formatTimeAgo(project.updated_at)}</p>
{project.product_name && (
<p className="text-xs text-secondary-500 mt-1">{project.product_name}</p>
)}
</div>
</Link>
))
) : (
// 空状态
<div className="col-span-full text-center py-8">
<Video className="mx-auto text-secondary-400 mb-4" size={48} />
<p className="text-secondary-600 mb-4"></p>
<Link
to="/projects"
className="btn-primary inline-flex items-center"
>
<Plus size={16} className="mr-2" />
</Link>
</div>
)}
</div>
</div>
{/* Features Highlight */}
<div className="bg-gradient-to-r from-primary-500 to-primary-600 rounded-lg p-8 text-white">
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<h3 className="font-semibold mb-2">🎬 </h3>
<p className="text-primary-100"></p>
</div>
<div>
<h3 className="font-semibold mb-2">🎵 </h3>
<p className="text-primary-100"></p>
</div>
<div>
<h3 className="font-semibold mb-2">🤖 AI </h3>
<p className="text-primary-100"></p>
</div>
</div>
</div>
{/* Additional Content for Testing Scroll */}
<div className="space-y-6">
<h2 className="text-2xl font-semibold text-secondary-900"></h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="card p-6">
<h3 className="text-lg font-semibold mb-3"></h3>
<ul className="space-y-2 text-secondary-600">
<li> </li>
<li> </li>
<li> </li>
<li> 绿</li>
<li> </li>
</ul>
</div>
<div className="card p-6">
<h3 className="text-lg font-semibold mb-3"></h3>
<ul className="space-y-2 text-secondary-600">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="card p-6">
<h3 className="text-lg font-semibold mb-3"></h3>
<ul className="space-y-2 text-secondary-600">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="card p-6">
<h3 className="text-lg font-semibold mb-3"></h3>
<ul className="space-y-2 text-secondary-600">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
</div>
{/* Footer */}
<div className="text-center py-8 border-t border-secondary-200">
<p className="text-secondary-600">
© 2025 MixVideo V2.
</p>
</div>
</div>
)
}
export default HomePage