Files
MaliangAINovalWriter/AINoval/lib/screens/settings/widgets/user_template_card.dart
2025-09-10 00:07:52 +08:00

479 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../../../models/prompt_models.dart';
import '../../../utils/web_theme.dart';
/// 用户模板卡片组件
class UserTemplateCard extends StatelessWidget {
final PromptTemplate template;
final bool isSelected;
final bool batchMode;
final VoidCallback? onTap;
final VoidCallback? onEdit;
final VoidCallback? onShare;
final VoidCallback? onFavorite;
final VoidCallback? onDelete;
final ValueChanged<bool>? onSelectionChanged;
UserTemplateCard({
Key? key,
required this.template,
this.isSelected = false,
this.batchMode = false,
this.onTap,
this.onEdit,
this.onShare,
this.onFavorite,
this.onDelete,
this.onSelectionChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
color: isSelected
? WebTheme.getPrimaryColor(context).withOpacity(0.1)
: WebTheme.getCardColor(context),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
const SizedBox(height: 12),
_buildContent(context),
const SizedBox(height: 12),
_buildFooter(context),
],
),
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Row(
children: [
if (batchMode) ...[
Checkbox(
value: isSelected,
onChanged: (value) => onSelectionChanged?.call(value ?? false),
),
const SizedBox(width: 8),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
template.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: WebTheme.getTextColor(context),
),
),
),
_buildStatusIndicators(context),
],
),
if ((template.description ?? '').isNotEmpty) ...[
const SizedBox(height: 4),
Text(
template.description ?? '',
style: TextStyle(
fontSize: 14,
color: WebTheme.getTextColor(context).withOpacity(0.7),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
if (!batchMode) ...[
PopupMenuButton<String>(
onSelected: (value) => _handleMenuAction(value),
itemBuilder: (context) => _buildMenuItems(),
),
],
],
);
}
Widget _buildStatusIndicators(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (template.isFavorite == true)
Container(
margin: const EdgeInsets.only(left: 6),
child: Icon(
Icons.favorite,
size: 16,
color: Colors.red,
),
),
if (template.isPublic == true)
Container(
margin: const EdgeInsets.only(left: 6),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.secondary.withOpacity(0.3),
),
),
child: Text(
'已分享',
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.w500,
),
),
),
if (template.isPublic == false)
Container(
margin: const EdgeInsets.only(left: 6),
child: Icon(
Icons.lock,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
Widget _buildContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 模板内容预览
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
child: Text(
template.content,
style: TextStyle(
fontSize: 13,
fontFamily: 'monospace',
color: WebTheme.getTextColor(context).withOpacity(0.8),
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
if ((template.templateTags ?? const <String>[]).isNotEmpty) ...[
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 4,
children: (template.templateTags ?? const <String>[])
.map((tag) => _buildTag(context, tag))
.toList(),
),
],
],
);
}
Widget _buildTag(BuildContext context, String tag) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: WebTheme.getTextColor(context).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
tag,
style: TextStyle(
fontSize: 11,
color: WebTheme.getTextColor(context).withOpacity(0.7),
),
),
);
}
Widget _buildFooter(BuildContext context) {
return Row(
children: [
// 功能类型
if (template.aiFeatureType != null) ...[
_buildFeatureTypeChip(context),
const SizedBox(width: 12),
],
// 创建时间
Icon(
Icons.access_time,
size: 14,
color: WebTheme.getTextColor(context).withOpacity(0.5),
),
const SizedBox(width: 4),
Text(
_formatDateTime(template.createdAt),
style: TextStyle(
fontSize: 12,
color: WebTheme.getTextColor(context).withOpacity(0.5),
),
),
const SizedBox(width: 16),
// 使用次数
Icon(
Icons.play_circle_outline,
size: 14,
color: WebTheme.getTextColor(context).withOpacity(0.5),
),
const SizedBox(width: 4),
Text(
'使用 ${template.useCount ?? 0}',
style: TextStyle(
fontSize: 12,
color: WebTheme.getTextColor(context).withOpacity(0.5),
),
),
const Spacer(),
// 版本信息
// 版本信息已移除PromptTemplate 无版本字段)
],
);
}
Widget _buildFeatureTypeChip(BuildContext context) {
final featureType = template.aiFeatureType!;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getFeatureTypeColor(featureType).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: _getFeatureTypeColor(featureType).withOpacity(0.3),
),
),
child: Text(
_getFeatureTypeLabel(featureType),
style: TextStyle(
fontSize: 11,
color: _getFeatureTypeColor(featureType),
fontWeight: FontWeight.w500,
),
),
);
}
List<PopupMenuEntry<String>> _buildMenuItems() {
List<PopupMenuEntry<String>> items = [];
// 编辑选项
items.add(const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 18),
SizedBox(width: 8),
Text('编辑'),
],
),
));
// 收藏选项
if (template.isFavorite != true) {
items.add(const PopupMenuItem(
value: 'favorite',
child: Row(
children: [
Icon(Icons.favorite_border, size: 18),
SizedBox(width: 8),
Text('添加到收藏'),
],
),
));
} else {
items.add(const PopupMenuItem(
value: 'unfavorite',
child: Row(
children: [
Icon(Icons.favorite, size: 18, color: Colors.red),
SizedBox(width: 8),
Text('取消收藏'),
],
),
));
}
// 分享选项
if (template.isPublic != true) {
items.add(const PopupMenuItem(
value: 'share',
child: Row(
children: [
Icon(Icons.share, size: 18),
SizedBox(width: 8),
Text('分享到社区'),
],
),
));
}
items.add(const PopupMenuDivider());
// 删除选项
items.add(const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, size: 18, color: Colors.red),
SizedBox(width: 8),
Text('删除', style: TextStyle(color: Colors.red)),
],
),
));
return items;
}
Color _getFeatureTypeColor(AIFeatureType featureType) {
final scheme = Theme.of(_cachedContext!).colorScheme;
switch (featureType) {
case AIFeatureType.aiChat:
return scheme.primary;
case AIFeatureType.novelGeneration:
return scheme.secondary;
case AIFeatureType.novelCompose:
return scheme.secondary; // 与内容生成保持一致的视觉语义
case AIFeatureType.textExpansion:
return scheme.tertiary;
case AIFeatureType.textRefactor:
return scheme.primary;
case AIFeatureType.textSummary:
return scheme.secondary;
case AIFeatureType.sceneToSummary:
return scheme.tertiary;
case AIFeatureType.summaryToScene:
return scheme.primary;
case AIFeatureType.professionalFictionContinuation:
return scheme.primary;
case AIFeatureType.sceneBeatGeneration:
return scheme.secondary;
case AIFeatureType.settingTreeGeneration:
return scheme.tertiary;
}
}
String _getFeatureTypeLabel(AIFeatureType featureType) {
switch (featureType) {
case AIFeatureType.aiChat:
return 'AI聊天';
case AIFeatureType.novelGeneration:
return '场景生成';
case AIFeatureType.novelCompose:
return '设定编排';
case AIFeatureType.textExpansion:
return '扩写';
case AIFeatureType.textRefactor:
return '重构';
case AIFeatureType.textSummary:
return '总结';
case AIFeatureType.sceneToSummary:
return '场景转摘要';
case AIFeatureType.summaryToScene:
return '摘要转场景';
case AIFeatureType.professionalFictionContinuation:
return '专业续写';
case AIFeatureType.sceneBeatGeneration:
return '场景节拍';
case AIFeatureType.settingTreeGeneration:
return '设定树生成';
}
}
// 为了在私有方法中访问 theme缓存一次 context仅在 build 调用期间有效)
final BuildContext? _cachedContext = null;
Widget _buildCard(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
color: isSelected
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
: WebTheme.getCardColor(context),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
const SizedBox(height: 12),
_buildContent(context),
const SizedBox(height: 12),
_buildFooter(context),
],
),
),
),
);
}
String _formatDateTime(DateTime? dateTime) {
if (dateTime == null) return '';
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays > 0) {
return '${difference.inDays}天前';
} else if (difference.inHours > 0) {
return '${difference.inHours}小时前';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}分钟前';
} else {
return '刚刚';
}
}
void _handleMenuAction(String action) {
switch (action) {
case 'edit':
onEdit?.call();
break;
case 'favorite':
case 'unfavorite':
onFavorite?.call();
break;
case 'share':
onShare?.call();
break;
case 'delete':
onDelete?.call();
break;
}
}
}