马良AI写作初始化仓库
This commit is contained in:
562
AINoval/lib/screens/prompt/widgets/prompt_content_editor.dart
Normal file
562
AINoval/lib/screens/prompt/widgets/prompt_content_editor.dart
Normal file
@@ -0,0 +1,562 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_bloc.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_state.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_event.dart';
|
||||
import 'package:ainoval/models/prompt_models.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
import 'package:ainoval/widgets/common/top_toast.dart';
|
||||
|
||||
/// 提示词内容编辑器
|
||||
class PromptContentEditor extends StatefulWidget {
|
||||
const PromptContentEditor({
|
||||
super.key,
|
||||
required this.prompt,
|
||||
});
|
||||
|
||||
final UserPromptInfo prompt;
|
||||
|
||||
@override
|
||||
State<PromptContentEditor> createState() => _PromptContentEditorState();
|
||||
}
|
||||
|
||||
class _PromptContentEditorState extends State<PromptContentEditor> {
|
||||
late TextEditingController _systemPromptController;
|
||||
late TextEditingController _userPromptController;
|
||||
late FocusNode _systemPromptFocusNode;
|
||||
late FocusNode _userPromptFocusNode;
|
||||
bool _isEdited = false;
|
||||
String _lastFocusedField = 'user'; // 'system' or 'user'
|
||||
|
||||
bool get _isReadOnlyTemplate =>
|
||||
widget.prompt.id.startsWith('system_default_') ||
|
||||
widget.prompt.id.startsWith('public_');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_systemPromptController = TextEditingController(text: widget.prompt.systemPrompt ?? '');
|
||||
_userPromptController = TextEditingController(text: widget.prompt.userPrompt);
|
||||
_systemPromptFocusNode = FocusNode();
|
||||
_userPromptFocusNode = FocusNode();
|
||||
|
||||
// 监听焦点变化
|
||||
_systemPromptFocusNode.addListener(() {
|
||||
if (_systemPromptFocusNode.hasFocus) {
|
||||
_lastFocusedField = 'system';
|
||||
}
|
||||
});
|
||||
_userPromptFocusNode.addListener(() {
|
||||
if (_userPromptFocusNode.hasFocus) {
|
||||
_lastFocusedField = 'user';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(PromptContentEditor oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.prompt.id != widget.prompt.id) {
|
||||
_systemPromptController.text = widget.prompt.systemPrompt ?? '';
|
||||
_userPromptController.text = widget.prompt.userPrompt;
|
||||
_isEdited = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_systemPromptController.dispose();
|
||||
_userPromptController.dispose();
|
||||
_systemPromptFocusNode.dispose();
|
||||
_userPromptFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Container(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 占位符提示
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: _buildPlaceholderChips(),
|
||||
),
|
||||
|
||||
// 左右编辑器布局
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 系统提示词编辑器 - 左侧
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 8, bottom: 16),
|
||||
child: _buildSystemPromptEditor(),
|
||||
),
|
||||
),
|
||||
|
||||
// 分割线
|
||||
Container(
|
||||
width: 1,
|
||||
color: WebTheme.isDarkMode(context)
|
||||
? WebTheme.darkGrey200
|
||||
: WebTheme.grey200,
|
||||
margin: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
|
||||
// 用户提示词编辑器 - 右侧
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 8, right: 16, bottom: 16),
|
||||
child: _buildUserPromptEditor(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 保存按钮(系统/公共模板不显示)
|
||||
if (!_isReadOnlyTemplate && _isEdited)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
|
||||
child: _buildSaveButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建占位符提示
|
||||
Widget _buildPlaceholderChips() {
|
||||
return BlocBuilder<PromptNewBloc, PromptNewState>(
|
||||
builder: (context, state) {
|
||||
// 获取当前功能类型的占位符数据
|
||||
final placeholders = _getPlaceholdersForCurrentFeature(state);
|
||||
|
||||
if (placeholders.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'可用占位符',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: placeholders.map((placeholder) => _buildPlaceholderChip(placeholder)).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建占位符芯片
|
||||
Widget _buildPlaceholderChip(String placeholder) {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
final primaryColor = WebTheme.getPrimaryColor(context);
|
||||
final description = _getPlaceholderDescription(placeholder);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 8, bottom: 4),
|
||||
child: Tooltip(
|
||||
message: description,
|
||||
child: Material(
|
||||
color: isDark
|
||||
? primaryColor.withOpacity(0.15)
|
||||
: primaryColor.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: () => _insertPlaceholder(placeholder),
|
||||
onLongPress: () => _copyPlaceholder(placeholder),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: isDark
|
||||
? primaryColor.withOpacity(0.3)
|
||||
: primaryColor.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.code,
|
||||
size: 14,
|
||||
color: isDark ? primaryColor.withOpacity(0.8) : primaryColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'{{$placeholder}}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? primaryColor.withOpacity(0.9) : primaryColor,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.touch_app_outlined,
|
||||
size: 12,
|
||||
color: isDark
|
||||
? primaryColor.withOpacity(0.6)
|
||||
: primaryColor.withOpacity(0.7),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建系统提示词编辑器
|
||||
Widget _buildSystemPromptEditor() {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_system_daydream_outlined,
|
||||
size: 18,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'系统提示词 (System Prompt)',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'设置AI的角色、行为规则和基本约束条件',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _systemPromptFocusNode.hasFocus
|
||||
? WebTheme.getPrimaryColor(context).withOpacity(0.5)
|
||||
: (isDark ? WebTheme.darkGrey300 : WebTheme.grey300),
|
||||
width: _systemPromptFocusNode.hasFocus ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isDark ? WebTheme.darkGrey50 : WebTheme.white,
|
||||
),
|
||||
child: TextField(
|
||||
controller: _systemPromptController,
|
||||
focusNode: _systemPromptFocusNode,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
readOnly: _isReadOnlyTemplate,
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入系统提示词...\n\n例如:你是一个专业的小说创作助手,请遵循以下原则:\n1. 保持情节连贯性\n2. 角色性格一致\n3. 语言风格统一',
|
||||
hintStyle: TextStyle(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
fontSize: 13,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (!_isReadOnlyTemplate) {
|
||||
setState(() {
|
||||
_isEdited = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建用户提示词编辑器
|
||||
Widget _buildUserPromptEditor() {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
size: 18,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'用户提示词 (User Prompt)',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'包含具体的任务指令和要求,可以使用占位符来动态插入内容',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _userPromptFocusNode.hasFocus
|
||||
? WebTheme.getPrimaryColor(context).withOpacity(0.5)
|
||||
: (isDark ? WebTheme.darkGrey300 : WebTheme.grey300),
|
||||
width: _userPromptFocusNode.hasFocus ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isDark ? WebTheme.darkGrey50 : WebTheme.white,
|
||||
),
|
||||
child: TextField(
|
||||
controller: _userPromptController,
|
||||
focusNode: _userPromptFocusNode,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
readOnly: _isReadOnlyTemplate,
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入用户提示词...\n\n例如:请基于以下设定生成小说情节:\n\n角色:{{character_name}}\n背景:{{story_background}}\n情节要求:{{plot_requirements}}\n\n请确保:\n1. 情节符合角色性格\n2. 与背景设定保持一致\n3. 满足指定的情节要求',
|
||||
hintStyle: TextStyle(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
fontSize: 13,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (!_isReadOnlyTemplate) {
|
||||
setState(() {
|
||||
_isEdited = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建保存按钮
|
||||
Widget _buildSaveButton() {
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.save, size: 16),
|
||||
label: const Text('保存更改'),
|
||||
onPressed: _saveChanges,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 插入占位符
|
||||
void _insertPlaceholder(String placeholder) {
|
||||
if (_isReadOnlyTemplate) return;
|
||||
TextEditingController targetController;
|
||||
|
||||
// 根据最后焦点的字段决定插入位置
|
||||
if (_lastFocusedField == 'system') {
|
||||
targetController = _systemPromptController;
|
||||
} else {
|
||||
targetController = _userPromptController;
|
||||
}
|
||||
|
||||
final currentSelection = targetController.selection;
|
||||
final currentText = targetController.text;
|
||||
final placeholderText = '{{$placeholder}}';
|
||||
|
||||
String newText;
|
||||
int newCursorPosition;
|
||||
|
||||
if (currentSelection.isValid) {
|
||||
// 在光标位置插入
|
||||
final before = currentText.substring(0, currentSelection.start);
|
||||
final after = currentText.substring(currentSelection.end);
|
||||
newText = before + placeholderText + after;
|
||||
newCursorPosition = currentSelection.start + placeholderText.length;
|
||||
} else {
|
||||
// 在末尾插入
|
||||
newText = currentText + placeholderText;
|
||||
newCursorPosition = newText.length;
|
||||
}
|
||||
|
||||
targetController.text = newText;
|
||||
targetController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: newCursorPosition),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_isEdited = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// 复制占位符到剪贴板
|
||||
void _copyPlaceholder(String placeholder) {
|
||||
final placeholderText = '{{$placeholder}}';
|
||||
Clipboard.setData(ClipboardData(text: placeholderText));
|
||||
TopToast.success(context, '已复制 $placeholderText 到剪贴板');
|
||||
}
|
||||
|
||||
/// 保存更改
|
||||
void _saveChanges() {
|
||||
if (_isReadOnlyTemplate) return;
|
||||
final request = UpdatePromptTemplateRequest(
|
||||
systemPrompt: _systemPromptController.text.trim(),
|
||||
userPrompt: _userPromptController.text.trim(),
|
||||
);
|
||||
|
||||
context.read<PromptNewBloc>().add(UpdatePromptDetails(
|
||||
promptId: widget.prompt.id,
|
||||
request: request,
|
||||
));
|
||||
|
||||
setState(() {
|
||||
_isEdited = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// 从当前状态获取功能类型的占位符
|
||||
List<String> _getPlaceholdersForCurrentFeature(PromptNewState state) {
|
||||
// 获取当前选中提示词的功能类型
|
||||
final selectedFeatureType = state.selectedFeatureType;
|
||||
if (selectedFeatureType == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 从 PromptPackage 中获取支持的占位符
|
||||
final package = state.promptPackages[selectedFeatureType];
|
||||
if (package == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return package.supportedPlaceholders.toList()..sort();
|
||||
}
|
||||
|
||||
/// 获取占位符描述
|
||||
String _getPlaceholderDescription(String placeholder) {
|
||||
final state = BlocProvider.of<PromptNewBloc>(context).state;
|
||||
final selectedFeatureType = state.selectedFeatureType;
|
||||
|
||||
if (selectedFeatureType != null) {
|
||||
final package = state.promptPackages[selectedFeatureType];
|
||||
final description = package?.placeholderDescriptions[placeholder];
|
||||
if (description != null && description.isNotEmpty) {
|
||||
return _enhanceDescription(placeholder, description, selectedFeatureType.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return _getDefaultDescription(placeholder);
|
||||
}
|
||||
|
||||
/// 增强占位符描述,添加上下文关系说明
|
||||
String _enhanceDescription(String placeholder, String baseDescription, String featureType) {
|
||||
String contextInfo = '';
|
||||
|
||||
// 分析占位符类型并添加上下文关系说明
|
||||
if (placeholder.contains('character')) {
|
||||
contextInfo = '\n\n🎭 角色上下文:\n• 与角色设定、性格特征相关\n• 可能包含多个角色的层级关系\n• 支持主角、配角、反派等分类';
|
||||
} else if (placeholder.contains('setting') || placeholder.contains('background')) {
|
||||
contextInfo = '\n\n🌍 设定上下文:\n• 与世界观、背景设定相关\n• 可能包含时代、地理、社会等层级\n• 支持主设定和子设定的嵌套关系';
|
||||
} else if (placeholder.contains('plot') || placeholder.contains('story')) {
|
||||
contextInfo = '\n\n📖 情节上下文:\n• 与故事情节、剧情发展相关\n• 可能包含主线、支线的层级关系\n• 支持章节、场景等结构化内容';
|
||||
} else if (placeholder.contains('dialogue') || placeholder.contains('conversation')) {
|
||||
contextInfo = '\n\n💬 对话上下文:\n• 与角色对话、交互相关\n• 可能包含说话者、语调等层级\n• 支持内心独白、旁白等分类';
|
||||
} else if (placeholder.contains('emotion') || placeholder.contains('mood')) {
|
||||
contextInfo = '\n\n💭 情感上下文:\n• 与情感表达、氛围营造相关\n• 可能包含角色情感、环境氛围等层级\n• 支持正面、负面、复杂情感等分类';
|
||||
} else if (placeholder.contains('action') || placeholder.contains('behavior')) {
|
||||
contextInfo = '\n\n⚡ 行为上下文:\n• 与角色行为、动作描述相关\n• 可能包含物理动作、心理活动等层级\n• 支持主动、被动、反应式行为等分类';
|
||||
}
|
||||
|
||||
String usageHint = '\n\n💡 使用提示:\n• 单击插入到光标位置\n• 长按复制到剪贴板\n• 格式:{{' + placeholder + '}}';
|
||||
|
||||
return baseDescription + contextInfo + usageHint;
|
||||
}
|
||||
|
||||
/// 获取默认占位符描述
|
||||
String _getDefaultDescription(String placeholder) {
|
||||
final Map<String, String> defaultDescriptions = {
|
||||
'character_name': '角色名称',
|
||||
'character_description': '角色描述',
|
||||
'story_background': '故事背景',
|
||||
'plot_requirements': '情节要求',
|
||||
'scene_description': '场景描述',
|
||||
'dialogue_content': '对话内容',
|
||||
'emotion_description': '情感描述',
|
||||
'action_description': '行为描述',
|
||||
'setting_details': '设定详情',
|
||||
'context_information': '上下文信息',
|
||||
};
|
||||
|
||||
final baseDescription = defaultDescriptions[placeholder] ?? '占位符:$placeholder';
|
||||
return _enhanceDescription(placeholder, baseDescription, 'unknown');
|
||||
}
|
||||
}
|
||||
561
AINoval/lib/screens/prompt/widgets/prompt_detail_view.dart
Normal file
561
AINoval/lib/screens/prompt/widgets/prompt_detail_view.dart
Normal file
@@ -0,0 +1,561 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_bloc.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_state.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_event.dart';
|
||||
import 'package:ainoval/models/prompt_models.dart';
|
||||
// removed duplicate import
|
||||
import 'package:ainoval/utils/logger.dart';
|
||||
import 'package:ainoval/screens/prompt/widgets/prompt_content_editor.dart';
|
||||
import 'package:ainoval/screens/prompt/widgets/prompt_properties_editor.dart';
|
||||
import 'package:ainoval/widgets/common/top_toast.dart';
|
||||
|
||||
/// 提示词详情视图
|
||||
class PromptDetailView extends StatefulWidget {
|
||||
const PromptDetailView({
|
||||
super.key,
|
||||
this.onBack,
|
||||
});
|
||||
|
||||
final VoidCallback? onBack;
|
||||
|
||||
@override
|
||||
State<PromptDetailView> createState() => _PromptDetailViewState();
|
||||
}
|
||||
|
||||
class _PromptDetailViewState extends State<PromptDetailView>
|
||||
with TickerProviderStateMixin {
|
||||
static const String _tag = 'PromptDetailView';
|
||||
|
||||
late TabController _tabController;
|
||||
|
||||
// 名称输入框控制器
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
|
||||
// 是否处于已编辑但未保存状态
|
||||
bool _isEdited = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// final isDark = WebTheme.isDarkMode(context); // unused
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.03),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(-2, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: BlocConsumer<PromptNewBloc, PromptNewState>(
|
||||
listener: (context, state) {
|
||||
// 当选中的提示词发生变化时,更新名称控制器
|
||||
if (state.selectedPrompt != null) {
|
||||
_nameController.text = state.selectedPrompt!.name;
|
||||
_isEdited = false;
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final prompt = state.selectedPrompt;
|
||||
|
||||
// 确保在非编辑状态下名称与当前提示词保持同步,避免首次点击时显示为空
|
||||
if (prompt != null && !_isEdited && _nameController.text != prompt.name) {
|
||||
_nameController.text = prompt.name;
|
||||
}
|
||||
|
||||
if (prompt == null) {
|
||||
return _buildEmptyView();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 顶部标题栏
|
||||
_buildTopBar(context, prompt, state),
|
||||
|
||||
// 标签栏
|
||||
_buildTabBar(),
|
||||
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
PromptContentEditor(prompt: prompt),
|
||||
PromptPropertiesEditor(prompt: prompt),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建顶部标题栏
|
||||
Widget _buildTopBar(BuildContext context, UserPromptInfo prompt, PromptNewState state) {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
final isSystemDefault = prompt.id.startsWith('system_default_');
|
||||
final isPublicTemplate = prompt.id.startsWith('public_');
|
||||
final isReadOnly = isSystemDefault || isPublicTemplate;
|
||||
|
||||
return Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: isDark ? WebTheme.darkGrey200 : WebTheme.grey200,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮(仅在窄屏幕显示)
|
||||
if (widget.onBack != null) ...[
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? WebTheme.darkGrey200 : WebTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: widget.onBack,
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
size: 18,
|
||||
color: isDark ? WebTheme.darkGrey600 : WebTheme.grey700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
|
||||
// 模板标题
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _nameController,
|
||||
style: WebTheme.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
height: 1.2,
|
||||
),
|
||||
decoration: WebTheme.getBorderlessInputDecoration(
|
||||
hintText: '输入模板名称...',
|
||||
context: context,
|
||||
),
|
||||
cursorColor: WebTheme.getTextColor(context),
|
||||
maxLines: 1,
|
||||
readOnly: isReadOnly,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isEdited = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 操作按钮
|
||||
_buildActionButtons(context, prompt, state),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建操作按钮
|
||||
Widget _buildActionButtons(BuildContext context, UserPromptInfo prompt, PromptNewState state) {
|
||||
// final isDark = WebTheme.isDarkMode(context); // unused
|
||||
final isSystemDefault = prompt.id.startsWith('system_default_');
|
||||
final isPublicTemplate = prompt.id.startsWith('public_');
|
||||
final canSetDefault = !isSystemDefault && !isPublicTemplate;
|
||||
final canEdit = !isSystemDefault && !isPublicTemplate;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 复制按钮
|
||||
_buildIconButton(
|
||||
icon: Icons.copy_outlined,
|
||||
tooltip: '复制模板',
|
||||
onPressed: () {
|
||||
context.read<PromptNewBloc>().add(CopyPromptTemplate(
|
||||
templateId: prompt.id,
|
||||
));
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// 收藏按钮
|
||||
_buildIconButton(
|
||||
icon: prompt.isFavorite ? Icons.star : Icons.star_outline,
|
||||
tooltip: prompt.isFavorite ? '取消收藏' : '收藏',
|
||||
onPressed: () {
|
||||
context.read<PromptNewBloc>().add(ToggleFavoriteStatus(
|
||||
promptId: prompt.id,
|
||||
isFavorite: !prompt.isFavorite,
|
||||
));
|
||||
},
|
||||
),
|
||||
|
||||
if (canSetDefault) ...[
|
||||
const SizedBox(width: 8),
|
||||
// 设为默认按钮
|
||||
_buildIconButton(
|
||||
icon: prompt.isDefault ? Icons.bookmark : Icons.bookmark_outline,
|
||||
tooltip: prompt.isDefault ? '已是默认' : '设为默认',
|
||||
onPressed: prompt.isDefault
|
||||
? null
|
||||
: () {
|
||||
final featureType = state.selectedFeatureType;
|
||||
if (featureType != null) {
|
||||
context.read<PromptNewBloc>().add(SetDefaultTemplate(
|
||||
promptId: prompt.id,
|
||||
featureType: featureType,
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
if (!isSystemDefault && !isPublicTemplate) ...[
|
||||
const SizedBox(width: 8),
|
||||
// 删除按钮
|
||||
_buildIconButton(
|
||||
icon: Icons.delete_outline,
|
||||
tooltip: '删除',
|
||||
onPressed: () => _showDeleteConfirmDialog(context, prompt),
|
||||
),
|
||||
],
|
||||
|
||||
// 保存按钮(系统/公共模板不显示)
|
||||
if (canEdit && (_isEdited || state.isUpdating)) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.grey900,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: state.isUpdating ? null : () => _saveChanges(context, prompt),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (state.isUpdating)
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: WebTheme.white,
|
||||
),
|
||||
)
|
||||
else
|
||||
const Icon(
|
||||
Icons.save,
|
||||
size: 14,
|
||||
color: WebTheme.white,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
state.isUpdating ? '保存中...' : '保存',
|
||||
style: WebTheme.labelSmall.copyWith(
|
||||
color: WebTheme.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建统一的图标按钮
|
||||
Widget _buildIconButton({
|
||||
required IconData icon,
|
||||
required String tooltip,
|
||||
required VoidCallback? onPressed,
|
||||
}) {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? WebTheme.darkGrey200 : WebTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: onPressed,
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: onPressed != null
|
||||
? (isDark ? WebTheme.darkGrey600 : WebTheme.grey700)
|
||||
: (isDark ? WebTheme.darkGrey400 : WebTheme.grey400),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标签栏
|
||||
Widget _buildTabBar() {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: isDark ? WebTheme.darkGrey200 : WebTheme.grey200,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: WebTheme.getPrimaryColor(context),
|
||||
unselectedLabelColor: WebTheme.getSecondaryTextColor(context),
|
||||
indicatorColor: WebTheme.getPrimaryColor(context),
|
||||
indicatorWeight: 3,
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.edit_outlined, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
const Text('内容编辑'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.settings_outlined, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
const Text('属性设置'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建空视图
|
||||
Widget _buildEmptyView() {
|
||||
return Container(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getPrimaryColor(context).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.auto_awesome_outlined,
|
||||
size: 48,
|
||||
color: WebTheme.getPrimaryColor(context).withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'选择一个提示词模板',
|
||||
style: WebTheme.headlineSmall.copyWith(
|
||||
color: WebTheme.getTextColor(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: Text(
|
||||
'在左侧列表中选择一个提示词模板以查看和编辑详情。\n您可以修改模板内容、设置属性、添加标签等。',
|
||||
style: WebTheme.bodyMedium.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildFeatureIcon(Icons.edit_outlined, '编辑内容'),
|
||||
const SizedBox(width: 24),
|
||||
_buildFeatureIcon(Icons.settings_outlined, '设置属性'),
|
||||
const SizedBox(width: 24),
|
||||
_buildFeatureIcon(Icons.label_outline, '管理标签'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建功能图标
|
||||
Widget _buildFeatureIcon(IconData icon, String label) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.isDarkMode(context)
|
||||
? WebTheme.darkGrey200.withOpacity(0.5)
|
||||
: WebTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示删除确认对话框
|
||||
void _showDeleteConfirmDialog(BuildContext context, UserPromptInfo prompt) {
|
||||
// final isDark = WebTheme.isDarkMode(context); // unused
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: WebTheme.getSurfaceColor(context),
|
||||
title: Text(
|
||||
'确认删除',
|
||||
style: WebTheme.titleMedium.copyWith(
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
'确定要删除提示词模板 "${prompt.name}" 吗?此操作无法撤销。',
|
||||
style: WebTheme.bodyMedium.copyWith(
|
||||
color: WebTheme.getTextColor(context, isPrimary: false),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.read<PromptNewBloc>().add(DeletePrompt(
|
||||
promptId: prompt.id,
|
||||
));
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: WebTheme.error,
|
||||
foregroundColor: WebTheme.white,
|
||||
),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 保存更改
|
||||
void _saveChanges(BuildContext context, UserPromptInfo prompt) {
|
||||
if (_nameController.text.trim().isEmpty) {
|
||||
TopToast.warning(context, '模板名称不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
final request = UpdatePromptTemplateRequest(
|
||||
name: _nameController.text.trim(),
|
||||
);
|
||||
|
||||
context.read<PromptNewBloc>().add(UpdatePromptDetails(
|
||||
promptId: prompt.id,
|
||||
request: request,
|
||||
));
|
||||
|
||||
setState(() {
|
||||
_isEdited = false;
|
||||
});
|
||||
|
||||
AppLogger.i(_tag, '保存提示词模板更改: ${prompt.id}');
|
||||
}
|
||||
}
|
||||
665
AINoval/lib/screens/prompt/widgets/prompt_list_view.dart
Normal file
665
AINoval/lib/screens/prompt/widgets/prompt_list_view.dart
Normal file
@@ -0,0 +1,665 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_bloc.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_state.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_event.dart';
|
||||
import 'package:ainoval/models/prompt_models.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
import 'package:ainoval/widgets/common/management_list_widgets.dart';
|
||||
// import 'package:ainoval/utils/logger.dart';
|
||||
|
||||
/// 提示词列表视图
|
||||
class PromptListView extends StatefulWidget {
|
||||
const PromptListView({
|
||||
super.key,
|
||||
required this.onPromptSelected,
|
||||
});
|
||||
|
||||
final Function(String promptId, AIFeatureType featureType) onPromptSelected;
|
||||
|
||||
@override
|
||||
State<PromptListView> createState() => _PromptListViewState();
|
||||
}
|
||||
|
||||
class _PromptListViewState extends State<PromptListView> {
|
||||
// static const String _tag = 'PromptListView';
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: isDark ? WebTheme.darkGrey200 : WebTheme.grey200,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.03),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部标题栏(共享)
|
||||
const ManagementListTopBar(
|
||||
title: '提示词管理',
|
||||
subtitle: 'AI 提示词模板库',
|
||||
icon: Icons.auto_awesome,
|
||||
),
|
||||
|
||||
// 搜索框
|
||||
_buildSearchBar(),
|
||||
|
||||
// 分隔线
|
||||
Container(
|
||||
height: 1,
|
||||
color: isDark ? WebTheme.darkGrey200 : WebTheme.grey200,
|
||||
),
|
||||
|
||||
// 提示词列表
|
||||
Expanded(
|
||||
child: BlocBuilder<PromptNewBloc, PromptNewState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return _buildLoadingView();
|
||||
} else if (state.hasError) {
|
||||
return _buildErrorView(state.errorMessage ?? '加载失败');
|
||||
} else if (!state.hasData) {
|
||||
return _buildEmptyView();
|
||||
} else {
|
||||
return _buildPromptList(state);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 顶部标题栏已由共享组件 ManagementListTopBar 提供
|
||||
|
||||
/// 构建搜索栏
|
||||
Widget _buildSearchBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: WebTheme.getBorderedInputDecoration(
|
||||
hintText: '搜索提示词...',
|
||||
context: context,
|
||||
).copyWith(
|
||||
filled: true,
|
||||
fillColor: WebTheme.getSurfaceColor(context),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
size: 18,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
size: 18,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
context.read<PromptNewBloc>().add(const ClearSearch());
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
),
|
||||
style: WebTheme.bodyMedium.copyWith(color: WebTheme.getTextColor(context)),
|
||||
onChanged: (query) {
|
||||
setState(() {}); // Trigger rebuild for suffix icon
|
||||
context.read<PromptNewBloc>().add(SearchPrompts(query: query));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建加载视图
|
||||
Widget _buildLoadingView() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(WebTheme.getTextColor(context)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'加载提示词中...',
|
||||
style: WebTheme.bodyMedium.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建错误视图
|
||||
Widget _buildErrorView(String message) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: WebTheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: WebTheme.bodyMedium.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<PromptNewBloc>().add(const LoadAllPromptPackages());
|
||||
},
|
||||
style: WebTheme.getPrimaryButtonStyle(context),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建空视图
|
||||
Widget _buildEmptyView() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_awesome_outlined,
|
||||
size: 64,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'没有找到提示词模板',
|
||||
style: WebTheme.headlineSmall.copyWith(
|
||||
color: WebTheme.getTextColor(context),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'请检查网络连接或稍后重试',
|
||||
style: WebTheme.bodyMedium.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建提示词列表
|
||||
Widget _buildPromptList(PromptNewState state) {
|
||||
final promptPackages = state.promptPackages;
|
||||
|
||||
if (promptPackages.isEmpty) {
|
||||
return _buildEmptyView();
|
||||
}
|
||||
|
||||
// 获取所有包的条目列表
|
||||
final packageEntries = promptPackages.entries.toList();
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: packageEntries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = packageEntries[index];
|
||||
final featureType = entry.key;
|
||||
final package = entry.value;
|
||||
|
||||
// 获取该功能类型的所有提示词
|
||||
final allPrompts = _getAllPromptsForFeatureType(featureType, package);
|
||||
|
||||
return _buildFeatureTypeSection(featureType, allPrompts, state);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取指定功能类型的所有提示词(系统默认 + 用户自定义 + 公开模板)
|
||||
List<UserPromptInfo> _getAllPromptsForFeatureType(AIFeatureType featureType, PromptPackage package) {
|
||||
final allPrompts = <UserPromptInfo>[];
|
||||
|
||||
// 检查是否有用户默认模板
|
||||
final hasUserDefault = package.userPrompts.any((prompt) => prompt.isDefault);
|
||||
|
||||
// 1. 添加系统默认提示词
|
||||
if (package.systemPrompt.defaultSystemPrompt.isNotEmpty) {
|
||||
final systemPromptAsUser = UserPromptInfo(
|
||||
id: 'system_default_${featureType.toString()}',
|
||||
name: '系统默认模板',
|
||||
description: '系统提供的默认提示词模板',
|
||||
featureType: featureType,
|
||||
systemPrompt: package.systemPrompt.effectivePrompt,
|
||||
userPrompt: package.systemPrompt.defaultUserPrompt,
|
||||
tags: const ['系统默认'],
|
||||
isDefault: !hasUserDefault, // 当没有用户默认模板时,系统默认模板显示为默认
|
||||
authorId: 'system',
|
||||
createdAt: package.lastUpdated,
|
||||
updatedAt: package.lastUpdated,
|
||||
);
|
||||
allPrompts.add(systemPromptAsUser);
|
||||
}
|
||||
|
||||
// 2. 添加用户自定义提示词
|
||||
allPrompts.addAll(package.userPrompts);
|
||||
|
||||
// 3. 添加公开提示词
|
||||
for (final publicPrompt in package.publicPrompts) {
|
||||
final publicPromptAsUser = UserPromptInfo(
|
||||
id: 'public_${publicPrompt.id}',
|
||||
name: '${publicPrompt.name} ${publicPrompt.isVerified ? '✓' : ''}',
|
||||
description: '${publicPrompt.description ?? ''} (作者: ${publicPrompt.authorName ?? '匿名'})',
|
||||
featureType: featureType,
|
||||
systemPrompt: publicPrompt.systemPrompt,
|
||||
userPrompt: publicPrompt.userPrompt,
|
||||
tags: const ['公开模板'],
|
||||
categories: publicPrompt.categories,
|
||||
isPublic: true,
|
||||
shareCode: publicPrompt.shareCode,
|
||||
isVerified: publicPrompt.isVerified,
|
||||
usageCount: publicPrompt.usageCount.toInt(),
|
||||
favoriteCount: publicPrompt.favoriteCount.toInt(),
|
||||
rating: publicPrompt.rating ?? 0.0,
|
||||
authorId: publicPrompt.authorName,
|
||||
version: publicPrompt.version,
|
||||
language: publicPrompt.language,
|
||||
createdAt: publicPrompt.createdAt,
|
||||
lastUsedAt: publicPrompt.lastUsedAt,
|
||||
updatedAt: publicPrompt.updatedAt,
|
||||
);
|
||||
allPrompts.add(publicPromptAsUser);
|
||||
}
|
||||
|
||||
return allPrompts;
|
||||
}
|
||||
|
||||
/// 构建功能类型分组
|
||||
Widget _buildFeatureTypeSection(
|
||||
AIFeatureType featureType,
|
||||
List<UserPromptInfo> prompts,
|
||||
PromptNewState state,
|
||||
) {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return ExpansionTile(
|
||||
initiallyExpanded: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
collapsedBackgroundColor: Colors.transparent,
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
leading: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: _getFeatureTypeColor(featureType).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Icon(
|
||||
_getFeatureTypeIcon(featureType),
|
||||
size: 14,
|
||||
color: _getFeatureTypeColor(featureType),
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getFeatureTypeName(featureType),
|
||||
style: WebTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// 数量徽章
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? WebTheme.darkGrey200 : WebTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'${prompts.length}',
|
||||
style: WebTheme.labelSmall.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 新建按钮
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? WebTheme.darkGrey200 : WebTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: () {
|
||||
context.read<PromptNewBloc>().add(CreateNewPrompt(
|
||||
featureType: featureType,
|
||||
));
|
||||
},
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
size: 16,
|
||||
color: isDark ? WebTheme.darkGrey600 : WebTheme.grey700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 展开/折叠图标
|
||||
Icon(
|
||||
Icons.expand_more,
|
||||
size: 20,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: prompts.map((prompt) => _buildPromptItem(prompt, featureType, state)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建提示词条目
|
||||
Widget _buildPromptItem(
|
||||
UserPromptInfo prompt,
|
||||
AIFeatureType featureType,
|
||||
PromptNewState state,
|
||||
) {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
final isSelected = state.selectedPromptId == prompt.id;
|
||||
final isSystemDefault = prompt.id.startsWith('system_default_');
|
||||
final isPublicTemplate = prompt.id.startsWith('public_');
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? (isDark ? WebTheme.darkGrey200 : WebTheme.grey100)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: isSelected
|
||||
? Border.all(
|
||||
color: isDark ? WebTheme.darkGrey400 : WebTheme.grey400,
|
||||
width: 1
|
||||
)
|
||||
: Border.all(color: Colors.transparent, width: 1),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: () {
|
||||
widget.onPromptSelected(prompt.id, featureType);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
// 左侧图标
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: _getPromptTypeColor(isSystemDefault, isPublicTemplate).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Icon(
|
||||
_getPromptTypeIcon(isSystemDefault, isPublicTemplate),
|
||||
size: 12,
|
||||
color: _getPromptTypeColor(isSystemDefault, isPublicTemplate),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 主要内容
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
prompt.name,
|
||||
style: WebTheme.bodyMedium.copyWith(
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected
|
||||
? WebTheme.getTextColor(context)
|
||||
: WebTheme.getTextColor(context, isPrimary: false),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// 状态标签
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// 默认标签
|
||||
if (prompt.isDefault)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? const Color(0xFF4A4A4A)
|
||||
: const Color(0xFFFFF3E0),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
'默认',
|
||||
style: WebTheme.labelSmall.copyWith(
|
||||
color: isDark
|
||||
? const Color(0xFFFFB74D)
|
||||
: const Color(0xFFE65100),
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (prompt.isDefault && prompt.isFavorite)
|
||||
const SizedBox(width: 4),
|
||||
|
||||
// 收藏图标
|
||||
if (prompt.isFavorite)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? const Color(0xFF4A4A4A)
|
||||
: const Color(0xFFFFF8E1),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.star,
|
||||
size: 10,
|
||||
color: isDark
|
||||
? const Color(0xFFFFB74D)
|
||||
: const Color(0xFFFF8F00),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (prompt.description != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
prompt.description!,
|
||||
style: WebTheme.bodySmall.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// 类型标签(共享)
|
||||
ManagementTypeChip(
|
||||
type: isSystemDefault
|
||||
? 'System'
|
||||
: isPublicTemplate
|
||||
? 'Public'
|
||||
: 'Custom',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 类型标签由共享组件 ManagementTypeChip 提供
|
||||
|
||||
/// 获取提示词类型图标
|
||||
IconData _getPromptTypeIcon(bool isSystemDefault, bool isPublicTemplate) {
|
||||
if (isSystemDefault) return Icons.settings;
|
||||
if (isPublicTemplate) return Icons.public;
|
||||
return Icons.person;
|
||||
}
|
||||
|
||||
/// 获取提示词类型颜色
|
||||
Color _getPromptTypeColor(bool isSystemDefault, bool isPublicTemplate) {
|
||||
if (isSystemDefault) return const Color(0xFF1565C0); // 优雅的蓝色
|
||||
if (isPublicTemplate) return const Color(0xFF2E7D32); // 优雅的绿色
|
||||
return const Color(0xFF7B1FA2); // 优雅的紫色
|
||||
}
|
||||
|
||||
/// 获取功能类型图标
|
||||
IconData _getFeatureTypeIcon(AIFeatureType featureType) {
|
||||
switch (featureType) {
|
||||
case AIFeatureType.sceneToSummary:
|
||||
return Icons.summarize;
|
||||
case AIFeatureType.summaryToScene:
|
||||
return Icons.expand_more;
|
||||
case AIFeatureType.textExpansion:
|
||||
return Icons.unfold_more;
|
||||
case AIFeatureType.textRefactor:
|
||||
return Icons.edit;
|
||||
case AIFeatureType.textSummary:
|
||||
return Icons.notes;
|
||||
case AIFeatureType.aiChat:
|
||||
return Icons.chat;
|
||||
case AIFeatureType.novelGeneration:
|
||||
return Icons.create;
|
||||
case AIFeatureType.novelCompose:
|
||||
return Icons.dashboard_customize; // 编排/组合的语义
|
||||
case AIFeatureType.professionalFictionContinuation:
|
||||
return Icons.auto_stories;
|
||||
case AIFeatureType.sceneBeatGeneration:
|
||||
return Icons.timeline;
|
||||
case AIFeatureType.settingTreeGeneration:
|
||||
return Icons.account_tree;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取功能类型颜色
|
||||
Color _getFeatureTypeColor(AIFeatureType featureType) {
|
||||
switch (featureType) {
|
||||
case AIFeatureType.sceneToSummary:
|
||||
return const Color(0xFF1976D2); // 蓝色
|
||||
case AIFeatureType.summaryToScene:
|
||||
return const Color(0xFF388E3C); // 绿色
|
||||
case AIFeatureType.textExpansion:
|
||||
return const Color(0xFF7B1FA2); // 紫色
|
||||
case AIFeatureType.textRefactor:
|
||||
return const Color(0xFFE64A19); // 深橙色
|
||||
case AIFeatureType.textSummary:
|
||||
return const Color(0xFF5D4037); // 棕色
|
||||
case AIFeatureType.aiChat:
|
||||
return const Color(0xFF0288D1); // 青色
|
||||
case AIFeatureType.novelGeneration:
|
||||
return const Color(0xFFD32F2F); // 红色
|
||||
case AIFeatureType.novelCompose:
|
||||
return const Color(0xFFD32F2F); // 与生成保持一致
|
||||
case AIFeatureType.professionalFictionContinuation:
|
||||
return const Color(0xFF303F9F); // 靛蓝色
|
||||
case AIFeatureType.sceneBeatGeneration:
|
||||
return const Color(0xFF795548); // 棕色
|
||||
case AIFeatureType.settingTreeGeneration:
|
||||
return const Color(0xFF689F38); // 浅绿色
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取功能类型名称
|
||||
String _getFeatureTypeName(AIFeatureType featureType) {
|
||||
switch (featureType) {
|
||||
case AIFeatureType.sceneToSummary:
|
||||
return 'Scene Beat Completions';
|
||||
case AIFeatureType.summaryToScene:
|
||||
return 'Summary Expansions';
|
||||
case AIFeatureType.textExpansion:
|
||||
return 'Text Expansion';
|
||||
case AIFeatureType.textRefactor:
|
||||
return 'Text Refactor';
|
||||
case AIFeatureType.textSummary:
|
||||
return 'Text Summary';
|
||||
case AIFeatureType.aiChat:
|
||||
return 'AI Chat';
|
||||
case AIFeatureType.novelGeneration:
|
||||
return 'Novel Generation';
|
||||
case AIFeatureType.novelCompose:
|
||||
return 'Novel Compose';
|
||||
case AIFeatureType.professionalFictionContinuation:
|
||||
return 'Professional Fiction Continuation';
|
||||
case AIFeatureType.sceneBeatGeneration:
|
||||
return 'Scene Beat Generation';
|
||||
case AIFeatureType.settingTreeGeneration:
|
||||
return 'Setting Tree Generation';
|
||||
}
|
||||
}
|
||||
}
|
||||
668
AINoval/lib/screens/prompt/widgets/prompt_properties_editor.dart
Normal file
668
AINoval/lib/screens/prompt/widgets/prompt_properties_editor.dart
Normal file
@@ -0,0 +1,668 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_bloc.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_event.dart';
|
||||
import 'package:ainoval/models/prompt_models.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
|
||||
/// 提示词属性编辑器
|
||||
class PromptPropertiesEditor extends StatefulWidget {
|
||||
const PromptPropertiesEditor({
|
||||
super.key,
|
||||
required this.prompt,
|
||||
});
|
||||
|
||||
final UserPromptInfo prompt;
|
||||
|
||||
@override
|
||||
State<PromptPropertiesEditor> createState() => _PromptPropertiesEditorState();
|
||||
}
|
||||
|
||||
class _PromptPropertiesEditorState extends State<PromptPropertiesEditor> {
|
||||
late TextEditingController _descriptionController;
|
||||
late List<String> _tags;
|
||||
late List<String> _categories;
|
||||
final TextEditingController _tagInputController = TextEditingController();
|
||||
final TextEditingController _categoryInputController = TextEditingController();
|
||||
bool _isEdited = false;
|
||||
bool get _isReadOnlyTemplate =>
|
||||
widget.prompt.id.startsWith('system_default_') ||
|
||||
widget.prompt.id.startsWith('public_');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_descriptionController = TextEditingController(text: widget.prompt.description ?? '');
|
||||
_tags = List.from(widget.prompt.tags);
|
||||
_categories = []; // UserPromptInfo 没有 categories 字段,这里留空
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(PromptPropertiesEditor oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.prompt.id != widget.prompt.id) {
|
||||
_descriptionController.text = widget.prompt.description ?? '';
|
||||
_tags = List.from(widget.prompt.tags);
|
||||
_categories = [];
|
||||
_isEdited = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_descriptionController.dispose();
|
||||
_tagInputController.dispose();
|
||||
_categoryInputController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 页面标题
|
||||
_buildPageHeader(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 描述
|
||||
_buildDescriptionEditor(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 标签
|
||||
_buildTagsEditor(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 分类
|
||||
_buildCategoriesEditor(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 收藏状态
|
||||
_buildFavoriteToggle(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 元数据
|
||||
_buildMetadata(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 保存按钮(系统/公共模板不显示)
|
||||
if (!_isReadOnlyTemplate && _isEdited) _buildSaveButton(),
|
||||
|
||||
// 底部留白
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建页面标题
|
||||
Widget _buildPageHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_outlined,
|
||||
size: 20,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'模板属性设置',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建描述编辑器
|
||||
Widget _buildDescriptionEditor() {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.description_outlined,
|
||||
size: 16,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'模板描述',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'为模板添加详细的功能描述和使用说明',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isDark ? WebTheme.darkGrey300 : WebTheme.grey300,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isDark ? WebTheme.darkGrey50 : WebTheme.white,
|
||||
),
|
||||
child: TextField(
|
||||
controller: _descriptionController,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
readOnly: _isReadOnlyTemplate,
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入模板描述...\n\n例如:用于生成小说角色对话的模板,适用于日常对话、情感表达等场景。',
|
||||
hintStyle: TextStyle(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
fontSize: 13,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (!_isReadOnlyTemplate) {
|
||||
setState(() {
|
||||
_isEdited = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标签编辑器
|
||||
Widget _buildTagsEditor() {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.label_outline,
|
||||
size: 16,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'标签管理',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'添加相关标签便于分类和搜索模板',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 现有标签
|
||||
if (_tags.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: _tags.map((tag) => _buildEditableChip(
|
||||
tag,
|
||||
onDeleted: () {
|
||||
if (_isReadOnlyTemplate) return;
|
||||
setState(() {
|
||||
_tags.remove(tag);
|
||||
_isEdited = true;
|
||||
});
|
||||
},
|
||||
)).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 添加标签输入框
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _tagInputController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '添加标签...',
|
||||
hintStyle: TextStyle(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: isDark ? WebTheme.darkGrey300 : WebTheme.grey300,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: isDark ? WebTheme.darkGrey300 : WebTheme.grey300,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: isDark ? WebTheme.darkGrey50 : WebTheme.white,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
isDense: true,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
onSubmitted: _isReadOnlyTemplate ? null : _addTag,
|
||||
readOnly: _isReadOnlyTemplate,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
onPressed: _isReadOnlyTemplate ? null : () => _addTag(_tagInputController.text),
|
||||
tooltip: '添加标签',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建分类编辑器
|
||||
Widget _buildCategoriesEditor() {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.category_outlined,
|
||||
size: 16,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'分类管理',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'设置模板所属的功能分类,支持多级分类',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 现有分类
|
||||
if (_categories.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: _categories.map((category) => _buildEditableChip(
|
||||
category,
|
||||
color: isDark ? Theme.of(context).colorScheme.primary.withOpacity(0.25) : Theme.of(context).colorScheme.primary.withOpacity(0.12),
|
||||
textColor: isDark ? Theme.of(context).colorScheme.primaryContainer : Theme.of(context).colorScheme.primary,
|
||||
onDeleted: () {
|
||||
if (_isReadOnlyTemplate) return;
|
||||
setState(() {
|
||||
_categories.remove(category);
|
||||
_isEdited = true;
|
||||
});
|
||||
},
|
||||
)).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 添加分类输入框
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _categoryInputController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '添加分类...',
|
||||
hintStyle: TextStyle(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: isDark ? WebTheme.darkGrey300 : WebTheme.grey300,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: isDark ? WebTheme.darkGrey300 : WebTheme.grey300,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: isDark ? WebTheme.darkGrey50 : WebTheme.white,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
isDense: true,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
onSubmitted: _isReadOnlyTemplate ? null : _addCategory,
|
||||
readOnly: _isReadOnlyTemplate,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
onPressed: _isReadOnlyTemplate ? null : () => _addCategory(_categoryInputController.text),
|
||||
tooltip: '添加分类',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建收藏开关
|
||||
Widget _buildFavoriteToggle() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.isDarkMode(context)
|
||||
? WebTheme.darkGrey100.withOpacity(0.3)
|
||||
: WebTheme.grey50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: WebTheme.isDarkMode(context)
|
||||
? WebTheme.darkGrey200
|
||||
: WebTheme.grey200,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
widget.prompt.isFavorite ? Icons.star : Icons.star_outline,
|
||||
size: 20,
|
||||
color: widget.prompt.isFavorite
|
||||
? Colors.amber
|
||||
: WebTheme.getTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'收藏模板',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'收藏后可在收藏列表中快速找到',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: widget.prompt.isFavorite,
|
||||
onChanged: _isReadOnlyTemplate
|
||||
? null
|
||||
: (value) {
|
||||
context.read<PromptNewBloc>().add(ToggleFavoriteStatus(
|
||||
promptId: widget.prompt.id,
|
||||
isFavorite: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建元数据
|
||||
Widget _buildMetadata() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'模板信息',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.isDarkMode(context)
|
||||
? WebTheme.darkGrey100.withOpacity(0.3)
|
||||
: WebTheme.grey50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: WebTheme.isDarkMode(context)
|
||||
? WebTheme.darkGrey200
|
||||
: WebTheme.grey200,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildMetadataRow('创建时间', _formatDateTime(widget.prompt.updatedAt), Icons.access_time),
|
||||
const Divider(height: 16),
|
||||
_buildMetadataRow('更新时间', _formatDateTime(widget.prompt.updatedAt), Icons.update),
|
||||
const Divider(height: 16),
|
||||
_buildMetadataRow('使用次数', '${widget.prompt.usageCount}', Icons.trending_up),
|
||||
if (widget.prompt.lastUsedAt != null) ...[
|
||||
const Divider(height: 16),
|
||||
_buildMetadataRow('最后使用', _formatDateTime(widget.prompt.lastUsedAt!), Icons.schedule),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建元数据行
|
||||
Widget _buildMetadataRow(String label, String value, [IconData? icon]) {
|
||||
return Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建可编辑芯片
|
||||
Widget _buildEditableChip(
|
||||
String label, {
|
||||
Color? color,
|
||||
Color? textColor,
|
||||
VoidCallback? onDeleted,
|
||||
}) {
|
||||
final isDark = WebTheme.isDarkMode(context);
|
||||
|
||||
return Chip(
|
||||
label: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: textColor ?? (isDark ? WebTheme.white : WebTheme.getTextColor(context)),
|
||||
),
|
||||
),
|
||||
backgroundColor: color ?? (isDark ? WebTheme.darkGrey300 : WebTheme.grey200),
|
||||
deleteIcon: Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: textColor ?? (isDark ? WebTheme.white : WebTheme.getTextColor(context)),
|
||||
),
|
||||
onDeleted: onDeleted,
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建保存按钮
|
||||
Widget _buildSaveButton() {
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.save, size: 16),
|
||||
label: const Text('保存更改'),
|
||||
onPressed: _saveChanges,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 添加标签
|
||||
void _addTag(String tag) {
|
||||
final trimmedTag = tag.trim();
|
||||
if (trimmedTag.isNotEmpty && !_tags.contains(trimmedTag)) {
|
||||
setState(() {
|
||||
_tags.add(trimmedTag);
|
||||
_tagInputController.clear();
|
||||
_isEdited = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加分类
|
||||
void _addCategory(String category) {
|
||||
final trimmedCategory = category.trim();
|
||||
if (trimmedCategory.isNotEmpty && !_categories.contains(trimmedCategory)) {
|
||||
setState(() {
|
||||
_categories.add(trimmedCategory);
|
||||
_categoryInputController.clear();
|
||||
_isEdited = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存更改
|
||||
void _saveChanges() {
|
||||
final request = UpdatePromptTemplateRequest(
|
||||
description: _descriptionController.text.trim().isEmpty
|
||||
? null
|
||||
: _descriptionController.text.trim(),
|
||||
tags: _tags,
|
||||
categories: _categories,
|
||||
);
|
||||
|
||||
context.read<PromptNewBloc>().add(UpdatePromptDetails(
|
||||
promptId: widget.prompt.id,
|
||||
request: request,
|
||||
));
|
||||
|
||||
setState(() {
|
||||
_isEdited = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// 格式化日期时间
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} '
|
||||
'${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user