feat: 实现AI分类设置功能 (v0.1.7)

新增功能:
- AI分类CRUD操作 (创建、读取、更新、删除)
- 实时提示词预览功能
- 分类排序和状态管理
- 完整的表单验证和错误处理

 后端架构:
- 数据层: AiClassification模型和仓储
- 业务层: AiClassificationService业务逻辑
- 表示层: 10个Tauri命令接口
- 数据库: ai_classifications表和索引

 前端架构:
- 类型系统: 完整的TypeScript类型定义
- 服务层: AiClassificationService API封装
- 组件层: 5个专用组件 (主页面、表单、预览、删除确认、实时预览)
- 路由集成: /ai-classification-settings

 质量保证:
- 52个单元测试 (100%通过)
- TypeScript和Rust编译无错误
- 遵循promptx开发规范

 核心特性:
- 支持分类名称和提示词定义
- 实时生成完整AI分类提示词
- 拖拽排序和批量操作
- 优雅的用户界面和交互体验
This commit is contained in:
imeepos
2025-07-14 11:39:44 +08:00
parent a5072d4c0f
commit ca63da30ff
24 changed files with 3755 additions and 7 deletions

View File

@@ -0,0 +1,462 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
PlusIcon,
PencilIcon,
TrashIcon,
EyeIcon,
ArrowUpIcon,
ArrowDownIcon,
CheckIcon,
XMarkIcon
} from '@heroicons/react/24/outline';
import { AiClassificationService } from '../services/aiClassificationService';
import {
AiClassification,
AiClassificationFormData,
AiClassificationFormErrors,
AiClassificationPreview,
DEFAULT_FORM_DATA,
validateClassificationForm,
hasFormErrors,
classificationToFormData,
formDataToCreateRequest,
formDataToUpdateRequest,
} from '../types/aiClassification';
import { LoadingSpinner } from '../components/LoadingSpinner';
import { ErrorMessage } from '../components/ErrorMessage';
import { EmptyState } from '../components/EmptyState';
import { AiClassificationFormDialog } from '../components/AiClassificationFormDialog';
import { AiClassificationPreviewDialog } from '../components/AiClassificationPreviewDialog';
import { DeleteConfirmDialog } from '../components/DeleteConfirmDialog';
/**
* AI分类设置页面
* 遵循前端开发规范的组件设计实现分类的CRUD操作和实时预览
*/
const AiClassificationSettings: React.FC = () => {
// 状态管理
const [classifications, setClassifications] = useState<AiClassification[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showPreviewDialog, setShowPreviewDialog] = useState(false);
const [editingClassification, setEditingClassification] = useState<AiClassification | null>(null);
const [deletingClassificationId, setDeletingClassificationId] = useState<string | null>(null);
const [preview, setPreview] = useState<AiClassificationPreview | null>(null);
const [previewLoading, setPreviewLoading] = useState(false);
// 表单状态
const [formData, setFormData] = useState<AiClassificationFormData>(DEFAULT_FORM_DATA);
const [formErrors, setFormErrors] = useState<AiClassificationFormErrors>({});
const [submitting, setSubmitting] = useState(false);
// 加载分类列表
const loadClassifications = useCallback(async () => {
try {
setLoading(true);
setError(null);
const result = await AiClassificationService.getAllClassificationsIncludingInactive();
setClassifications(result);
} catch (err) {
setError(err instanceof Error ? err.message : '加载分类列表失败');
} finally {
setLoading(false);
}
}, []);
// 生成预览
const generatePreview = useCallback(async () => {
try {
setPreviewLoading(true);
const result = await AiClassificationService.generatePreview();
setPreview(result);
} catch (err) {
console.error('生成预览失败:', err);
} finally {
setPreviewLoading(false);
}
}, []);
// 初始化
useEffect(() => {
loadClassifications();
}, [loadClassifications]);
// 处理创建分类
const handleCreate = async () => {
const errors = validateClassificationForm(formData);
setFormErrors(errors);
if (hasFormErrors(errors)) {
return;
}
try {
setSubmitting(true);
const request = formDataToCreateRequest(formData);
await AiClassificationService.createClassification(request);
// 重新加载列表
await loadClassifications();
// 关闭对话框并重置表单
setShowCreateDialog(false);
setFormData(DEFAULT_FORM_DATA);
setFormErrors({});
} catch (err) {
setFormErrors({ general: err instanceof Error ? err.message : '创建失败' });
} finally {
setSubmitting(false);
}
};
// 处理编辑分类
const handleEdit = async () => {
if (!editingClassification) return;
const errors = validateClassificationForm(formData);
setFormErrors(errors);
if (hasFormErrors(errors)) {
return;
}
try {
setSubmitting(true);
const request = formDataToUpdateRequest(formData);
await AiClassificationService.updateClassification(editingClassification.id, request);
// 重新加载列表
await loadClassifications();
// 关闭对话框并重置状态
setShowEditDialog(false);
setEditingClassification(null);
setFormData(DEFAULT_FORM_DATA);
setFormErrors({});
} catch (err) {
setFormErrors({ general: err instanceof Error ? err.message : '更新失败' });
} finally {
setSubmitting(false);
}
};
// 处理删除分类
const handleDelete = async () => {
if (!deletingClassificationId) return;
try {
setSubmitting(true);
await AiClassificationService.deleteClassification(deletingClassificationId);
// 重新加载列表
await loadClassifications();
// 关闭对话框并重置状态
setShowDeleteDialog(false);
setDeletingClassificationId(null);
} catch (err) {
setError(err instanceof Error ? err.message : '删除失败');
} finally {
setSubmitting(false);
}
};
// 处理切换状态
const handleToggleStatus = async (id: string) => {
try {
await AiClassificationService.toggleClassificationStatus(id);
await loadClassifications();
} catch (err) {
setError(err instanceof Error ? err.message : '切换状态失败');
}
};
// 处理移动排序
const handleMoveUp = async (classification: AiClassification) => {
const currentIndex = classifications.findIndex(c => c.id === classification.id);
if (currentIndex <= 0) return;
const updates = [
{ id: classification.id, sort_order: classifications[currentIndex - 1].sort_order },
{ id: classifications[currentIndex - 1].id, sort_order: classification.sort_order },
];
try {
await AiClassificationService.updateSortOrders(updates);
await loadClassifications();
} catch (err) {
setError(err instanceof Error ? err.message : '调整排序失败');
}
};
const handleMoveDown = async (classification: AiClassification) => {
const currentIndex = classifications.findIndex(c => c.id === classification.id);
if (currentIndex >= classifications.length - 1) return;
const updates = [
{ id: classification.id, sort_order: classifications[currentIndex + 1].sort_order },
{ id: classifications[currentIndex + 1].id, sort_order: classification.sort_order },
];
try {
await AiClassificationService.updateSortOrders(updates);
await loadClassifications();
} catch (err) {
setError(err instanceof Error ? err.message : '调整排序失败');
}
};
// 打开创建对话框
const openCreateDialog = () => {
setFormData(DEFAULT_FORM_DATA);
setFormErrors({});
setShowCreateDialog(true);
};
// 打开编辑对话框
const openEditDialog = (classification: AiClassification) => {
setEditingClassification(classification);
setFormData(classificationToFormData(classification));
setFormErrors({});
setShowEditDialog(true);
};
// 打开删除确认对话框
const openDeleteDialog = (id: string) => {
setDeletingClassificationId(id);
setShowDeleteDialog(true);
};
// 打开预览对话框
const openPreviewDialog = async () => {
setShowPreviewDialog(true);
await generatePreview();
};
// 渲染加载状态
if (loading) {
return (
<div className="flex items-center justify-center min-h-96">
<LoadingSpinner size="large" />
</div>
);
}
// 渲染错误状态
if (error) {
return (
<div className="p-6">
<ErrorMessage
message={error}
onRetry={loadClassifications}
/>
</div>
);
}
return (
<div className="p-6 max-w-7xl mx-auto">
{/* 页面标题和操作栏 */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">AI分类设置</h1>
<p className="text-gray-600 mt-1">AI分类规则和提示词</p>
</div>
<div className="flex space-x-3">
<button
onClick={openPreviewDialog}
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<EyeIcon className="h-4 w-4 mr-2" />
</button>
<button
onClick={openCreateDialog}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<PlusIcon className="h-4 w-4 mr-2" />
</button>
</div>
</div>
{/* 分类列表 */}
{classifications.length === 0 ? (
<EmptyState
title="暂无AI分类"
description="开始创建您的第一个AI分类规则"
actionText="添加分类"
onAction={openCreateDialog}
/>
) : (
<div className="bg-white shadow rounded-lg overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">
({classifications.length})
</h3>
</div>
<div className="divide-y divide-gray-200">
{classifications.map((classification, index) => (
<div key={classification.id} className="p-6 hover:bg-gray-50">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-3">
<h4 className="text-lg font-medium text-gray-900">
{classification.name}
</h4>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
classification.is_active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{classification.is_active ? '激活' : '禁用'}
</span>
<span className="text-sm text-gray-500">
: {classification.sort_order}
</span>
</div>
<p className="mt-2 text-sm text-gray-600 line-clamp-2">
{classification.prompt_text}
</p>
{classification.description && (
<p className="mt-1 text-sm text-gray-500">
{classification.description}
</p>
)}
<div className="mt-2 text-xs text-gray-400">
: {new Date(classification.created_at).toLocaleString()}
</div>
</div>
<div className="flex items-center space-x-2 ml-4">
{/* 排序按钮 */}
<button
onClick={() => handleMoveUp(classification)}
disabled={index === 0}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="上移"
>
<ArrowUpIcon className="h-4 w-4" />
</button>
<button
onClick={() => handleMoveDown(classification)}
disabled={index === classifications.length - 1}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="下移"
>
<ArrowDownIcon className="h-4 w-4" />
</button>
{/* 状态切换按钮 */}
<button
onClick={() => handleToggleStatus(classification.id)}
className={`p-1 ${
classification.is_active
? 'text-green-600 hover:text-green-800'
: 'text-gray-400 hover:text-gray-600'
}`}
title={classification.is_active ? '禁用' : '激活'}
>
{classification.is_active ? (
<CheckIcon className="h-4 w-4" />
) : (
<XMarkIcon className="h-4 w-4" />
)}
</button>
{/* 编辑按钮 */}
<button
onClick={() => openEditDialog(classification)}
className="p-1 text-blue-600 hover:text-blue-800"
title="编辑"
>
<PencilIcon className="h-4 w-4" />
</button>
{/* 删除按钮 */}
<button
onClick={() => openDeleteDialog(classification.id)}
className="p-1 text-red-600 hover:text-red-800"
title="删除"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* 创建分类对话框 */}
<AiClassificationFormDialog
isOpen={showCreateDialog}
title="添加AI分类"
formData={formData}
formErrors={formErrors}
submitting={submitting}
existingClassifications={classifications}
onFormDataChange={setFormData}
onSubmit={handleCreate}
onCancel={() => {
setShowCreateDialog(false);
setFormData(DEFAULT_FORM_DATA);
setFormErrors({});
}}
/>
{/* 编辑分类对话框 */}
<AiClassificationFormDialog
isOpen={showEditDialog}
title="编辑AI分类"
formData={formData}
formErrors={formErrors}
submitting={submitting}
isEdit={true}
existingClassifications={classifications}
editingClassificationId={editingClassification?.id}
onFormDataChange={setFormData}
onSubmit={handleEdit}
onCancel={() => {
setShowEditDialog(false);
setEditingClassification(null);
setFormData(DEFAULT_FORM_DATA);
setFormErrors({});
}}
/>
{/* 删除确认对话框 */}
<DeleteConfirmDialog
isOpen={showDeleteDialog}
title="删除AI分类"
message="您确定要删除这个AI分类吗"
itemName={deletingClassificationId ?
classifications.find(c => c.id === deletingClassificationId)?.name :
undefined
}
deleting={submitting}
onConfirm={handleDelete}
onCancel={() => {
setShowDeleteDialog(false);
setDeletingClassificationId(null);
}}
/>
{/* 预览对话框 */}
<AiClassificationPreviewDialog
isOpen={showPreviewDialog}
preview={preview}
loading={previewLoading}
onClose={() => {
setShowPreviewDialog(false);
setPreview(null);
}}
/>
</div>
);
};
export default AiClassificationSettings;