mxivideo/src/pages/ProjectManagePage.tsx

449 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { ProjectService, Project } from '../services/projectService'
const ProjectManagePage: React.FC = () => {
const [projects, setProjects] = useState<Project[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
const [editingProject, setEditingProject] = useState<Project | null>(null)
const [showCreateForm, setShowCreateForm] = useState(false)
const [formData, setFormData] = useState({
name: '',
local_directory: '',
product_name: '',
product_image: ''
})
useEffect(() => {
loadProjects()
}, [])
const loadProjects = async () => {
try {
setLoading(true)
const response = await ProjectService.getAllProjects()
console.log(`loadProjects`, response)
if (response.status && response.data) {
setProjects(response.data)
} else {
console.error('Failed to load projects:', response.msg)
}
} catch (error) {
console.error('Failed to load projects:', error)
} finally {
setLoading(false)
}
}
const handleCreateProject = async () => {
try {
const response = await ProjectService.createProject(formData)
console.log(`handleCreateProject`, response)
if (response.status && response.data) {
setProjects([...projects, response.data])
setShowCreateForm(false)
setFormData({ name: '', local_directory: '', product_name: '', product_image: '' })
} else {
console.error('创建失败:', response.msg || '未知错误')
}
} catch (error) {
console.error('Failed to create project:', error)
}
}
const handleUpdateProject = async () => {
if (!editingProject) return
try {
const response = await ProjectService.updateProject(editingProject.id, formData)
if (response.status && response.data) {
const updatedProjects = projects.map(proj =>
proj.id === editingProject.id ? response.data! : proj
)
setProjects(updatedProjects)
setEditingProject(null)
setFormData({ name: '', local_directory: '', product_name: '', product_image: '' })
} else {
console.error('更新失败:', response.msg || '未知错误')
}
} catch (error) {
console.error('Failed to update project:', error)
}
}
const handleDeleteProject = async (projectId: string) => {
if (!confirm('确定要删除这个项目吗?')) return
try {
const response = await ProjectService.deleteProject(projectId)
if (response.status) {
setProjects(projects.filter(proj => proj.id !== projectId))
} else {
console.error('删除失败:', response.msg || '未知错误')
}
} catch (error) {
console.error('Failed to delete project:', error)
}
}
const handleOpenDirectory = async (projectId: string) => {
try {
const response = await ProjectService.openProjectDirectory(projectId)
if (!response.status) {
console.error('打开目录失败:', response.msg || '未知错误')
}
} catch (error) {
console.error('Failed to open directory:', error)
}
}
const selectDirectory = async () => {
try {
const directory = await open({
directory: true,
multiple: false,
title: '选择项目目录'
})
if (directory && typeof directory === 'string') {
setFormData({ ...formData, local_directory: directory })
}
} catch (error) {
console.error('Failed to select directory:', error)
}
}
const selectImage = async () => {
try {
const imagePath = await open({
multiple: false,
title: '选择商品图片',
filters: [
{
name: '图片文件',
extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']
}
]
})
if (imagePath && typeof imagePath === 'string') {
setFormData({ ...formData, product_image: imagePath })
}
} catch (error) {
console.error('Failed to select image:', error)
}
}
const startEdit = (project: Project) => {
setEditingProject(project)
setFormData({
name: project.name,
local_directory: project.local_directory,
product_name: project.product_name,
product_image: project.product_image
})
setShowCreateForm(false)
}
const cancelEdit = () => {
setEditingProject(null)
setShowCreateForm(false)
setFormData({ name: '', local_directory: '', product_name: '', product_image: '' })
}
const filteredProjects = projects.filter(project =>
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.product_name.toLowerCase().includes(searchTerm.toLowerCase())
)
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">...</div>
</div>
)
}
return (
<div className="p-6">
{/* 页面标题 */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-gray-600 mt-2"></p>
</div>
{/* 搜索和创建按钮 */}
<div className="flex items-center justify-between mb-6">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
placeholder="搜索项目..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent w-full"
/>
</div>
<button
onClick={() => {
setShowCreateForm(true)
setEditingProject(null)
setFormData({ name: '', local_directory: '', product_name: '', product_image: '' })
}}
className="ml-4 flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Plus size={20} className="mr-2" />
</button>
</div>
{/* 项目列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredProjects.map((project) => (
<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={project.product_image.startsWith('http') ? project.product_image : `file:///${project.product_image.split('\\').join('/')}`}
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>
)}
</div>
<div className="p-6">
{/* 项目标题 */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">{project.name}</h3>
<div className="flex items-center space-x-2">
<button
onClick={() => handleOpenDirectory(project.id)}
className="p-2 text-gray-400 hover:text-green-600 transition-colors"
title="打开目录"
>
<FolderOpen size={16} />
</button>
<button
onClick={() => startEdit(project)}
className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
title="编辑"
>
<Edit size={16} />
</button>
<button
onClick={() => handleDeleteProject(project.id)}
className="p-2 text-gray-400 hover:text-red-600 transition-colors"
title="删除"
>
<Trash2 size={16} />
</button>
</div>
</div>
{/* 项目信息 */}
<div className="space-y-3">
{/* 商品信息 */}
{project.product_name && (
<div>
<p className="text-sm text-gray-600">:</p>
<p className="text-sm text-gray-900">{project.product_name}</p>
</div>
)}
{/* 创建时间 */}
<div className="text-xs text-gray-400 pt-2 border-t border-gray-100">
{new Date(project.created_at).toLocaleDateString()}
</div>
</div>
</div>
</div>
))}
</div>
{/* 空状态 */}
{filteredProjects.length === 0 && (
<div className="text-center py-12">
<div className="text-gray-400 mb-4">
{searchTerm ? '没有找到匹配的项目' : '暂无项目'}
</div>
{!searchTerm && (
<button
onClick={() => setShowCreateForm(true)}
className="text-blue-600 hover:text-blue-700"
>
</button>
)}
</div>
)}
{/* 创建/编辑表单弹窗 */}
{(showCreateForm || editingProject) && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
{/* 表单标题 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">
{editingProject ? '编辑项目' : '新建项目'}
</h2>
<button
onClick={cancelEdit}
className="text-gray-400 hover:text-gray-600"
>
<X size={24} />
</button>
</div>
{/* 表单内容 */}
<div className="p-6 space-y-4">
{/* 项目名 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="请输入项目名称"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* 本地目录 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<div className="flex space-x-2">
<input
type="text"
value={formData.local_directory}
onChange={(e) => setFormData({ ...formData, local_directory: e.target.value })}
placeholder="请选择项目目录路径"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
readOnly
/>
<button
onClick={selectDirectory}
className="px-4 py-2 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors flex items-center space-x-1"
title="选择目录"
>
<FolderOpen size={16} />
<span className="text-sm"></span>
</button>
</div>
</div>
{/* 商品名 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
value={formData.product_name}
onChange={(e) => setFormData({ ...formData, product_name: e.target.value })}
placeholder="请输入商品名称"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* 商品图片 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex space-x-2">
<input
type="text"
value={formData.product_image}
onChange={(e) => setFormData({ ...formData, product_image: e.target.value })}
placeholder="请选择商品图片文件"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
readOnly
/>
<button
onClick={selectImage}
className="px-4 py-2 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition-colors flex items-center space-x-1"
title="选择图片"
>
<Package size={16} />
<span className="text-sm"></span>
</button>
</div>
{/* 图片预览 */}
{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={formData.product_image.startsWith('http') ? formData.product_image : `file:///${formData.product_image.split('\\').join('/')}`}
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>
)}
</div>
</div>
{/* 表单按钮 */}
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200">
<button
onClick={cancelEdit}
className="px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
</button>
<button
onClick={editingProject ? handleUpdateProject : handleCreateProject}
disabled={!formData.name.trim() || !formData.local_directory.trim()}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
<Save size={16} className="mr-2" />
{editingProject ? '保存' : '创建'}
</button>
</div>
</div>
</div>
)}
</div>
)
}
export default ProjectManagePage