fix
This commit is contained in:
parent
e377ae46ba
commit
aaf96c5aed
|
|
@ -0,0 +1,307 @@
|
|||
"""
|
||||
项目管理服务
|
||||
管理项目信息,包括项目名、本地目录、分类文件夹初始化、商品信息等
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime
|
||||
|
||||
from python_core.config import settings
|
||||
from python_core.utils.logger import logger
|
||||
from python_core.utils.jsonrpc import create_response_handler
|
||||
from python_core.services.resource_category_manager import resource_category_manager
|
||||
|
||||
|
||||
@dataclass
|
||||
class Project:
|
||||
"""项目数据结构"""
|
||||
id: str
|
||||
name: str # 项目名
|
||||
local_directory: str # 本地目录路径
|
||||
product_name: str # 商品名
|
||||
product_image: str # 商品图片路径
|
||||
created_at: str
|
||||
updated_at: str
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
"""项目管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.cache_dir = settings.temp_dir / "cache"
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 项目数据文件
|
||||
self.projects_file = self.cache_dir / "projects.json"
|
||||
self.projects = self._load_projects()
|
||||
|
||||
def _load_projects(self) -> List[Project]:
|
||||
"""加载项目数据"""
|
||||
if self.projects_file.exists():
|
||||
try:
|
||||
with open(self.projects_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return [Project(**item) for item in data]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load projects: {e}")
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
|
||||
def _save_projects(self, projects: List[Project] = None):
|
||||
"""保存项目数据"""
|
||||
if projects is None:
|
||||
projects = self.projects
|
||||
|
||||
try:
|
||||
data = [asdict(project) for project in projects]
|
||||
with open(self.projects_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"Projects saved to {self.projects_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save projects: {e}")
|
||||
raise
|
||||
|
||||
def _initialize_category_folders(self, project_directory: str) -> bool:
|
||||
"""为项目初始化分类文件夹"""
|
||||
try:
|
||||
project_path = Path(project_directory)
|
||||
if not project_path.exists():
|
||||
project_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 获取所有启用的分类
|
||||
active_categories = resource_category_manager.get_all_categories()
|
||||
|
||||
for category in active_categories:
|
||||
if category.get('is_active', True): # 只为启用的分类创建文件夹
|
||||
category_folder = project_path / category['title']
|
||||
category_folder.mkdir(exist_ok=True)
|
||||
logger.info(f"Created category folder: {category_folder}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize category folders: {e}")
|
||||
return False
|
||||
|
||||
def get_all_projects(self) -> List[Dict]:
|
||||
"""获取所有项目"""
|
||||
return [asdict(project) for project in self.projects if project.is_active]
|
||||
|
||||
def get_project_by_id(self, project_id: str) -> Optional[Dict]:
|
||||
"""根据ID获取项目"""
|
||||
for project in self.projects:
|
||||
if project.id == project_id and project.is_active:
|
||||
return asdict(project)
|
||||
return None
|
||||
|
||||
def create_project(self, name: str, local_directory: str, product_name: str = "",
|
||||
product_image: str = "") -> Dict:
|
||||
"""创建新项目"""
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
# 验证目录路径
|
||||
if not os.path.isabs(local_directory):
|
||||
raise ValueError("Local directory must be an absolute path")
|
||||
|
||||
new_project = Project(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
local_directory=local_directory,
|
||||
product_name=product_name,
|
||||
product_image=product_image,
|
||||
created_at=now,
|
||||
updated_at=now
|
||||
)
|
||||
|
||||
# 初始化分类文件夹
|
||||
if not self._initialize_category_folders(local_directory):
|
||||
raise RuntimeError("Failed to initialize category folders")
|
||||
|
||||
self.projects.append(new_project)
|
||||
self._save_projects()
|
||||
|
||||
logger.info(f"Created new project: {name}")
|
||||
return asdict(new_project)
|
||||
|
||||
def update_project(self, project_id: str, name: str = None, local_directory: str = None,
|
||||
product_name: str = None, product_image: str = None) -> Optional[Dict]:
|
||||
"""更新项目"""
|
||||
for i, project in enumerate(self.projects):
|
||||
if project.id == project_id and project.is_active:
|
||||
if name is not None:
|
||||
project.name = name
|
||||
if local_directory is not None:
|
||||
# 验证新目录路径
|
||||
if not os.path.isabs(local_directory):
|
||||
raise ValueError("Local directory must be an absolute path")
|
||||
project.local_directory = local_directory
|
||||
if product_name is not None:
|
||||
project.product_name = product_name
|
||||
if product_image is not None:
|
||||
project.product_image = product_image
|
||||
project.updated_at = datetime.now().isoformat()
|
||||
|
||||
self.projects[i] = project
|
||||
self._save_projects()
|
||||
|
||||
logger.info(f"Updated project: {project_id}")
|
||||
return asdict(project)
|
||||
return None
|
||||
|
||||
def delete_project(self, project_id: str) -> bool:
|
||||
"""删除项目(软删除)"""
|
||||
for i, project in enumerate(self.projects):
|
||||
if project.id == project_id and project.is_active:
|
||||
project.is_active = False
|
||||
project.updated_at = datetime.now().isoformat()
|
||||
|
||||
self.projects[i] = project
|
||||
self._save_projects()
|
||||
|
||||
logger.info(f"Deleted project: {project_id}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def search_projects(self, keyword: str) -> List[Dict]:
|
||||
"""搜索项目"""
|
||||
keyword = keyword.lower()
|
||||
results = []
|
||||
|
||||
for project in self.projects:
|
||||
if (project.is_active and
|
||||
(keyword in project.name.lower() or
|
||||
keyword in project.product_name.lower())):
|
||||
results.append(asdict(project))
|
||||
|
||||
return results
|
||||
|
||||
def open_project_directory(self, project_id: str) -> bool:
|
||||
"""打开项目目录"""
|
||||
project = self.get_project_by_id(project_id)
|
||||
if project and os.path.exists(project['local_directory']):
|
||||
try:
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
directory = project['local_directory']
|
||||
system = platform.system()
|
||||
|
||||
if system == "Windows":
|
||||
subprocess.run(['explorer', directory])
|
||||
elif system == "Darwin": # macOS
|
||||
subprocess.run(['open', directory])
|
||||
else: # Linux
|
||||
subprocess.run(['xdg-open', directory])
|
||||
|
||||
logger.info(f"Opened project directory: {directory}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to open directory: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
# 全局实例
|
||||
project_manager = ProjectManager()
|
||||
|
||||
|
||||
def main():
|
||||
"""命令行接口 - 使用JSON-RPC协议"""
|
||||
import sys
|
||||
import json
|
||||
|
||||
# 创建响应处理器
|
||||
rpc = create_response_handler()
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
rpc.error("INVALID_REQUEST", "No command specified")
|
||||
return
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
try:
|
||||
if command == "get_all_projects":
|
||||
projects = project_manager.get_all_projects()
|
||||
rpc.success(projects)
|
||||
|
||||
elif command == "get_project_by_id":
|
||||
if len(sys.argv) < 3:
|
||||
rpc.error("INVALID_REQUEST", "Project ID required")
|
||||
return
|
||||
project_id = sys.argv[2]
|
||||
project = project_manager.get_project_by_id(project_id)
|
||||
if project:
|
||||
rpc.success(project)
|
||||
else:
|
||||
rpc.error("NOT_FOUND", "Project not found")
|
||||
|
||||
elif command == "create_project":
|
||||
if len(sys.argv) < 4:
|
||||
rpc.error("INVALID_REQUEST", "Name and directory required")
|
||||
return
|
||||
name = sys.argv[2]
|
||||
local_directory = sys.argv[3]
|
||||
product_name = sys.argv[4] if len(sys.argv) > 4 else ""
|
||||
product_image = sys.argv[5] if len(sys.argv) > 5 else ""
|
||||
result = project_manager.create_project(name, local_directory, product_name, product_image)
|
||||
rpc.success(result)
|
||||
|
||||
elif command == "update_project":
|
||||
if len(sys.argv) < 4:
|
||||
rpc.error("INVALID_REQUEST", "Project ID and update data required")
|
||||
return
|
||||
project_id = sys.argv[2]
|
||||
update_data = json.loads(sys.argv[3])
|
||||
result = project_manager.update_project(
|
||||
project_id,
|
||||
update_data.get('name'),
|
||||
update_data.get('local_directory'),
|
||||
update_data.get('product_name'),
|
||||
update_data.get('product_image')
|
||||
)
|
||||
if result:
|
||||
rpc.success(result)
|
||||
else:
|
||||
rpc.error("NOT_FOUND", "Project not found")
|
||||
|
||||
elif command == "delete_project":
|
||||
if len(sys.argv) < 3:
|
||||
rpc.error("INVALID_REQUEST", "Project ID required")
|
||||
return
|
||||
project_id = sys.argv[2]
|
||||
success = project_manager.delete_project(project_id)
|
||||
rpc.success(success)
|
||||
|
||||
elif command == "search_projects":
|
||||
if len(sys.argv) < 3:
|
||||
rpc.error("INVALID_REQUEST", "Search keyword required")
|
||||
return
|
||||
keyword = sys.argv[2]
|
||||
results = project_manager.search_projects(keyword)
|
||||
rpc.success(results)
|
||||
|
||||
elif command == "open_project_directory":
|
||||
if len(sys.argv) < 3:
|
||||
rpc.error("INVALID_REQUEST", "Project ID required")
|
||||
return
|
||||
project_id = sys.argv[2]
|
||||
success = project_manager.open_project_directory(project_id)
|
||||
rpc.success(success)
|
||||
|
||||
else:
|
||||
rpc.error("INVALID_REQUEST", f"Unknown command: {command}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Command execution failed: {e}")
|
||||
rpc.error("INTERNAL_ERROR", str(e))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{command, AppHandle};
|
||||
use crate::python_executor::execute_python_command;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ProjectInfo {
|
||||
|
|
@ -75,3 +77,122 @@ pub async fn load_project(project_path: String) -> Result<ProjectInfo, String> {
|
|||
|
||||
Ok(project_info)
|
||||
}
|
||||
|
||||
// 项目管理相关结构体和命令
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateProjectRequest {
|
||||
pub name: String,
|
||||
pub local_directory: String,
|
||||
pub product_name: Option<String>,
|
||||
pub product_image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateProjectRequest {
|
||||
pub name: Option<String>,
|
||||
pub local_directory: Option<String>,
|
||||
pub product_name: Option<String>,
|
||||
pub product_image: Option<String>,
|
||||
}
|
||||
|
||||
/// 获取所有项目
|
||||
#[command]
|
||||
pub async fn get_all_projects(app: AppHandle) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.project_manager".to_string(),
|
||||
"get_all_projects".to_string(),
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 根据ID获取项目
|
||||
#[command]
|
||||
pub async fn get_project_by_id(app: AppHandle, project_id: String) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.project_manager".to_string(),
|
||||
"get_project_by_id".to_string(),
|
||||
project_id,
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 创建新项目
|
||||
#[command]
|
||||
pub async fn create_project(app: AppHandle, request: CreateProjectRequest) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.project_manager".to_string(),
|
||||
"create_project".to_string(),
|
||||
request.name,
|
||||
request.local_directory,
|
||||
request.product_name.unwrap_or_default(),
|
||||
request.product_image.unwrap_or_default(),
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 更新项目
|
||||
#[command]
|
||||
pub async fn update_project(
|
||||
app: AppHandle,
|
||||
project_id: String,
|
||||
request: UpdateProjectRequest,
|
||||
) -> Result<String, String> {
|
||||
let request_json = serde_json::to_string(&request)
|
||||
.map_err(|e| format!("Failed to serialize request: {}", e))?;
|
||||
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.project_manager".to_string(),
|
||||
"update_project".to_string(),
|
||||
project_id,
|
||||
request_json,
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 删除项目
|
||||
#[command]
|
||||
pub async fn delete_project(app: AppHandle, project_id: String) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.project_manager".to_string(),
|
||||
"delete_project".to_string(),
|
||||
project_id,
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 搜索项目
|
||||
#[command]
|
||||
pub async fn search_projects(app: AppHandle, keyword: String) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.project_manager".to_string(),
|
||||
"search_projects".to_string(),
|
||||
keyword,
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 打开项目目录
|
||||
#[command]
|
||||
pub async fn open_project_directory(app: AppHandle, project_id: String) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.project_manager".to_string(),
|
||||
"open_project_directory".to_string(),
|
||||
project_id,
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,14 @@ pub fn run() {
|
|||
commands::create_resource_category,
|
||||
commands::update_resource_category,
|
||||
commands::delete_resource_category,
|
||||
commands::search_resource_categories
|
||||
commands::search_resource_categories,
|
||||
commands::get_all_projects,
|
||||
commands::get_project_by_id,
|
||||
commands::create_project,
|
||||
commands::update_project,
|
||||
commands::delete_project,
|
||||
commands::search_projects,
|
||||
commands::open_project_directory
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import SettingsPage from './pages/SettingsPage'
|
|||
import TemplateManagePage from './pages/TemplateManagePage'
|
||||
import TemplateDetailPage from './pages/TemplateDetailPage'
|
||||
import ResourceCategoryPage from './pages/ResourceCategoryPage'
|
||||
import ProjectManagePage from './pages/ProjectManagePage'
|
||||
import KVTestPage from './pages/KVTestPage'
|
||||
|
||||
function App() {
|
||||
|
|
@ -20,6 +21,7 @@ function App() {
|
|||
<Route path="/templates" element={<TemplateManagePage />} />
|
||||
<Route path="/templates/:templateId" element={<TemplateDetailPage />} />
|
||||
<Route path="/resource-categories" element={<ResourceCategoryPage />} />
|
||||
<Route path="/projects" element={<ProjectManagePage />} />
|
||||
<Route path="/kv-test" element={<KVTestPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,395 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { Plus, Edit, Trash2, Search, Save, X, FolderOpen, Package } from 'lucide-react'
|
||||
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 {
|
||||
// TODO: 使用Tauri的文件选择器
|
||||
const directory = prompt('请输入项目目录路径:')
|
||||
if (directory) {
|
||||
setFormData({ ...formData, local_directory: directory })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select directory:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const selectImage = async () => {
|
||||
try {
|
||||
// TODO: 使用Tauri的文件选择器
|
||||
const imagePath = prompt('请输入商品图片路径:')
|
||||
if (imagePath) {
|
||||
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 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">
|
||||
{/* 本地目录 */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">本地目录:</p>
|
||||
<p className="text-sm text-gray-900 truncate" title={project.local_directory}>
|
||||
{project.local_directory}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 商品信息 */}
|
||||
{project.product_name && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">商品名:</p>
|
||||
<p className="text-sm text-gray-900">{project.product_name}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 商品图片 */}
|
||||
{project.product_image && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">商品图片:</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Package size={16} className="text-gray-400" />
|
||||
<p className="text-sm text-gray-900 truncate flex-1" title={project.product_image}>
|
||||
{project.product_image.split('/').pop()}
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
{/* 空状态 */}
|
||||
{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"
|
||||
/>
|
||||
<button
|
||||
onClick={selectDirectory}
|
||||
className="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<FolderOpen size={16} />
|
||||
</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"
|
||||
/>
|
||||
<button
|
||||
onClick={selectImage}
|
||||
className="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<Package size={16} />
|
||||
</button>
|
||||
</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
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export interface Project {
|
||||
id: string
|
||||
name: string
|
||||
local_directory: string
|
||||
product_name: string
|
||||
product_image: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
export interface CreateProjectRequest {
|
||||
name: string
|
||||
local_directory: string
|
||||
product_name?: string
|
||||
product_image?: string
|
||||
}
|
||||
|
||||
export interface UpdateProjectRequest {
|
||||
name?: string
|
||||
local_directory?: string
|
||||
product_name?: string
|
||||
product_image?: string
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
status: boolean
|
||||
data?: T
|
||||
msg?: string
|
||||
}
|
||||
|
||||
export class ProjectService {
|
||||
/**
|
||||
* 获取所有项目
|
||||
*/
|
||||
static async getAllProjects(): Promise<ApiResponse<Project[]>> {
|
||||
try {
|
||||
console.log('Calling get_all_projects...')
|
||||
const result = await invoke('get_all_projects')
|
||||
console.log('Raw result from Tauri:', result)
|
||||
|
||||
// 如果result是字符串,尝试解析JSON
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
console.log('Parsed result:', parsed)
|
||||
return parsed as ApiResponse<Project[]>
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse JSON response:', parseError)
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Project[]>
|
||||
} catch (error) {
|
||||
console.error('Failed to get all projects:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取项目
|
||||
*/
|
||||
static async getProjectById(projectId: string): Promise<ApiResponse<Project>> {
|
||||
try {
|
||||
const result = await invoke('get_project_by_id', { projectId })
|
||||
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
return parsed as ApiResponse<Project>
|
||||
} catch (parseError) {
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Project>
|
||||
} catch (error) {
|
||||
console.error('Failed to get project by id:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新项目
|
||||
*/
|
||||
static async createProject(request: CreateProjectRequest): Promise<ApiResponse<Project>> {
|
||||
try {
|
||||
console.log('Calling create_project with:', request)
|
||||
const result = await invoke('create_project', { request })
|
||||
console.log('Raw result from Tauri:', result)
|
||||
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
console.log('Parsed result:', parsed)
|
||||
return parsed as ApiResponse<Project>
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse JSON response:', parseError)
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Project>
|
||||
} catch (error) {
|
||||
console.error('Failed to create project:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目
|
||||
*/
|
||||
static async updateProject(
|
||||
projectId: string,
|
||||
request: UpdateProjectRequest
|
||||
): Promise<ApiResponse<Project>> {
|
||||
try {
|
||||
const result = await invoke('update_project', { projectId, request })
|
||||
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
return parsed as ApiResponse<Project>
|
||||
} catch (parseError) {
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Project>
|
||||
} catch (error) {
|
||||
console.error('Failed to update project:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项目
|
||||
*/
|
||||
static async deleteProject(projectId: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
const result = await invoke('delete_project', { projectId })
|
||||
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
return parsed as ApiResponse<boolean>
|
||||
} catch (parseError) {
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<boolean>
|
||||
} catch (error) {
|
||||
console.error('Failed to delete project:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索项目
|
||||
*/
|
||||
static async searchProjects(keyword: string): Promise<ApiResponse<Project[]>> {
|
||||
try {
|
||||
const result = await invoke('search_projects', { keyword })
|
||||
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
return parsed as ApiResponse<Project[]>
|
||||
} catch (parseError) {
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Project[]>
|
||||
} catch (error) {
|
||||
console.error('Failed to search projects:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开项目目录
|
||||
*/
|
||||
static async openProjectDirectory(projectId: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
const result = await invoke('open_project_directory', { projectId })
|
||||
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
return parsed as ApiResponse<boolean>
|
||||
} catch (parseError) {
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<boolean>
|
||||
} catch (error) {
|
||||
console.error('Failed to open project directory:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue