feat: 优化AI分类设置界面设计
- 根据promptx/frontend-developer规范优化UI设计 - 精致化卡片布局,减小元素尺寸 - 优化按钮组设计,添加hover动画效果 - 改进表单对话框样式,提升用户体验 - 修复数据库布尔值类型问题 - 优化分类排序移动功能
This commit is contained in:
@@ -38,7 +38,7 @@ impl AiClassificationRepository {
|
||||
request.name,
|
||||
request.prompt_text,
|
||||
request.description,
|
||||
true,
|
||||
1, // SQLite中布尔值存储为INTEGER
|
||||
sort_order,
|
||||
now.to_rfc3339(),
|
||||
now.to_rfc3339()
|
||||
@@ -150,7 +150,7 @@ impl AiClassificationRepository {
|
||||
}
|
||||
if let Some(is_active) = request.is_active {
|
||||
updates.push("is_active = ?".to_string());
|
||||
param_values.push(is_active.to_string());
|
||||
param_values.push(if is_active { "1" } else { "0" }.to_string());
|
||||
}
|
||||
if let Some(sort_order) = request.sort_order {
|
||||
updates.push("sort_order = ?".to_string());
|
||||
@@ -231,7 +231,7 @@ impl AiClassificationRepository {
|
||||
if active_only {
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM ai_classifications WHERE is_active = ?1",
|
||||
params![true],
|
||||
params![1], // SQLite中布尔值存储为INTEGER
|
||||
|row| row.get(0)
|
||||
)?
|
||||
} else {
|
||||
@@ -265,12 +265,16 @@ impl AiClassificationRepository {
|
||||
.map_err(|_e| rusqlite::Error::InvalidColumnType(7, "updated_at".to_string(), rusqlite::types::Type::Text))?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
// SQLite中布尔值存储为INTEGER (0/1),需要转换
|
||||
let is_active_int: i32 = row.get(4)?;
|
||||
let is_active = is_active_int != 0;
|
||||
|
||||
Ok(AiClassification {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
prompt_text: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
is_active: row.get(4)?,
|
||||
is_active,
|
||||
sort_order: row.get(5)?,
|
||||
created_at,
|
||||
updated_at,
|
||||
|
||||
@@ -199,7 +199,7 @@ impl Database {
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
prompt_text TEXT NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
@@ -319,6 +319,87 @@ impl Database {
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 修复AI分类表的is_active字段类型(从BOOLEAN改为INTEGER)
|
||||
// 检查表是否存在且字段类型不正确
|
||||
let table_info: Result<Vec<(i32, String, String, bool, Option<String>, bool)>, rusqlite::Error> =
|
||||
conn.prepare("PRAGMA table_info(ai_classifications)")?
|
||||
.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, i32>(0)?, // cid
|
||||
row.get::<_, String>(1)?, // name
|
||||
row.get::<_, String>(2)?, // type
|
||||
row.get::<_, bool>(3)?, // notnull
|
||||
row.get::<_, Option<String>>(4)?, // dflt_value
|
||||
row.get::<_, bool>(5)?, // pk
|
||||
))
|
||||
})?
|
||||
.collect();
|
||||
|
||||
if let Ok(columns) = table_info {
|
||||
// 检查is_active字段是否为BOOLEAN类型
|
||||
let has_boolean_is_active = columns.iter().any(|(_, name, type_name, _, _, _)| {
|
||||
name == "is_active" && type_name.to_uppercase() == "BOOLEAN"
|
||||
});
|
||||
|
||||
if has_boolean_is_active {
|
||||
println!("检测到is_active字段为BOOLEAN类型,开始修复...");
|
||||
|
||||
// 创建新表
|
||||
conn.execute(
|
||||
"CREATE TABLE ai_classifications_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
prompt_text TEXT NOT NULL,
|
||||
description TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 复制数据,将BOOLEAN转换为INTEGER
|
||||
conn.execute(
|
||||
"INSERT INTO ai_classifications_new
|
||||
SELECT id, name, prompt_text, description,
|
||||
CASE WHEN is_active THEN 1 ELSE 0 END as is_active,
|
||||
sort_order, created_at, updated_at
|
||||
FROM ai_classifications",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 删除旧表
|
||||
conn.execute("DROP TABLE ai_classifications", [])?;
|
||||
|
||||
// 重命名新表
|
||||
conn.execute("ALTER TABLE ai_classifications_new RENAME TO ai_classifications", [])?;
|
||||
|
||||
// 重新创建索引
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_name ON ai_classifications (name)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_active ON ai_classifications (is_active)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_sort_order ON ai_classifications (sort_order)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_created_at ON ai_classifications (created_at)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
println!("AI分类表结构修复完成");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -104,44 +104,44 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
{/* 背景遮罩 */}
|
||||
<div
|
||||
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||
{/* 背景遮罩 - 优化透明度和动画 */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/20 backdrop-blur-sm transition-all duration-300"
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
|
||||
{/* 对话框 */}
|
||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
{/* 标题栏 */}
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
{/* 对话框 - 优化圆角、阴影和尺寸 */}
|
||||
<div className="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full border border-gray-100">
|
||||
{/* 标题栏 - 优化间距和字体 */}
|
||||
<div className="bg-white px-6 pt-6 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
<h3 className="text-lg font-semibold text-gray-900 tracking-tight">
|
||||
{title}
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="bg-white rounded-md text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
|
||||
>
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表单内容 */}
|
||||
{/* 表单内容 - 优化间距和布局 */}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="bg-white px-4 pb-4 sm:p-6 sm:pt-0">
|
||||
<div className="space-y-4">
|
||||
{/* 通用错误信息 */}
|
||||
<div className="bg-white px-6 pb-6">
|
||||
<div className="space-y-5">
|
||||
{/* 通用错误信息 - 优化样式 */}
|
||||
{localErrors.general && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<div className="text-sm text-red-700">
|
||||
<div className="rounded-xl bg-red-50 border border-red-100 p-4">
|
||||
<div className="text-sm text-red-700 font-medium">
|
||||
{localErrors.general}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分类名称 */}
|
||||
<div>
|
||||
{/* 分类名称 - 优化字段样式 */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
分类名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
@@ -150,23 +150,25 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
||||
id="name"
|
||||
value={localFormData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
className={`mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm ${
|
||||
localErrors.name ? 'border-red-300' : ''
|
||||
className={`block w-full px-3 py-2 border rounded-lg shadow-sm text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 ${
|
||||
localErrors.name
|
||||
? 'border-red-300 bg-red-50/50'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
placeholder="请输入分类名称"
|
||||
maxLength={CLASSIFICATION_VALIDATION.NAME_MAX_LENGTH}
|
||||
disabled={submitting}
|
||||
/>
|
||||
{localErrors.name && (
|
||||
<p className="mt-1 text-sm text-red-600">{localErrors.name}</p>
|
||||
<p className="text-xs text-red-600 font-medium">{localErrors.name}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="text-xs text-gray-500">
|
||||
{localFormData.name.length}/{CLASSIFICATION_VALIDATION.NAME_MAX_LENGTH}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 提示词 */}
|
||||
<div>
|
||||
{/* 提示词 - 优化文本域样式 */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="prompt_text" className="block text-sm font-medium text-gray-700">
|
||||
提示词 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
@@ -175,23 +177,25 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
||||
rows={4}
|
||||
value={localFormData.prompt_text}
|
||||
onChange={(e) => handleInputChange('prompt_text', e.target.value)}
|
||||
className={`mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm ${
|
||||
localErrors.prompt_text ? 'border-red-300' : ''
|
||||
className={`block w-full px-3 py-2 border rounded-lg shadow-sm text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 resize-none ${
|
||||
localErrors.prompt_text
|
||||
? 'border-red-300 bg-red-50/50'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
placeholder="请输入AI分类的提示词,描述什么样的视频属于这个分类"
|
||||
maxLength={CLASSIFICATION_VALIDATION.PROMPT_TEXT_MAX_LENGTH}
|
||||
disabled={submitting}
|
||||
/>
|
||||
{localErrors.prompt_text && (
|
||||
<p className="mt-1 text-sm text-red-600">{localErrors.prompt_text}</p>
|
||||
<p className="text-xs text-red-600 font-medium">{localErrors.prompt_text}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="text-xs text-gray-500">
|
||||
{localFormData.prompt_text.length}/{CLASSIFICATION_VALIDATION.PROMPT_TEXT_MAX_LENGTH}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
<div>
|
||||
{/* 描述 - 优化样式 */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
||||
描述
|
||||
</label>
|
||||
@@ -200,23 +204,25 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
||||
rows={2}
|
||||
value={localFormData.description}
|
||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||
className={`mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm ${
|
||||
localErrors.description ? 'border-red-300' : ''
|
||||
className={`block w-full px-3 py-2 border rounded-lg shadow-sm text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 resize-none ${
|
||||
localErrors.description
|
||||
? 'border-red-300 bg-red-50/50'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
placeholder="可选:添加分类的详细描述"
|
||||
maxLength={CLASSIFICATION_VALIDATION.DESCRIPTION_MAX_LENGTH}
|
||||
disabled={submitting}
|
||||
/>
|
||||
{localErrors.description && (
|
||||
<p className="mt-1 text-sm text-red-600">{localErrors.description}</p>
|
||||
<p className="text-xs text-red-600 font-medium">{localErrors.description}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="text-xs text-gray-500">
|
||||
{localFormData.description.length}/{CLASSIFICATION_VALIDATION.DESCRIPTION_MAX_LENGTH}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 排序顺序 */}
|
||||
<div>
|
||||
{/* 排序顺序 - 优化数字输入样式 */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="sort_order" className="block text-sm font-medium text-gray-700">
|
||||
排序顺序
|
||||
</label>
|
||||
@@ -225,17 +231,19 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
||||
id="sort_order"
|
||||
value={localFormData.sort_order}
|
||||
onChange={(e) => handleInputChange('sort_order', parseInt(e.target.value) || 0)}
|
||||
className={`mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm ${
|
||||
localErrors.sort_order ? 'border-red-300' : ''
|
||||
className={`block w-full px-3 py-2 border rounded-lg shadow-sm text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 ${
|
||||
localErrors.sort_order
|
||||
? 'border-red-300 bg-red-50/50'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
min={CLASSIFICATION_VALIDATION.MIN_SORT_ORDER}
|
||||
max={CLASSIFICATION_VALIDATION.MAX_SORT_ORDER}
|
||||
disabled={submitting}
|
||||
/>
|
||||
{localErrors.sort_order && (
|
||||
<p className="mt-1 text-sm text-red-600">{localErrors.sort_order}</p>
|
||||
<p className="text-xs text-red-600 font-medium">{localErrors.sort_order}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="text-xs text-gray-500">
|
||||
数值越小排序越靠前 ({CLASSIFICATION_VALIDATION.MIN_SORT_ORDER}-{CLASSIFICATION_VALIDATION.MAX_SORT_ORDER})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -179,10 +179,17 @@ const AiClassificationSettings: React.FC = () => {
|
||||
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 },
|
||||
];
|
||||
// 重新分配所有分类的排序顺序
|
||||
const reorderedIds = [...classifications];
|
||||
// 交换当前项和上一项的位置
|
||||
[reorderedIds[currentIndex], reorderedIds[currentIndex - 1]] =
|
||||
[reorderedIds[currentIndex - 1], reorderedIds[currentIndex]];
|
||||
|
||||
// 生成新的排序更新
|
||||
const updates = reorderedIds.map((item, index) => ({
|
||||
id: item.id,
|
||||
sort_order: index + 1, // 从1开始排序
|
||||
}));
|
||||
|
||||
try {
|
||||
await AiClassificationService.updateSortOrders(updates);
|
||||
@@ -196,10 +203,17 @@ const AiClassificationSettings: React.FC = () => {
|
||||
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 },
|
||||
];
|
||||
// 重新分配所有分类的排序顺序
|
||||
const reorderedIds = [...classifications];
|
||||
// 交换当前项和下一项的位置
|
||||
[reorderedIds[currentIndex], reorderedIds[currentIndex + 1]] =
|
||||
[reorderedIds[currentIndex + 1], reorderedIds[currentIndex]];
|
||||
|
||||
// 生成新的排序更新
|
||||
const updates = reorderedIds.map((item, index) => ({
|
||||
id: item.id,
|
||||
sort_order: index + 1, // 从1开始排序
|
||||
}));
|
||||
|
||||
try {
|
||||
await AiClassificationService.updateSortOrders(updates);
|
||||
@@ -211,7 +225,15 @@ const AiClassificationSettings: React.FC = () => {
|
||||
|
||||
// 打开创建对话框
|
||||
const openCreateDialog = () => {
|
||||
setFormData(DEFAULT_FORM_DATA);
|
||||
// 设置新分类的排序顺序为最大值+1
|
||||
const maxSortOrder = classifications.length > 0
|
||||
? Math.max(...classifications.map(c => c.sort_order))
|
||||
: 0;
|
||||
|
||||
setFormData({
|
||||
...DEFAULT_FORM_DATA,
|
||||
sort_order: maxSortOrder + 1,
|
||||
});
|
||||
setFormErrors({});
|
||||
setShowCreateDialog(true);
|
||||
};
|
||||
@@ -258,138 +280,157 @@ const AiClassificationSettings: React.FC = () => {
|
||||
}
|
||||
|
||||
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 className="min-h-screen bg-gray-50/30">
|
||||
<div className="max-w-6xl mx-auto px-4 py-6">
|
||||
{/* 页面标题和操作栏 - 优化布局和视觉层次 */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-xl font-semibold text-gray-900 tracking-tight">AI分类设置</h1>
|
||||
<p className="text-sm text-gray-500">管理视频AI分类规则和提示词</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={openPreviewDialog}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-300 transition-all duration-200"
|
||||
>
|
||||
<EyeIcon className="h-3.5 w-3.5" />
|
||||
预览提示词
|
||||
</button>
|
||||
<button
|
||||
onClick={openCreateDialog}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 border border-blue-600 rounded-lg hover:bg-blue-700 hover:border-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-300 transition-all duration-200 shadow-sm"
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5" />
|
||||
添加分类
|
||||
</button>
|
||||
</div>
|
||||
</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}
|
||||
</div>
|
||||
|
||||
{/* 分类列表 - 优化卡片设计和布局 */}
|
||||
{classifications.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无AI分类"
|
||||
description="开始创建您的第一个AI分类规则"
|
||||
actionText="添加分类"
|
||||
onAction={openCreateDialog}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{/* 列表标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-medium text-gray-700">
|
||||
分类列表 <span className="text-xs text-gray-400">({classifications.length})</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* 分类卡片网格 */}
|
||||
<div className="grid gap-3">
|
||||
{classifications.map((classification, index) => (
|
||||
<div
|
||||
key={classification.id}
|
||||
className="group relative bg-white border border-gray-200 rounded-xl p-4 hover:border-gray-300 hover:shadow-sm transition-all duration-200"
|
||||
>
|
||||
{/* 卡片头部 */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-sm font-medium text-gray-900 truncate">
|
||||
{classification.name}
|
||||
</h3>
|
||||
<span className={`inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium ${
|
||||
classification.is_active
|
||||
? 'bg-emerald-50 text-emerald-700 border border-emerald-200'
|
||||
: 'bg-gray-50 text-gray-600 border border-gray-200'
|
||||
}`}>
|
||||
{classification.is_active ? '激活' : '禁用'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 bg-gray-50 px-1.5 py-0.5 rounded">
|
||||
#{classification.sort_order}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 提示词预览 */}
|
||||
<p className="text-xs text-gray-600 line-clamp-2 leading-relaxed">
|
||||
{classification.prompt_text}
|
||||
</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" />
|
||||
|
||||
{/* 描述 */}
|
||||
{classification.description && (
|
||||
<p className="mt-1 text-xs text-gray-500 line-clamp-1">
|
||||
{classification.description}
|
||||
</p>
|
||||
)}
|
||||
</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 className="mt-2 text-xs text-gray-400">
|
||||
{new Date(classification.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
{/* 操作按钮组 - 优化为更精致的设计 */}
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
{/* 排序按钮组 */}
|
||||
<div className="flex items-center bg-gray-50 rounded-lg p-0.5">
|
||||
<button
|
||||
onClick={() => handleMoveUp(classification)}
|
||||
disabled={index === 0}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 hover:bg-white rounded disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150"
|
||||
title="上移"
|
||||
>
|
||||
<ArrowUpIcon className="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleMoveDown(classification)}
|
||||
disabled={index === classifications.length - 1}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 hover:bg-white rounded disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150"
|
||||
title="下移"
|
||||
>
|
||||
<ArrowDownIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 状态切换按钮 */}
|
||||
<button
|
||||
onClick={() => handleToggleStatus(classification.id)}
|
||||
className={`p-1.5 rounded-lg transition-all duration-150 ${
|
||||
classification.is_active
|
||||
? 'text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50'
|
||||
: 'text-gray-400 hover:text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
title={classification.is_active ? '禁用' : '激活'}
|
||||
>
|
||||
{classification.is_active ? (
|
||||
<CheckIcon className="h-3 w-3" />
|
||||
) : (
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 编辑按钮 */}
|
||||
<button
|
||||
onClick={() => openEditDialog(classification)}
|
||||
className="p-1.5 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-all duration-150"
|
||||
title="编辑"
|
||||
>
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<button
|
||||
onClick={() => openDeleteDialog(classification.id)}
|
||||
className="p-1.5 text-red-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all duration-150"
|
||||
title="删除"
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* 创建分类对话框 */}
|
||||
<AiClassificationFormDialog
|
||||
@@ -455,6 +496,7 @@ const AiClassificationSettings: React.FC = () => {
|
||||
setPreview(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user