feat: 优化AI分类设置界面设计
- 根据promptx/frontend-developer规范优化UI设计 - 精致化卡片布局,减小元素尺寸 - 优化按钮组设计,添加hover动画效果 - 改进表单对话框样式,提升用户体验 - 修复数据库布尔值类型问题 - 优化分类排序移动功能
This commit is contained in:
@@ -38,7 +38,7 @@ impl AiClassificationRepository {
|
|||||||
request.name,
|
request.name,
|
||||||
request.prompt_text,
|
request.prompt_text,
|
||||||
request.description,
|
request.description,
|
||||||
true,
|
1, // SQLite中布尔值存储为INTEGER
|
||||||
sort_order,
|
sort_order,
|
||||||
now.to_rfc3339(),
|
now.to_rfc3339(),
|
||||||
now.to_rfc3339()
|
now.to_rfc3339()
|
||||||
@@ -150,7 +150,7 @@ impl AiClassificationRepository {
|
|||||||
}
|
}
|
||||||
if let Some(is_active) = request.is_active {
|
if let Some(is_active) = request.is_active {
|
||||||
updates.push("is_active = ?".to_string());
|
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 {
|
if let Some(sort_order) = request.sort_order {
|
||||||
updates.push("sort_order = ?".to_string());
|
updates.push("sort_order = ?".to_string());
|
||||||
@@ -231,7 +231,7 @@ impl AiClassificationRepository {
|
|||||||
if active_only {
|
if active_only {
|
||||||
conn.query_row(
|
conn.query_row(
|
||||||
"SELECT COUNT(*) FROM ai_classifications WHERE is_active = ?1",
|
"SELECT COUNT(*) FROM ai_classifications WHERE is_active = ?1",
|
||||||
params![true],
|
params![1], // SQLite中布尔值存储为INTEGER
|
||||||
|row| row.get(0)
|
|row| row.get(0)
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
@@ -265,12 +265,16 @@ impl AiClassificationRepository {
|
|||||||
.map_err(|_e| rusqlite::Error::InvalidColumnType(7, "updated_at".to_string(), rusqlite::types::Type::Text))?
|
.map_err(|_e| rusqlite::Error::InvalidColumnType(7, "updated_at".to_string(), rusqlite::types::Type::Text))?
|
||||||
.with_timezone(&Utc);
|
.with_timezone(&Utc);
|
||||||
|
|
||||||
|
// SQLite中布尔值存储为INTEGER (0/1),需要转换
|
||||||
|
let is_active_int: i32 = row.get(4)?;
|
||||||
|
let is_active = is_active_int != 0;
|
||||||
|
|
||||||
Ok(AiClassification {
|
Ok(AiClassification {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
name: row.get(1)?,
|
name: row.get(1)?,
|
||||||
prompt_text: row.get(2)?,
|
prompt_text: row.get(2)?,
|
||||||
description: row.get(3)?,
|
description: row.get(3)?,
|
||||||
is_active: row.get(4)?,
|
is_active,
|
||||||
sort_order: row.get(5)?,
|
sort_order: row.get(5)?,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ impl Database {
|
|||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
prompt_text TEXT NOT NULL,
|
prompt_text TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
is_active BOOLEAN DEFAULT 1,
|
is_active INTEGER DEFAULT 1,
|
||||||
sort_order INTEGER DEFAULT 0,
|
sort_order INTEGER DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,44 +104,44 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<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="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
{/* 背景遮罩 */}
|
{/* 背景遮罩 - 优化透明度和动画 */}
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
className="fixed inset-0 bg-black/20 backdrop-blur-sm transition-all duration-300"
|
||||||
onClick={handleCancel}
|
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="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-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="bg-white px-6 pt-6 pb-4">
|
||||||
<div className="flex items-center justify-between">
|
<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}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={handleCancel}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 表单内容 */}
|
{/* 表单内容 - 优化间距和布局 */}
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="bg-white px-4 pb-4 sm:p-6 sm:pt-0">
|
<div className="bg-white px-6 pb-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-5">
|
||||||
{/* 通用错误信息 */}
|
{/* 通用错误信息 - 优化样式 */}
|
||||||
{localErrors.general && (
|
{localErrors.general && (
|
||||||
<div className="rounded-md bg-red-50 p-4">
|
<div className="rounded-xl bg-red-50 border border-red-100 p-4">
|
||||||
<div className="text-sm text-red-700">
|
<div className="text-sm text-red-700 font-medium">
|
||||||
{localErrors.general}
|
{localErrors.general}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 分类名称 */}
|
{/* 分类名称 - 优化字段样式 */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
分类名称 <span className="text-red-500">*</span>
|
分类名称 <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -150,23 +150,25 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
|||||||
id="name"
|
id="name"
|
||||||
value={localFormData.name}
|
value={localFormData.name}
|
||||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
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 ${
|
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' : ''
|
localErrors.name
|
||||||
|
? 'border-red-300 bg-red-50/50'
|
||||||
|
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="请输入分类名称"
|
placeholder="请输入分类名称"
|
||||||
maxLength={CLASSIFICATION_VALIDATION.NAME_MAX_LENGTH}
|
maxLength={CLASSIFICATION_VALIDATION.NAME_MAX_LENGTH}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
/>
|
/>
|
||||||
{localErrors.name && (
|
{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}
|
{localFormData.name.length}/{CLASSIFICATION_VALIDATION.NAME_MAX_LENGTH}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 提示词 */}
|
{/* 提示词 - 优化文本域样式 */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="prompt_text" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="prompt_text" className="block text-sm font-medium text-gray-700">
|
||||||
提示词 <span className="text-red-500">*</span>
|
提示词 <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -175,23 +177,25 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
|||||||
rows={4}
|
rows={4}
|
||||||
value={localFormData.prompt_text}
|
value={localFormData.prompt_text}
|
||||||
onChange={(e) => handleInputChange('prompt_text', e.target.value)}
|
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 ${
|
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' : ''
|
localErrors.prompt_text
|
||||||
|
? 'border-red-300 bg-red-50/50'
|
||||||
|
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="请输入AI分类的提示词,描述什么样的视频属于这个分类"
|
placeholder="请输入AI分类的提示词,描述什么样的视频属于这个分类"
|
||||||
maxLength={CLASSIFICATION_VALIDATION.PROMPT_TEXT_MAX_LENGTH}
|
maxLength={CLASSIFICATION_VALIDATION.PROMPT_TEXT_MAX_LENGTH}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
/>
|
/>
|
||||||
{localErrors.prompt_text && (
|
{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}
|
{localFormData.prompt_text.length}/{CLASSIFICATION_VALIDATION.PROMPT_TEXT_MAX_LENGTH}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 描述 */}
|
{/* 描述 - 优化样式 */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
||||||
描述
|
描述
|
||||||
</label>
|
</label>
|
||||||
@@ -200,23 +204,25 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
|||||||
rows={2}
|
rows={2}
|
||||||
value={localFormData.description}
|
value={localFormData.description}
|
||||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
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 ${
|
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' : ''
|
localErrors.description
|
||||||
|
? 'border-red-300 bg-red-50/50'
|
||||||
|
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
placeholder="可选:添加分类的详细描述"
|
placeholder="可选:添加分类的详细描述"
|
||||||
maxLength={CLASSIFICATION_VALIDATION.DESCRIPTION_MAX_LENGTH}
|
maxLength={CLASSIFICATION_VALIDATION.DESCRIPTION_MAX_LENGTH}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
/>
|
/>
|
||||||
{localErrors.description && (
|
{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}
|
{localFormData.description.length}/{CLASSIFICATION_VALIDATION.DESCRIPTION_MAX_LENGTH}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 排序顺序 */}
|
{/* 排序顺序 - 优化数字输入样式 */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="sort_order" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="sort_order" className="block text-sm font-medium text-gray-700">
|
||||||
排序顺序
|
排序顺序
|
||||||
</label>
|
</label>
|
||||||
@@ -225,17 +231,19 @@ export const AiClassificationFormDialog: React.FC<AiClassificationFormDialogProp
|
|||||||
id="sort_order"
|
id="sort_order"
|
||||||
value={localFormData.sort_order}
|
value={localFormData.sort_order}
|
||||||
onChange={(e) => handleInputChange('sort_order', parseInt(e.target.value) || 0)}
|
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 ${
|
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' : ''
|
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}
|
min={CLASSIFICATION_VALIDATION.MIN_SORT_ORDER}
|
||||||
max={CLASSIFICATION_VALIDATION.MAX_SORT_ORDER}
|
max={CLASSIFICATION_VALIDATION.MAX_SORT_ORDER}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
/>
|
/>
|
||||||
{localErrors.sort_order && (
|
{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})
|
数值越小排序越靠前 ({CLASSIFICATION_VALIDATION.MIN_SORT_ORDER}-{CLASSIFICATION_VALIDATION.MAX_SORT_ORDER})
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -179,10 +179,17 @@ const AiClassificationSettings: React.FC = () => {
|
|||||||
const currentIndex = classifications.findIndex(c => c.id === classification.id);
|
const currentIndex = classifications.findIndex(c => c.id === classification.id);
|
||||||
if (currentIndex <= 0) return;
|
if (currentIndex <= 0) return;
|
||||||
|
|
||||||
const updates = [
|
// 重新分配所有分类的排序顺序
|
||||||
{ id: classification.id, sort_order: classifications[currentIndex - 1].sort_order },
|
const reorderedIds = [...classifications];
|
||||||
{ id: classifications[currentIndex - 1].id, sort_order: classification.sort_order },
|
// 交换当前项和上一项的位置
|
||||||
];
|
[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 {
|
try {
|
||||||
await AiClassificationService.updateSortOrders(updates);
|
await AiClassificationService.updateSortOrders(updates);
|
||||||
@@ -196,10 +203,17 @@ const AiClassificationSettings: React.FC = () => {
|
|||||||
const currentIndex = classifications.findIndex(c => c.id === classification.id);
|
const currentIndex = classifications.findIndex(c => c.id === classification.id);
|
||||||
if (currentIndex >= classifications.length - 1) return;
|
if (currentIndex >= classifications.length - 1) return;
|
||||||
|
|
||||||
const updates = [
|
// 重新分配所有分类的排序顺序
|
||||||
{ id: classification.id, sort_order: classifications[currentIndex + 1].sort_order },
|
const reorderedIds = [...classifications];
|
||||||
{ id: classifications[currentIndex + 1].id, sort_order: classification.sort_order },
|
// 交换当前项和下一项的位置
|
||||||
];
|
[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 {
|
try {
|
||||||
await AiClassificationService.updateSortOrders(updates);
|
await AiClassificationService.updateSortOrders(updates);
|
||||||
@@ -211,7 +225,15 @@ const AiClassificationSettings: React.FC = () => {
|
|||||||
|
|
||||||
// 打开创建对话框
|
// 打开创建对话框
|
||||||
const openCreateDialog = () => {
|
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({});
|
setFormErrors({});
|
||||||
setShowCreateDialog(true);
|
setShowCreateDialog(true);
|
||||||
};
|
};
|
||||||
@@ -258,32 +280,35 @@ const AiClassificationSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="min-h-screen bg-gray-50/30">
|
||||||
{/* 页面标题和操作栏 */}
|
<div className="max-w-6xl mx-auto px-4 py-6">
|
||||||
<div className="flex items-center justify-between mb-6">
|
{/* 页面标题和操作栏 - 优化布局和视觉层次 */}
|
||||||
<div>
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">AI分类设置</h1>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-gray-600 mt-1">管理视频AI分类规则和提示词</p>
|
<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>
|
||||||
<div className="flex space-x-3">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={openPreviewDialog}
|
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"
|
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-4 w-4 mr-2" />
|
<EyeIcon className="h-3.5 w-3.5" />
|
||||||
预览提示词
|
预览提示词
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={openCreateDialog}
|
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"
|
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-4 w-4 mr-2" />
|
<PlusIcon className="h-3.5 w-3.5" />
|
||||||
添加分类
|
添加分类
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 分类列表 */}
|
{/* 分类列表 - 优化卡片设计和布局 */}
|
||||||
{classifications.length === 0 ? (
|
{classifications.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="暂无AI分类"
|
title="暂无AI分类"
|
||||||
@@ -292,96 +317,112 @@ const AiClassificationSettings: React.FC = () => {
|
|||||||
onAction={openCreateDialog}
|
onAction={openCreateDialog}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white shadow rounded-lg overflow-hidden">
|
<div className="space-y-3">
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
{/* 列表标题 */}
|
||||||
<h3 className="text-lg font-medium text-gray-900">
|
<div className="flex items-center justify-between">
|
||||||
分类列表 ({classifications.length})
|
<h2 className="text-sm font-medium text-gray-700">
|
||||||
</h3>
|
分类列表 <span className="text-xs text-gray-400">({classifications.length})</span>
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-gray-200">
|
|
||||||
|
{/* 分类卡片网格 */}
|
||||||
|
<div className="grid gap-3">
|
||||||
{classifications.map((classification, index) => (
|
{classifications.map((classification, index) => (
|
||||||
<div key={classification.id} className="p-6 hover:bg-gray-50">
|
<div
|
||||||
<div className="flex items-start justify-between">
|
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-1 min-w-0">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<h4 className="text-lg font-medium text-gray-900">
|
<h3 className="text-sm font-medium text-gray-900 truncate">
|
||||||
{classification.name}
|
{classification.name}
|
||||||
</h4>
|
</h3>
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
<span className={`inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium ${
|
||||||
classification.is_active
|
classification.is_active
|
||||||
? 'bg-green-100 text-green-800'
|
? 'bg-emerald-50 text-emerald-700 border border-emerald-200'
|
||||||
: 'bg-gray-100 text-gray-800'
|
: 'bg-gray-50 text-gray-600 border border-gray-200'
|
||||||
}`}>
|
}`}>
|
||||||
{classification.is_active ? '激活' : '禁用'}
|
{classification.is_active ? '激活' : '禁用'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-xs text-gray-400 bg-gray-50 px-1.5 py-0.5 rounded">
|
||||||
排序: {classification.sort_order}
|
#{classification.sort_order}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-sm text-gray-600 line-clamp-2">
|
|
||||||
|
{/* 提示词预览 */}
|
||||||
|
<p className="text-xs text-gray-600 line-clamp-2 leading-relaxed">
|
||||||
{classification.prompt_text}
|
{classification.prompt_text}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* 描述 */}
|
||||||
{classification.description && (
|
{classification.description && (
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-xs text-gray-500 line-clamp-1">
|
||||||
{classification.description}
|
{classification.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 时间信息 */}
|
||||||
<div className="mt-2 text-xs text-gray-400">
|
<div className="mt-2 text-xs text-gray-400">
|
||||||
创建时间: {new Date(classification.created_at).toLocaleString()}
|
{new Date(classification.created_at).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 ml-4">
|
{/* 操作按钮组 - 优化为更精致的设计 */}
|
||||||
{/* 排序按钮 */}
|
<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
|
<button
|
||||||
onClick={() => handleMoveUp(classification)}
|
onClick={() => handleMoveUp(classification)}
|
||||||
disabled={index === 0}
|
disabled={index === 0}
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
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="上移"
|
title="上移"
|
||||||
>
|
>
|
||||||
<ArrowUpIcon className="h-4 w-4" />
|
<ArrowUpIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMoveDown(classification)}
|
onClick={() => handleMoveDown(classification)}
|
||||||
disabled={index === classifications.length - 1}
|
disabled={index === classifications.length - 1}
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
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="下移"
|
title="下移"
|
||||||
>
|
>
|
||||||
<ArrowDownIcon className="h-4 w-4" />
|
<ArrowDownIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 状态切换按钮 */}
|
{/* 状态切换按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggleStatus(classification.id)}
|
onClick={() => handleToggleStatus(classification.id)}
|
||||||
className={`p-1 ${
|
className={`p-1.5 rounded-lg transition-all duration-150 ${
|
||||||
classification.is_active
|
classification.is_active
|
||||||
? 'text-green-600 hover:text-green-800'
|
? 'text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50'
|
||||||
: 'text-gray-400 hover:text-gray-600'
|
: 'text-gray-400 hover:text-gray-600 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
title={classification.is_active ? '禁用' : '激活'}
|
title={classification.is_active ? '禁用' : '激活'}
|
||||||
>
|
>
|
||||||
{classification.is_active ? (
|
{classification.is_active ? (
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-3 w-3" />
|
||||||
) : (
|
) : (
|
||||||
<XMarkIcon className="h-4 w-4" />
|
<XMarkIcon className="h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 编辑按钮 */}
|
{/* 编辑按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => openEditDialog(classification)}
|
onClick={() => openEditDialog(classification)}
|
||||||
className="p-1 text-blue-600 hover:text-blue-800"
|
className="p-1.5 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-all duration-150"
|
||||||
title="编辑"
|
title="编辑"
|
||||||
>
|
>
|
||||||
<PencilIcon className="h-4 w-4" />
|
<PencilIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 删除按钮 */}
|
{/* 删除按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => openDeleteDialog(classification.id)}
|
onClick={() => openDeleteDialog(classification.id)}
|
||||||
className="p-1 text-red-600 hover:text-red-800"
|
className="p-1.5 text-red-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all duration-150"
|
||||||
title="删除"
|
title="删除"
|
||||||
>
|
>
|
||||||
<TrashIcon className="h-4 w-4" />
|
<TrashIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -456,6 +497,7 @@ const AiClassificationSettings: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user