This commit is contained in:
root 2025-07-11 01:36:13 +08:00
parent 50ca8c1e3a
commit 01b6603eec
6 changed files with 115 additions and 69 deletions

View File

@ -28,4 +28,5 @@ tauri-plugin-fs = "2"
tauri-plugin-shell = "2"
tokio = { version = "1.0", features = ["full"] }
uuid = { version = "1.0", features = ["v4"] }
base64 = "0.21"
anyhow = "1.0"

View File

@ -0,0 +1,42 @@
use std::fs;
use std::path::Path;
use base64::{Engine as _, engine::general_purpose};
use tauri::command;
/// 读取图片文件并返回base64编码的数据URL
#[command]
pub async fn read_image_as_data_url(file_path: String) -> Result<String, String> {
let path = Path::new(&file_path);
// 检查文件是否存在
if !path.exists() {
return Err("File does not exist".to_string());
}
// 检查是否是图片文件
let extension = path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase())
.unwrap_or_default();
let mime_type = match extension.as_str() {
"jpg" | "jpeg" => "image/jpeg",
"png" => "image/png",
"gif" => "image/gif",
"bmp" => "image/bmp",
"webp" => "image/webp",
"svg" => "image/svg+xml",
_ => return Err("Unsupported image format".to_string()),
};
// 读取文件内容
match fs::read(&file_path) {
Ok(file_data) => {
// 编码为base64
let base64_data = general_purpose::STANDARD.encode(&file_data);
// 返回data URL格式
Ok(format!("data:{};base64,{}", mime_type, base64_data))
}
Err(e) => Err(format!("Failed to read file: {}", e)),
}
}

View File

@ -5,6 +5,7 @@ pub mod ai_video;
pub mod file_system;
pub mod project;
pub mod template;
pub mod file_utils;
pub mod resource_category;
// Re-export all commands for easy access

View File

@ -55,7 +55,8 @@ pub fn run() {
commands::update_project,
commands::delete_project,
commands::search_projects,
commands::open_project_directory
commands::open_project_directory,
commands::file_utils::read_image_as_data_url
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -30,8 +30,6 @@ const Sidebar: React.FC = () => {
{ path: '/settings', icon: Settings, label: '设置' },
]
const isAIVideoPage = location.pathname === '/ai-video'
const getStatusIcon = (status: string) => {
switch (status) {
case 'processing': return <Clock size={14} className="text-blue-500" />
@ -61,24 +59,6 @@ const Sidebar: React.FC = () => {
>
</button>
{isAIVideoPage && (
<button
onClick={() => setActiveTab('tasks')}
className={clsx(
'flex-1 px-4 py-3 text-sm font-medium border-b-2 transition-colors relative',
activeTab === 'tasks'
? 'border-primary-500 text-primary-600 bg-primary-50'
: 'border-transparent text-secondary-600 hover:text-secondary-900 hover:bg-secondary-50'
)}
>
{jobs.length > 0 && (
<span className="absolute -top-1 -right-1 bg-primary-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{jobs.length}
</span>
)}
</button>
)}
</div>
</div>

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'
import { Plus, Edit, Trash2, Search, Save, X, FolderOpen, Package } from 'lucide-react'
import { open } from '@tauri-apps/plugin-dialog'
import { convertFileSrc } from '@tauri-apps/api/core'
import { invoke } from '@tauri-apps/api/core'
import { ProjectService, Project } from '../services/projectService'
const ProjectManagePage: React.FC = () => {
@ -17,16 +17,67 @@ const ProjectManagePage: React.FC = () => {
product_image: ''
})
// 辅助函数转换图片路径为Tauri可访问的URL
const getImageSrc = (imagePath: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
try {
return convertFileSrc(imagePath)
} catch (error) {
console.error('Failed to convert file src:', error)
return ''
// 图片组件:处理异步图片加载
const ImageComponent: React.FC<{
imagePath: string
alt: string
className: string
onError?: () => void
}> = ({ imagePath, alt, className, onError }) => {
const [imageSrc, setImageSrc] = useState<string>('')
const [loading, setLoading] = useState(true)
useEffect(() => {
const loadImage = async () => {
if (!imagePath) {
setLoading(false)
return
}
if (imagePath.startsWith('http') || imagePath.startsWith('data:')) {
setImageSrc(imagePath)
setLoading(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)
onError?.()
} finally {
setLoading(false)
}
}
loadImage()
}, [imagePath])
if (loading) {
return (
<div className={`${className} bg-gray-200 flex items-center justify-center`}>
<div className="text-xs text-gray-400">...</div>
</div>
)
}
if (!imageSrc) {
return (
<div className={`${className} bg-gray-200 flex items-center justify-center`}>
<Package size={32} className="text-gray-400" />
</div>
)
}
return (
<img
src={imageSrc}
alt={alt}
className={className}
onError={onError}
/>
)
}
useEffect(() => {
@ -219,30 +270,11 @@ const ProjectManagePage: React.FC = () => {
<div key={project.id} className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
{/* 商品图片 */}
<div className="h-32 bg-gray-100 overflow-hidden">
{project.product_image ? (
<>
<img
src={getImageSrc(project.product_image)}
alt={project.product_name || project.name}
className="w-full h-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
const fallback = target.parentElement?.querySelector('.image-fallback') as HTMLElement
if (fallback) {
fallback.style.display = 'flex'
}
}}
/>
<div className="image-fallback hidden w-full h-full bg-gray-200 flex items-center justify-center">
<Package size={32} className="text-gray-400" />
</div>
</>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
<Package size={32} className="text-gray-400" />
</div>
)}
<ImageComponent
imagePath={project.product_image}
alt={project.product_name || project.name}
className="w-full h-full object-cover"
/>
</div>
<div className="p-6">
@ -411,23 +443,12 @@ const ProjectManagePage: React.FC = () => {
{formData.product_image && (
<div className="mt-2">
<p className="text-xs text-gray-500 mb-1">:</p>
<div className="w-20 h-20 border border-gray-200 rounded-lg overflow-hidden bg-gray-50 flex items-center justify-center">
<img
src={getImageSrc(formData.product_image)}
<div className="w-20 h-20 border border-gray-200 rounded-lg overflow-hidden">
<ImageComponent
imagePath={formData.product_image}
alt="商品图片预览"
className="w-full h-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
const fallback = target.parentElement?.querySelector('.fallback-text') as HTMLElement
if (fallback) {
fallback.style.display = 'block'
}
}}
/>
<div className="fallback-text hidden text-xs text-gray-400 text-center p-2">
</div>
</div>
</div>
)}