马良AI写作初始化仓库

This commit is contained in:
邓滨杰
2025-09-10 00:07:52 +08:00
parent 3c06bb1a03
commit 39c0f8840f
1309 changed files with 318528 additions and 0 deletions

View 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');
}
}

View 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}');
}
}

View 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';
}
}
}

View 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')}';
}
}