模特管理
This commit is contained in:
277
python_core/services/model_manager.py
Normal file
277
python_core/services/model_manager.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
模特管理服务
|
||||
管理模特信息,包括模特编号、模特图片等
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import os
|
||||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
class Model:
|
||||
"""模特数据结构"""
|
||||
id: str
|
||||
model_number: str # 模特编号
|
||||
model_image: str # 模特图片路径
|
||||
created_at: str
|
||||
updated_at: str
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class ModelManager:
|
||||
"""模特管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.cache_dir = settings.temp_dir / "cache"
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 模特数据文件
|
||||
self.models_file = self.cache_dir / "models.json"
|
||||
self.models = self._load_models()
|
||||
|
||||
# 如果没有数据,创建默认模特
|
||||
if not self.models:
|
||||
self._create_default_models()
|
||||
|
||||
def _load_models(self) -> List[Model]:
|
||||
"""加载模特数据"""
|
||||
if self.models_file.exists():
|
||||
try:
|
||||
with open(self.models_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return [Model(**item) for item in data]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load models: {e}")
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
|
||||
def _save_models(self, models: List[Model] = None):
|
||||
"""保存模特数据"""
|
||||
if models is None:
|
||||
models = self.models
|
||||
|
||||
try:
|
||||
data = [asdict(model) for model in models]
|
||||
with open(self.models_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"Models saved to {self.models_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save models: {e}")
|
||||
raise
|
||||
|
||||
def _create_default_models(self):
|
||||
"""创建默认模特数据"""
|
||||
default_models = [
|
||||
{
|
||||
"model_number": "M001",
|
||||
"model_image": "",
|
||||
},
|
||||
{
|
||||
"model_number": "M002",
|
||||
"model_image": "",
|
||||
},
|
||||
{
|
||||
"model_number": "M003",
|
||||
"model_image": "",
|
||||
},
|
||||
]
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
for model_data in default_models:
|
||||
model = Model(
|
||||
id=str(uuid.uuid4()),
|
||||
model_number=model_data["model_number"],
|
||||
model_image=model_data["model_image"],
|
||||
created_at=now,
|
||||
updated_at=now
|
||||
)
|
||||
self.models.append(model)
|
||||
|
||||
self._save_models()
|
||||
logger.info("Created default models")
|
||||
|
||||
def get_all_models(self) -> List[Dict]:
|
||||
"""获取所有模特"""
|
||||
return [asdict(model) for model in self.models if model.is_active]
|
||||
|
||||
def get_model_by_id(self, model_id: str) -> Optional[Dict]:
|
||||
"""根据ID获取模特"""
|
||||
for model in self.models:
|
||||
if model.id == model_id and model.is_active:
|
||||
return asdict(model)
|
||||
return None
|
||||
|
||||
def get_model_by_number(self, model_number: str) -> Optional[Dict]:
|
||||
"""根据编号获取模特"""
|
||||
for model in self.models:
|
||||
if model.model_number == model_number and model.is_active:
|
||||
return asdict(model)
|
||||
return None
|
||||
|
||||
def create_model(self, model_number: str, model_image: str = "") -> Dict:
|
||||
"""创建新模特"""
|
||||
# 检查编号是否已存在
|
||||
if self.get_model_by_number(model_number):
|
||||
raise ValueError(f"Model number {model_number} already exists")
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
new_model = Model(
|
||||
id=str(uuid.uuid4()),
|
||||
model_number=model_number,
|
||||
model_image=model_image,
|
||||
created_at=now,
|
||||
updated_at=now
|
||||
)
|
||||
|
||||
self.models.append(new_model)
|
||||
self._save_models()
|
||||
|
||||
logger.info(f"Created new model: {model_number}")
|
||||
return asdict(new_model)
|
||||
|
||||
def update_model(self, model_id: str, model_number: str = None,
|
||||
model_image: str = None) -> Optional[Dict]:
|
||||
"""更新模特"""
|
||||
for i, model in enumerate(self.models):
|
||||
if model.id == model_id and model.is_active:
|
||||
# 如果更新编号,检查是否与其他模特冲突
|
||||
if model_number is not None and model_number != model.model_number:
|
||||
existing = self.get_model_by_number(model_number)
|
||||
if existing and existing['id'] != model_id:
|
||||
raise ValueError(f"Model number {model_number} already exists")
|
||||
model.model_number = model_number
|
||||
|
||||
if model_image is not None:
|
||||
model.model_image = model_image
|
||||
|
||||
model.updated_at = datetime.now().isoformat()
|
||||
|
||||
self.models[i] = model
|
||||
self._save_models()
|
||||
|
||||
logger.info(f"Updated model: {model_id}")
|
||||
return asdict(model)
|
||||
return None
|
||||
|
||||
def delete_model(self, model_id: str) -> bool:
|
||||
"""删除模特(真删除)"""
|
||||
for i, model in enumerate(self.models):
|
||||
if model.id == model_id:
|
||||
# 真删除:从列表中移除
|
||||
deleted_model = self.models.pop(i)
|
||||
self._save_models()
|
||||
|
||||
logger.info(f"Deleted model: {model_id} - {deleted_model.model_number}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def search_models(self, keyword: str) -> List[Dict]:
|
||||
"""搜索模特"""
|
||||
keyword = keyword.lower()
|
||||
results = []
|
||||
|
||||
for model in self.models:
|
||||
if (model.is_active and
|
||||
keyword in model.model_number.lower()):
|
||||
results.append(asdict(model))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# 全局实例
|
||||
model_manager = ModelManager()
|
||||
|
||||
|
||||
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_models":
|
||||
models = model_manager.get_all_models()
|
||||
rpc.success(models)
|
||||
|
||||
elif command == "get_model_by_id":
|
||||
if len(sys.argv) < 3:
|
||||
rpc.error("INVALID_REQUEST", "Model ID required")
|
||||
return
|
||||
model_id = sys.argv[2]
|
||||
model = model_manager.get_model_by_id(model_id)
|
||||
if model:
|
||||
rpc.success(model)
|
||||
else:
|
||||
rpc.error("NOT_FOUND", "Model not found")
|
||||
|
||||
elif command == "create_model":
|
||||
if len(sys.argv) < 3:
|
||||
rpc.error("INVALID_REQUEST", "Model number required")
|
||||
return
|
||||
model_number = sys.argv[2]
|
||||
model_image = sys.argv[3] if len(sys.argv) > 3 else ""
|
||||
result = model_manager.create_model(model_number, model_image)
|
||||
rpc.success(result)
|
||||
|
||||
elif command == "update_model":
|
||||
if len(sys.argv) < 4:
|
||||
rpc.error("INVALID_REQUEST", "Model ID and update data required")
|
||||
return
|
||||
model_id = sys.argv[2]
|
||||
update_data = json.loads(sys.argv[3])
|
||||
result = model_manager.update_model(
|
||||
model_id,
|
||||
update_data.get('model_number'),
|
||||
update_data.get('model_image')
|
||||
)
|
||||
if result:
|
||||
rpc.success(result)
|
||||
else:
|
||||
rpc.error("NOT_FOUND", "Model not found")
|
||||
|
||||
elif command == "delete_model":
|
||||
if len(sys.argv) < 3:
|
||||
rpc.error("INVALID_REQUEST", "Model ID required")
|
||||
return
|
||||
model_id = sys.argv[2]
|
||||
success = model_manager.delete_model(model_id)
|
||||
rpc.success(success)
|
||||
|
||||
elif command == "search_models":
|
||||
if len(sys.argv) < 3:
|
||||
rpc.error("INVALID_REQUEST", "Search keyword required")
|
||||
return
|
||||
keyword = sys.argv[2]
|
||||
results = model_manager.search_models(keyword)
|
||||
rpc.success(results)
|
||||
|
||||
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()
|
||||
@@ -6,6 +6,7 @@ pub mod file_system;
|
||||
pub mod project;
|
||||
pub mod template;
|
||||
pub mod file_utils;
|
||||
pub mod model;
|
||||
pub mod resource_category;
|
||||
|
||||
// Re-export all commands for easy access
|
||||
|
||||
101
src-tauri/src/commands/model.rs
Normal file
101
src-tauri/src/commands/model.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{command, AppHandle};
|
||||
use crate::python_executor::execute_python_command;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateModelRequest {
|
||||
pub model_number: String,
|
||||
pub model_image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateModelRequest {
|
||||
pub model_number: Option<String>,
|
||||
pub model_image: Option<String>,
|
||||
}
|
||||
|
||||
/// 获取所有模特
|
||||
#[command]
|
||||
pub async fn get_all_models(app: AppHandle) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.model_manager".to_string(),
|
||||
"get_all_models".to_string(),
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 根据ID获取模特
|
||||
#[command]
|
||||
pub async fn get_model_by_id(app: AppHandle, model_id: String) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.model_manager".to_string(),
|
||||
"get_model_by_id".to_string(),
|
||||
model_id,
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 创建新模特
|
||||
#[command]
|
||||
pub async fn create_model(app: AppHandle, request: CreateModelRequest) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.model_manager".to_string(),
|
||||
"create_model".to_string(),
|
||||
request.model_number,
|
||||
request.model_image.unwrap_or_default(),
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 更新模特
|
||||
#[command]
|
||||
pub async fn update_model(
|
||||
app: AppHandle,
|
||||
model_id: String,
|
||||
request: UpdateModelRequest,
|
||||
) -> 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.model_manager".to_string(),
|
||||
"update_model".to_string(),
|
||||
model_id,
|
||||
request_json,
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 删除模特
|
||||
#[command]
|
||||
pub async fn delete_model(app: AppHandle, model_id: String) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.model_manager".to_string(),
|
||||
"delete_model".to_string(),
|
||||
model_id,
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
|
||||
/// 搜索模特
|
||||
#[command]
|
||||
pub async fn search_models(app: AppHandle, keyword: String) -> Result<String, String> {
|
||||
let args = vec![
|
||||
"-m".to_string(),
|
||||
"python_core.services.model_manager".to_string(),
|
||||
"search_models".to_string(),
|
||||
keyword,
|
||||
];
|
||||
|
||||
execute_python_command(app, &args, None).await
|
||||
}
|
||||
@@ -56,6 +56,12 @@ pub fn run() {
|
||||
commands::delete_project,
|
||||
commands::search_projects,
|
||||
commands::open_project_directory,
|
||||
commands::model::get_all_models,
|
||||
commands::model::get_model_by_id,
|
||||
commands::model::create_model,
|
||||
commands::model::update_model,
|
||||
commands::model::delete_model,
|
||||
commands::model::search_models,
|
||||
commands::file_utils::read_image_as_data_url
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -9,6 +9,7 @@ import TemplateManagePage from './pages/TemplateManagePage'
|
||||
import TemplateDetailPage from './pages/TemplateDetailPage'
|
||||
import ResourceCategoryPage from './pages/ResourceCategoryPage'
|
||||
import ProjectManagePage from './pages/ProjectManagePage'
|
||||
import ModelManagePage from './pages/ModelManagePage'
|
||||
import KVTestPage from './pages/KVTestPage'
|
||||
|
||||
function App() {
|
||||
@@ -22,6 +23,7 @@ function App() {
|
||||
<Route path="/templates/:templateId" element={<TemplateDetailPage />} />
|
||||
<Route path="/resource-categories" element={<ResourceCategoryPage />} />
|
||||
<Route path="/projects" element={<ProjectManagePage />} />
|
||||
<Route path="/models" element={<ModelManagePage />} />
|
||||
<Route path="/kv-test" element={<KVTestPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
|
||||
65
src/components/ModelCard.tsx
Normal file
65
src/components/ModelCard.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import { Edit, Trash2, User } from 'lucide-react'
|
||||
import { Model } from '../services/modelService'
|
||||
import ProjectImage from './ProjectImage'
|
||||
|
||||
interface ModelCardProps {
|
||||
model: Model
|
||||
onEdit: (model: Model) => void
|
||||
onDelete: (modelId: string) => void
|
||||
}
|
||||
|
||||
const ModelCard: React.FC<ModelCardProps> = ({
|
||||
model,
|
||||
onEdit,
|
||||
onDelete
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
|
||||
{/* 模特图片 */}
|
||||
<div className="h-48 bg-gray-100 overflow-hidden relative">
|
||||
{model.model_image ? (
|
||||
<ProjectImage
|
||||
imagePath={model.model_image}
|
||||
alt={`模特 ${model.model_number}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
|
||||
<User size={48} className="text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="absolute top-2 right-2 flex space-x-1">
|
||||
<button
|
||||
onClick={() => onEdit(model)}
|
||||
className="p-2 bg-white/80 text-gray-600 rounded-lg hover:bg-white hover:text-blue-600 transition-colors"
|
||||
title="编辑"
|
||||
>
|
||||
<Edit size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDelete(model.id)}
|
||||
className="p-2 bg-white/80 text-gray-600 rounded-lg hover:bg-white hover:text-red-600 transition-colors"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{/* 模特编号 */}
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">{model.model_number}</h3>
|
||||
<p className="text-xs text-gray-400">
|
||||
创建于 {new Date(model.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelCard
|
||||
144
src/components/ModelForm.tsx
Normal file
144
src/components/ModelForm.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React from 'react'
|
||||
import { Save, X, Package } from 'lucide-react'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import ProjectImage from './ProjectImage'
|
||||
|
||||
interface ModelFormData {
|
||||
model_number: string
|
||||
model_image: string
|
||||
}
|
||||
|
||||
interface ModelFormProps {
|
||||
isOpen: boolean
|
||||
isEditing: boolean
|
||||
formData: ModelFormData
|
||||
onFormDataChange: (data: ModelFormData) => void
|
||||
onSubmit: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const ModelForm: React.FC<ModelFormProps> = ({
|
||||
isOpen,
|
||||
isEditing,
|
||||
formData,
|
||||
onFormDataChange,
|
||||
onSubmit,
|
||||
onCancel
|
||||
}) => {
|
||||
if (!isOpen) return null
|
||||
|
||||
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') {
|
||||
onFormDataChange({ ...formData, model_image: imagePath })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select image:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
{isEditing ? '编辑模特' : '新建模特'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
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.model_number}
|
||||
onChange={(e) => onFormDataChange({ ...formData, model_number: e.target.value })}
|
||||
placeholder="请输入模特编号,如:M001"
|
||||
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.model_image}
|
||||
onChange={(e) => onFormDataChange({ ...formData, model_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.model_image && (
|
||||
<div className="mt-2">
|
||||
<p className="text-xs text-gray-500 mb-1">预览:</p>
|
||||
<div className="w-32 h-40 border border-gray-200 rounded-lg overflow-hidden">
|
||||
<ProjectImage
|
||||
imagePath={formData.model_image}
|
||||
alt="模特图片预览"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表单按钮 */}
|
||||
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={!formData.model_number.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" />
|
||||
{isEditing ? '保存' : '创建'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelForm
|
||||
80
src/components/ModelList.tsx
Normal file
80
src/components/ModelList.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react'
|
||||
import { Plus, User } from 'lucide-react'
|
||||
import { Model } from '../services/modelService'
|
||||
import ModelCard from './ModelCard'
|
||||
|
||||
interface ModelListProps {
|
||||
models: Model[]
|
||||
loading: boolean
|
||||
searchTerm: string
|
||||
onEdit: (model: Model) => void
|
||||
onDelete: (modelId: string) => void
|
||||
onCreateNew: () => void
|
||||
}
|
||||
|
||||
const ModelList: React.FC<ModelListProps> = ({
|
||||
models,
|
||||
loading,
|
||||
searchTerm,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onCreateNew
|
||||
}) => {
|
||||
const filteredModels = models.filter(model =>
|
||||
model.model_number.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
// 加载状态
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={index} className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div className="h-48 bg-gray-200 animate-pulse"></div>
|
||||
<div className="p-4">
|
||||
<div className="h-6 bg-gray-200 rounded animate-pulse mb-2"></div>
|
||||
<div className="h-3 bg-gray-200 rounded animate-pulse w-2/3 mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 空状态
|
||||
if (filteredModels.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<User className="mx-auto text-gray-400 mb-4" size={64} />
|
||||
<div className="text-gray-400 mb-4">
|
||||
{searchTerm ? '没有找到匹配的模特' : '暂无模特'}
|
||||
</div>
|
||||
{!searchTerm && (
|
||||
<button
|
||||
onClick={onCreateNew}
|
||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
添加第一个模特
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 模特列表
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{filteredModels.map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
model={model}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelList
|
||||
39
src/components/ModelSearchBar.tsx
Normal file
39
src/components/ModelSearchBar.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import { Search, Plus } from 'lucide-react'
|
||||
|
||||
interface ModelSearchBarProps {
|
||||
searchTerm: string
|
||||
onSearchChange: (value: string) => void
|
||||
onCreateNew: () => void
|
||||
}
|
||||
|
||||
const ModelSearchBar: React.FC<ModelSearchBarProps> = ({
|
||||
searchTerm,
|
||||
onSearchChange,
|
||||
onCreateNew
|
||||
}) => {
|
||||
return (
|
||||
<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) => onSearchChange(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={onCreateNew}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelSearchBar
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Home, Video, Settings, FolderOpen, Music, Image, Sparkles, Layout, Clock, CheckCircle, XCircle, List, Tags } from 'lucide-react'
|
||||
import { Home, Video, Settings, FolderOpen, Music, Image, Sparkles, Layout, Clock, CheckCircle, XCircle, List, Tags, User } from 'lucide-react'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
@@ -25,6 +25,7 @@ const Sidebar: React.FC = () => {
|
||||
{ path: '/templates', icon: Layout, label: '模板管理' },
|
||||
{ path: '/resource-categories', icon: Tags, label: '分类管理' },
|
||||
{ path: '/projects', icon: FolderOpen, label: '项目' },
|
||||
{ path: '/models', icon: User, label: '模特管理' },
|
||||
{ path: '/media', icon: Image, label: '媒体库' },
|
||||
{ path: '/audio', icon: Music, label: '音频' },
|
||||
{ path: '/settings', icon: Settings, label: '设置' },
|
||||
|
||||
164
src/pages/ModelManagePage.tsx
Normal file
164
src/pages/ModelManagePage.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { ModelService, Model } from '../services/modelService'
|
||||
import ModelSearchBar from '../components/ModelSearchBar'
|
||||
import ModelList from '../components/ModelList'
|
||||
import ModelForm from '../components/ModelForm'
|
||||
|
||||
interface FormData {
|
||||
model_number: string
|
||||
model_image: string
|
||||
}
|
||||
|
||||
const ModelManagePage: React.FC = () => {
|
||||
const [models, setModels] = useState<Model[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [editingModel, setEditingModel] = useState<Model | null>(null)
|
||||
const [showCreateForm, setShowCreateForm] = useState(false)
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
model_number: '',
|
||||
model_image: ''
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
loadModels()
|
||||
}, [])
|
||||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await ModelService.getAllModels()
|
||||
if (response.status && response.data) {
|
||||
setModels(response.data)
|
||||
} else {
|
||||
console.error('Failed to load models:', response.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load models:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateModel = async () => {
|
||||
try {
|
||||
const response = await ModelService.createModel(formData)
|
||||
if (response.status && response.data) {
|
||||
setModels([...models, response.data])
|
||||
handleCancelForm()
|
||||
} else {
|
||||
console.error('创建失败:', response.msg || '未知错误')
|
||||
alert('创建失败: ' + (response.msg || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create model:', error)
|
||||
alert('创建失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateModel = async () => {
|
||||
if (!editingModel) return
|
||||
|
||||
try {
|
||||
const response = await ModelService.updateModel(editingModel.id, formData)
|
||||
if (response.status && response.data) {
|
||||
const updatedModels = models.map(model =>
|
||||
model.id === editingModel.id ? response.data! : model
|
||||
)
|
||||
setModels(updatedModels)
|
||||
handleCancelForm()
|
||||
} else {
|
||||
console.error('更新失败:', response.msg || '未知错误')
|
||||
alert('更新失败: ' + (response.msg || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update model:', error)
|
||||
alert('更新失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteModel = async (modelId: string) => {
|
||||
if (!confirm('确定要删除这个模特吗?')) return
|
||||
|
||||
try {
|
||||
const response = await ModelService.deleteModel(modelId)
|
||||
if (response.status) {
|
||||
setModels(models.filter(model => model.id !== modelId))
|
||||
} else {
|
||||
console.error('删除失败:', response.msg || '未知错误')
|
||||
alert('删除失败: ' + (response.msg || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete model:', error)
|
||||
alert('删除失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditModel = (model: Model) => {
|
||||
setEditingModel(model)
|
||||
setFormData({
|
||||
model_number: model.model_number,
|
||||
model_image: model.model_image
|
||||
})
|
||||
setShowCreateForm(true)
|
||||
}
|
||||
|
||||
const handleCreateNew = () => {
|
||||
setEditingModel(null)
|
||||
setFormData({ model_number: '', model_image: '' })
|
||||
setShowCreateForm(true)
|
||||
}
|
||||
|
||||
const handleCancelForm = () => {
|
||||
setEditingModel(null)
|
||||
setShowCreateForm(false)
|
||||
setFormData({ model_number: '', model_image: '' })
|
||||
}
|
||||
|
||||
const handleSubmitForm = () => {
|
||||
if (editingModel) {
|
||||
handleUpdateModel()
|
||||
} else {
|
||||
handleCreateModel()
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
{/* 搜索和操作栏 */}
|
||||
<ModelSearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
onCreateNew={handleCreateNew}
|
||||
/>
|
||||
|
||||
{/* 模特列表 */}
|
||||
<ModelList
|
||||
models={models}
|
||||
loading={loading}
|
||||
searchTerm={searchTerm}
|
||||
onEdit={handleEditModel}
|
||||
onDelete={handleDeleteModel}
|
||||
onCreateNew={handleCreateNew}
|
||||
/>
|
||||
|
||||
{/* 模特表单 */}
|
||||
<ModelForm
|
||||
isOpen={showCreateForm}
|
||||
isEditing={!!editingModel}
|
||||
formData={formData}
|
||||
onFormDataChange={setFormData}
|
||||
onSubmit={handleSubmitForm}
|
||||
onCancel={handleCancelForm}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelManagePage
|
||||
213
src/services/modelService.ts
Normal file
213
src/services/modelService.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export interface Model {
|
||||
id: string
|
||||
model_number: string
|
||||
model_image: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
export interface CreateModelRequest {
|
||||
model_number: string
|
||||
model_image?: string
|
||||
}
|
||||
|
||||
export interface UpdateModelRequest {
|
||||
model_number?: string
|
||||
model_image?: string
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
status: boolean
|
||||
data?: T
|
||||
msg?: string
|
||||
}
|
||||
|
||||
export class ModelService {
|
||||
/**
|
||||
* 获取所有模特
|
||||
*/
|
||||
static async getAllModels(): Promise<ApiResponse<Model[]>> {
|
||||
try {
|
||||
console.log('Calling get_all_models...')
|
||||
const result = await invoke('get_all_models')
|
||||
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<Model[]>
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse JSON response:', parseError)
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Model[]>
|
||||
} catch (error) {
|
||||
console.error('Failed to get all models:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取模特
|
||||
*/
|
||||
static async getModelById(modelId: string): Promise<ApiResponse<Model>> {
|
||||
try {
|
||||
const result = await invoke('get_model_by_id', { modelId })
|
||||
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
return parsed as ApiResponse<Model>
|
||||
} catch (parseError) {
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Model>
|
||||
} catch (error) {
|
||||
console.error('Failed to get model by id:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新模特
|
||||
*/
|
||||
static async createModel(request: CreateModelRequest): Promise<ApiResponse<Model>> {
|
||||
try {
|
||||
console.log('Calling create_model with:', request)
|
||||
const result = await invoke('create_model', { 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<Model>
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse JSON response:', parseError)
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Model>
|
||||
} catch (error) {
|
||||
console.error('Failed to create model:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模特
|
||||
*/
|
||||
static async updateModel(
|
||||
modelId: string,
|
||||
request: UpdateModelRequest
|
||||
): Promise<ApiResponse<Model>> {
|
||||
try {
|
||||
const result = await invoke('update_model', { modelId, request })
|
||||
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
return parsed as ApiResponse<Model>
|
||||
} catch (parseError) {
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Model>
|
||||
} catch (error) {
|
||||
console.error('Failed to update model:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模特
|
||||
*/
|
||||
static async deleteModel(modelId: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
const result = await invoke('delete_model', { modelId })
|
||||
|
||||
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 model:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索模特
|
||||
*/
|
||||
static async searchModels(keyword: string): Promise<ApiResponse<Model[]>> {
|
||||
try {
|
||||
const result = await invoke('search_models', { keyword })
|
||||
|
||||
if (typeof result === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
return parsed as ApiResponse<Model[]>
|
||||
} catch (parseError) {
|
||||
return {
|
||||
status: false,
|
||||
msg: `Invalid JSON response: ${result}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as ApiResponse<Model[]>
|
||||
} catch (error) {
|
||||
console.error('Failed to search models:', error)
|
||||
return {
|
||||
status: false,
|
||||
msg: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user