马良AI写作初始化仓库
This commit is contained in:
547
AINoval/lib/screens/settings/registration_settings_screen.dart
Normal file
547
AINoval/lib/screens/settings/registration_settings_screen.dart
Normal file
@@ -0,0 +1,547 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/models/app_registration_config.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
|
||||
/// 注册设置管理页面
|
||||
/// 用于管理应用的注册功能配置
|
||||
class RegistrationSettingsScreen extends StatefulWidget {
|
||||
const RegistrationSettingsScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RegistrationSettingsScreen> createState() => _RegistrationSettingsScreenState();
|
||||
}
|
||||
|
||||
class _RegistrationSettingsScreenState extends State<RegistrationSettingsScreen> {
|
||||
RegistrationConfig? _config;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadConfiguration();
|
||||
}
|
||||
|
||||
/// 加载当前配置
|
||||
Future<void> _loadConfiguration() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final config = RegistrationConfig(
|
||||
phoneRegistrationEnabled: await AppRegistrationConfig.isPhoneRegistrationEnabled(),
|
||||
emailRegistrationEnabled: await AppRegistrationConfig.isEmailRegistrationEnabled(),
|
||||
verificationRequired: await AppRegistrationConfig.isVerificationRequired(),
|
||||
quickRegistrationEnabled: await AppRegistrationConfig.isQuickRegistrationEnabled(),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_config = config;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
_showError('加载配置失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新配置
|
||||
Future<void> _updateConfiguration(RegistrationConfig newConfig) async {
|
||||
try {
|
||||
await AppRegistrationConfig.setPhoneRegistrationEnabled(newConfig.phoneRegistrationEnabled);
|
||||
await AppRegistrationConfig.setEmailRegistrationEnabled(newConfig.emailRegistrationEnabled);
|
||||
await AppRegistrationConfig.setVerificationRequired(newConfig.verificationRequired);
|
||||
await AppRegistrationConfig.setQuickRegistrationEnabled(newConfig.quickRegistrationEnabled);
|
||||
|
||||
setState(() {
|
||||
_config = newConfig;
|
||||
});
|
||||
|
||||
_showSuccess('配置已保存');
|
||||
} catch (e) {
|
||||
_showError('保存配置失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置到默认配置
|
||||
Future<void> _resetToDefaults() async {
|
||||
try {
|
||||
await AppRegistrationConfig.resetToDefaults();
|
||||
await _loadConfiguration();
|
||||
_showSuccess('已重置为默认配置');
|
||||
} catch (e) {
|
||||
_showError('重置配置失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示成功消息
|
||||
void _showSuccess(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Theme.of(context).colorScheme.onPrimary, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(message),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示错误消息
|
||||
void _showError(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.error, color: Theme.of(context).colorScheme.onError, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Expanded(child: Text(message)),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示确认对话框
|
||||
Future<bool> _showConfirmDialog(String title, String content) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('注册设置'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: _loadConfiguration,
|
||||
tooltip: '刷新配置',
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) async {
|
||||
if (value == 'reset') {
|
||||
final confirmed = await _showConfirmDialog(
|
||||
'重置配置',
|
||||
'确定要将所有注册设置重置为默认值吗?',
|
||||
);
|
||||
if (confirmed) {
|
||||
_resetToDefaults();
|
||||
}
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'reset',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.restore, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('重置为默认'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: _config == null
|
||||
? _buildErrorState()
|
||||
: _buildConfigurationForm(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建错误状态
|
||||
Widget _buildErrorState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'加载配置失败',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'请检查应用权限或重新启动应用',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _loadConfiguration,
|
||||
child: Text('重新加载'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建配置表单
|
||||
Widget _buildConfigurationForm() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 配置概览卡片
|
||||
_buildOverviewCard(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 注册方式设置
|
||||
_buildRegistrationMethodsSection(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 验证设置
|
||||
_buildVerificationSection(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 预览和测试
|
||||
_buildPreviewSection(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建概览卡片
|
||||
Widget _buildOverviewCard() {
|
||||
final availableMethods = _config!.availableMethods;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'当前配置状态',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
_config!.quickRegistrationEnabled ? Icons.flash_on : Icons.flash_off,
|
||||
color: _config!.quickRegistrationEnabled ? Theme.of(context).colorScheme.primary : WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text('快捷注册: ${_config!.quickRegistrationEnabled ? "开启" : "关闭"}'),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
if (availableMethods.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'警告:当前没有启用邮箱或手机注册方式!',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
Text(
|
||||
'可用的注册方式: ${availableMethods.map((m) => m.displayName).join('、')}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'验证码验证: ${_config!.verificationRequired ? "必需" : "可选"}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建注册方式设置区域
|
||||
Widget _buildRegistrationMethodsSection() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'注册方式',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// 快捷注册开关
|
||||
_buildSettingTile(
|
||||
title: '快捷注册',
|
||||
subtitle: '仅用户名+密码,无需邮箱/手机与验证码',
|
||||
value: _config!.quickRegistrationEnabled,
|
||||
icon: Icons.flash_on,
|
||||
onChanged: (value) {
|
||||
_updateConfiguration(_config!.copyWith(
|
||||
quickRegistrationEnabled: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
// 邮箱注册开关
|
||||
_buildSettingTile(
|
||||
title: '邮箱注册',
|
||||
subtitle: '允许用户通过邮箱地址注册账户',
|
||||
value: _config!.emailRegistrationEnabled,
|
||||
icon: Icons.email,
|
||||
onChanged: (value) {
|
||||
_updateConfiguration(_config!.copyWith(
|
||||
emailRegistrationEnabled: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
|
||||
Divider(),
|
||||
|
||||
// 手机注册开关
|
||||
_buildSettingTile(
|
||||
title: '手机注册',
|
||||
subtitle: '允许用户通过手机号注册账户',
|
||||
value: _config!.phoneRegistrationEnabled,
|
||||
icon: Icons.phone,
|
||||
onChanged: (value) {
|
||||
_updateConfiguration(_config!.copyWith(
|
||||
phoneRegistrationEnabled: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建验证设置区域
|
||||
Widget _buildVerificationSection() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'验证设置',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
_buildSettingTile(
|
||||
title: '验证码验证',
|
||||
subtitle: '注册时是否必须进行邮箱或手机验证码验证',
|
||||
value: _config!.verificationRequired,
|
||||
icon: Icons.verified_user,
|
||||
onChanged: (value) {
|
||||
_updateConfiguration(_config!.copyWith(
|
||||
verificationRequired: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建预览区域
|
||||
Widget _buildPreviewSection() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'配置预览',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.isDarkMode(context)
|
||||
? WebTheme.darkGrey800
|
||||
: WebTheme.grey100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'注册页面将显示的选项:',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
if (_config!.availableMethods.isEmpty) ...[
|
||||
Text(
|
||||
'• 无邮箱/手机注册(建议开启快捷注册以允许用户注册)',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
for (final method in _config!.availableMethods)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
method == RegistrationMethod.email
|
||||
? Icons.email
|
||||
: Icons.phone,
|
||||
size: 16,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text('${method.displayName}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_config!.verificationRequired) ...[
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.security,
|
||||
size: 16,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text('需要验证码验证'),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (_config!.availableMethods.isNotEmpty) ...[
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'提示:用户可以使用 EnhancedLoginScreen 进行注册',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建设置项
|
||||
Widget _buildSettingTile({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required bool value,
|
||||
required IconData icon,
|
||||
required ValueChanged<bool> onChanged,
|
||||
}) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
icon,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
trailing: Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
609
AINoval/lib/screens/settings/settings_panel.dart
Normal file
609
AINoval/lib/screens/settings/settings_panel.dart
Normal file
@@ -0,0 +1,609 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:ainoval/widgets/common/top_toast.dart';
|
||||
|
||||
import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart';
|
||||
import 'package:ainoval/models/editor_settings.dart';
|
||||
import 'package:ainoval/models/user_ai_model_config_model.dart';
|
||||
import 'package:ainoval/screens/editor/managers/editor_state_manager.dart';
|
||||
import 'package:ainoval/screens/settings/widgets/ai_config_form.dart';
|
||||
import 'package:ainoval/screens/settings/widgets/model_service_list_page.dart';
|
||||
import 'package:ainoval/screens/settings/widgets/editor_settings_panel.dart';
|
||||
import 'package:ainoval/screens/settings/widgets/membership_panel.dart' as membership;
|
||||
import 'package:ainoval/screens/settings/widgets/account_management_panel.dart';
|
||||
// import 'package:ainoval/widgets/common/settings_widgets.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/novel_repository_impl.dart';
|
||||
import 'package:ainoval/utils/logger.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
|
||||
class SettingsPanel extends StatefulWidget {
|
||||
const SettingsPanel({
|
||||
super.key,
|
||||
required this.onClose,
|
||||
required this.userId,
|
||||
this.editorSettings,
|
||||
this.onEditorSettingsChanged,
|
||||
required this.stateManager,
|
||||
this.initialCategoryIndex = 0,
|
||||
});
|
||||
final VoidCallback onClose;
|
||||
final String userId;
|
||||
final EditorSettings? editorSettings;
|
||||
final Function(EditorSettings)? onEditorSettingsChanged;
|
||||
final EditorStateManager stateManager;
|
||||
final int initialCategoryIndex;
|
||||
|
||||
/// 账户管理分类的索引
|
||||
static const int accountManagementCategoryIndex = 1;
|
||||
|
||||
@override
|
||||
State<SettingsPanel> createState() => _SettingsPanelState();
|
||||
}
|
||||
|
||||
class _SettingsPanelState extends State<SettingsPanel> {
|
||||
int _selectedIndex = 0; // Track the selected category index
|
||||
UserAIModelConfigModel?
|
||||
_configToEdit; // Track config being edited, null for add mode
|
||||
bool _showAddEditForm = false; // Flag to show the add/edit form view
|
||||
late EditorSettings _editorSettings;
|
||||
// 🚀 新增:NovelRepository实例用于调用后端API
|
||||
late NovelRepositoryImpl _novelRepository;
|
||||
|
||||
// Define category titles and icons (adjust as needed)
|
||||
final List<Map<String, dynamic>> _categories = [
|
||||
{'title': '模型服务', 'icon': Icons.cloud_queue},
|
||||
{'title': '账户管理', 'icon': Icons.account_circle_outlined},
|
||||
{'title': '会员与订阅', 'icon': Icons.workspace_premium},
|
||||
// {'title': '默认模型', 'icon': Icons.star_border}, // Example: Can be added later
|
||||
// {'title': '网络搜索', 'icon': Icons.search},
|
||||
// {'title': 'MCP 服务器', 'icon': Icons.dns},
|
||||
{'title': '常规设置', 'icon': Icons.settings_outlined},
|
||||
{'title': '显示设置', 'icon': Icons.display_settings},
|
||||
{'title': '主题设置', 'icon': Icons.palette_outlined},
|
||||
{'title': '编辑器设置', 'icon': Icons.edit_note},
|
||||
// {'title': '快捷方式', 'icon': Icons.shortcut},
|
||||
// {'title': '快捷助手', 'icon': Icons.assistant_photo},
|
||||
// {'title': '数据设置', 'icon': Icons.data_usage},
|
||||
// {'title': '关于我们\', 'icon': Icons.info_outline},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_editorSettings = widget.editorSettings ?? const EditorSettings();
|
||||
// 🚀 初始化NovelRepository
|
||||
_novelRepository = NovelRepositoryImpl();
|
||||
// 设置初始分类索引
|
||||
_selectedIndex = widget.initialCategoryIndex;
|
||||
}
|
||||
|
||||
void _showAddForm() {
|
||||
// <<< Explicitly trigger provider loading every time we enter add mode >>>
|
||||
// Ensure context is available and mounted before reading bloc
|
||||
if (mounted) {
|
||||
context.read<AiConfigBloc>().add(LoadAvailableProviders());
|
||||
}
|
||||
setState(() {
|
||||
_configToEdit = null; // Clear any previous edit state
|
||||
_showAddEditForm = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _hideAddEditForm() {
|
||||
setState(() {
|
||||
// Optionally clear BLoC state related to model loading if needed
|
||||
// context.read<AiConfigBloc>().add(ClearProviderModels());
|
||||
_configToEdit = null;
|
||||
_showAddEditForm = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 新增方法:显示编辑表单
|
||||
void _showEditForm(UserAIModelConfigModel config) {
|
||||
// 检查Bloc是否已有该Provider的模型,若无则加载
|
||||
if (mounted) {
|
||||
final bloc = context.read<AiConfigBloc>();
|
||||
final cachedGroup = bloc.state.modelGroups[config.provider];
|
||||
final hasCache = cachedGroup != null && cachedGroup.allModelsInfo.isNotEmpty;
|
||||
if (!hasCache) {
|
||||
bloc.add(LoadModelsForProvider(provider: config.provider));
|
||||
} else {
|
||||
AppLogger.d('SettingsPanel', '编辑模式使用缓存的模型列表,provider=${config.provider}');
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_configToEdit = config; // 设置要编辑的配置
|
||||
_showAddEditForm = true; // 显示表单
|
||||
_selectedIndex = 0; // 确保在 '模型服务' 类别下
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Material(
|
||||
elevation: 4.0,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
color: Colors.transparent, // Make Material transparent
|
||||
child: Container(
|
||||
width: 1440, // 增加宽度从800到960
|
||||
height: 1080, // 增加高度从600到700
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? theme.colorScheme.surface.withAlpha(217) // 0.85 opacity
|
||||
: theme.colorScheme.surface.withAlpha(242), // 0.95 opacity
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark
|
||||
? Colors.black.withAlpha(77) // 0.3 opacity
|
||||
: Colors.black.withAlpha(26), // 0.1 opacity
|
||||
blurRadius: 20,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: isDark
|
||||
? Colors.white.withAlpha(26) // 0.1 opacity
|
||||
: Colors.white.withAlpha(153), // 0.6 opacity
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
// 添加背景模糊效果
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Navigation Rail
|
||||
Container(
|
||||
width: 200,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? theme.colorScheme.surfaceContainerHighest.withAlpha(51) // 0.2 opacity
|
||||
: theme.colorScheme.surfaceContainerLowest.withAlpha(179), // 0.7 opacity
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
bottomLeft: Radius.circular(16.0),
|
||||
),
|
||||
border: Border.all(
|
||||
color: isDark
|
||||
? Colors.white.withAlpha(13) // 0.05 opacity
|
||||
: Colors.white.withAlpha(77), // 0.3 opacity
|
||||
width: 0.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark
|
||||
? Colors.black.withAlpha(51) // 0.2 opacity
|
||||
: Colors.black.withAlpha(13), // 0.05 opacity
|
||||
blurRadius: 10,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemCount: _categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = _categories[index];
|
||||
final isSelected = _selectedIndex == index;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? (isDark
|
||||
? theme.colorScheme.primary.withAlpha(38) // 0.15 opacity
|
||||
: theme.colorScheme.primary.withAlpha(26)) // 0.1 opacity
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: isSelected ? [
|
||||
BoxShadow(
|
||||
color: theme.colorScheme.primary.withAlpha(26), // 0.1 opacity
|
||||
blurRadius: 8,
|
||||
spreadRadius: 0,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
] : [],
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
category['icon'] as IconData?,
|
||||
color: isSelected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
size: 20, // Smaller icon
|
||||
),
|
||||
title: Text(
|
||||
category['title'] as String,
|
||||
style: TextStyle(
|
||||
fontSize: 13, // Slightly smaller font
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
_hideAddEditForm(); // Hide form when changing category
|
||||
});
|
||||
},
|
||||
selected: isSelected,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0, vertical: 4.0),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Right Content Area
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
// Clip content to rounded corners
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(16.0),
|
||||
bottomRight: Radius.circular(16.0),
|
||||
),
|
||||
child: Container(
|
||||
// Add a background for the content area if needed
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? theme.cardColor.withAlpha(179) // 0.7 opacity
|
||||
: theme.cardColor.withAlpha(217), // 0.85 opacity
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark
|
||||
? Colors.black.withAlpha(51) // 0.2 opacity
|
||||
: Colors.black.withAlpha(13), // 0.05 opacity
|
||||
blurRadius: 10,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Listener for Feedback Toasts
|
||||
BlocListener<AiConfigBloc, AiConfigState>(
|
||||
listener: (context, state) {
|
||||
if (!mounted) return;
|
||||
|
||||
if (state.actionStatus == AiConfigActionStatus.error ||
|
||||
state.actionStatus == AiConfigActionStatus.success) {
|
||||
widget.stateManager.setModelOperationInProgress(false);
|
||||
}
|
||||
|
||||
// Show Toast for errors
|
||||
if (state.actionStatus ==
|
||||
AiConfigActionStatus.error &&
|
||||
state.actionErrorMessage != null) {
|
||||
TopToast.error(context, '操作失败: ${state.actionErrorMessage!}');
|
||||
}
|
||||
// Show Toast for success
|
||||
else if (state.actionStatus ==
|
||||
AiConfigActionStatus.success) {
|
||||
TopToast.success(context, '操作成功');
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(32.0, 48.0, 32.0, 32.0),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
switchInCurve: Curves.easeOutQuint,
|
||||
switchOutCurve: Curves.easeInQuint,
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
// Using Key on the child ensures AnimatedSwitcher differentiates them
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.05, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
)
|
||||
);
|
||||
},
|
||||
// Directly determine the child and its key here
|
||||
child: _showAddEditForm &&
|
||||
_selectedIndex ==
|
||||
0 // Only show form for '模型服务'
|
||||
? _buildAiConfigForm(
|
||||
key: ValueKey(_configToEdit?.id ??
|
||||
'add')) // Form View
|
||||
: _buildCategoryListContent(
|
||||
key: ValueKey('list_$_selectedIndex'),
|
||||
index:
|
||||
_selectedIndex), // List View or other categories
|
||||
),
|
||||
),
|
||||
),
|
||||
// Close Button - Positioned relative to the Stack
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? Colors.black.withAlpha(51) // 0.2 opacity
|
||||
: Colors.white.withAlpha(128), // 0.5 opacity
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(26), // 0.1 opacity
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: '关闭设置',
|
||||
onPressed: widget.onClose,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Renamed for clarity and added index parameter
|
||||
Widget _buildCategoryListContent({required Key key, required int index}) {
|
||||
final categoryTitle = _categories[index]['title'] as String;
|
||||
|
||||
switch (categoryTitle) {
|
||||
case '模型服务':
|
||||
return ModelServiceListPage(
|
||||
key: key,
|
||||
userId: widget.userId,
|
||||
onAddNew: _showAddForm,
|
||||
onEditConfig: _showEditForm, // 传递编辑回调
|
||||
editorStateManager: widget.stateManager,
|
||||
);
|
||||
case '账户管理':
|
||||
return AccountManagementPanel(key: key);
|
||||
case '会员与订阅':
|
||||
return SizedBox(
|
||||
key: const ValueKey('membership_panel'),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
width: 820,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('会员计划', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 12),
|
||||
membership.MembershipPanel(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
case '编辑器设置':
|
||||
return EditorSettingsPanel(
|
||||
key: key,
|
||||
settings: _editorSettings,
|
||||
onSettingsChanged: (newSettings) {
|
||||
setState(() {
|
||||
_editorSettings = newSettings;
|
||||
});
|
||||
widget.onEditorSettingsChanged?.call(newSettings);
|
||||
},
|
||||
onSave: () async {
|
||||
// 🚀 修复:实际调用后端API保存编辑器设置
|
||||
try {
|
||||
AppLogger.i('SettingsPanel', '开始保存用户编辑器设置: userId=${widget.userId}');
|
||||
|
||||
final savedSettings = await _novelRepository.saveUserEditorSettings(
|
||||
widget.userId,
|
||||
_editorSettings
|
||||
);
|
||||
|
||||
AppLogger.i('SettingsPanel', '成功保存用户编辑器设置');
|
||||
|
||||
// 更新本地状态
|
||||
setState(() {
|
||||
_editorSettings = savedSettings;
|
||||
});
|
||||
|
||||
// 通知父组件
|
||||
widget.onEditorSettingsChanged?.call(savedSettings);
|
||||
|
||||
} catch (e) {
|
||||
AppLogger.e('SettingsPanel', '保存用户编辑器设置失败: $e');
|
||||
|
||||
// 显示错误提示
|
||||
if (mounted) {
|
||||
TopToast.error(context, '保存编辑器设置失败: $e');
|
||||
}
|
||||
|
||||
// 重新抛出异常,让EditorSettingsPanel的错误处理机制处理
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
onReset: () async {
|
||||
// 🚀 修复:实际调用后端API重置编辑器设置
|
||||
try {
|
||||
AppLogger.i('SettingsPanel', '开始重置用户编辑器设置: userId=${widget.userId}');
|
||||
|
||||
final defaultSettings = await _novelRepository.resetUserEditorSettings(widget.userId);
|
||||
|
||||
AppLogger.i('SettingsPanel', '成功重置用户编辑器设置');
|
||||
|
||||
setState(() {
|
||||
_editorSettings = defaultSettings;
|
||||
});
|
||||
|
||||
widget.onEditorSettingsChanged?.call(defaultSettings);
|
||||
|
||||
} catch (e) {
|
||||
AppLogger.e('SettingsPanel', '重置用户编辑器设置失败: $e');
|
||||
|
||||
// 显示错误提示
|
||||
if (mounted) {
|
||||
TopToast.error(context, '重置编辑器设置失败: $e');
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
case '主题设置':
|
||||
return _ThemeSettingsPage(
|
||||
key: key,
|
||||
currentVariant: _editorSettings.themeVariant,
|
||||
onChanged: (variant) {
|
||||
// 更新本地 EditorSettings 并立即应用
|
||||
setState(() {
|
||||
_editorSettings = _editorSettings.copyWith(themeVariant: variant);
|
||||
});
|
||||
WebTheme.applyVariant(variant);
|
||||
// 同步给外层
|
||||
widget.onEditorSettingsChanged?.call(_editorSettings);
|
||||
},
|
||||
onSave: () async {
|
||||
try {
|
||||
AppLogger.i('SettingsPanel', '保存主题设置: ${_editorSettings.themeVariant}');
|
||||
final saved = await _novelRepository.saveUserEditorSettings(
|
||||
widget.userId,
|
||||
_editorSettings,
|
||||
);
|
||||
setState(() {
|
||||
_editorSettings = saved;
|
||||
});
|
||||
// 关键:以服务端返回为准重新应用,避免非法/回退
|
||||
WebTheme.applyVariant(saved.themeVariant);
|
||||
widget.onEditorSettingsChanged?.call(saved);
|
||||
TopToast.success(context, '主题设置已保存');
|
||||
} catch (e) {
|
||||
TopToast.error(context, '保存主题设置失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
onReset: () async {
|
||||
try {
|
||||
AppLogger.i('SettingsPanel', '重置主题设置');
|
||||
final defaults = await _novelRepository.resetUserEditorSettings(widget.userId);
|
||||
setState(() {
|
||||
_editorSettings = defaults;
|
||||
});
|
||||
WebTheme.applyVariant(_editorSettings.themeVariant);
|
||||
widget.onEditorSettingsChanged?.call(defaults);
|
||||
} catch (e) {
|
||||
TopToast.error(context, '重置主题设置失败: $e');
|
||||
}
|
||||
},
|
||||
);
|
||||
default:
|
||||
return Center(
|
||||
key: key,
|
||||
child: Text('这里将显示 $categoryTitle 设置',
|
||||
style: Theme.of(context).textTheme.bodyLarge));
|
||||
}
|
||||
}
|
||||
|
||||
// Builds the actual form widget, added key parameter
|
||||
Widget _buildAiConfigForm({required Key key}) {
|
||||
// REMOVE the BlocListener that was here, as it might prematurely hide the form.
|
||||
// Success/failure should be handled internally by AiConfigForm or via callbacks if needed.
|
||||
return AiConfigForm(
|
||||
// The actual form content
|
||||
key: key, // Pass the key provided by the parent
|
||||
userId: widget.userId,
|
||||
configToEdit: _configToEdit, // Pass the current configToEdit state
|
||||
onCancel: _hideAddEditForm, // Use the hide function for cancel
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// 主题设置页(简洁 UI)
|
||||
class _ThemeSettingsPage extends StatelessWidget {
|
||||
const _ThemeSettingsPage({
|
||||
super.key,
|
||||
required this.currentVariant,
|
||||
required this.onChanged,
|
||||
required this.onSave,
|
||||
required this.onReset,
|
||||
});
|
||||
|
||||
final String currentVariant;
|
||||
final ValueChanged<String> onChanged;
|
||||
final Future<void> Function() onSave;
|
||||
final Future<void> Function() onReset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final options = const [
|
||||
{'key': WebTheme.variantMonochrome, 'label': '黑白(默认)'},
|
||||
{'key': WebTheme.variantBlueWhite, 'label': '蓝白'},
|
||||
{'key': WebTheme.variantPinkWhite, 'label': '粉白'},
|
||||
{'key': WebTheme.variantPaper, 'label': '书页米色'},
|
||||
];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('主题设置', style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
for (final opt in options)
|
||||
ChoiceChip(
|
||||
label: Text(opt['label'] as String),
|
||||
selected: currentVariant == (opt['key'] as String),
|
||||
onSelected: (_) => onChanged(opt['key'] as String),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: onSave,
|
||||
icon: const Icon(Icons.save_outlined),
|
||||
label: const Text('保存'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: onReset,
|
||||
icon: const Icon(Icons.refresh_outlined),
|
||||
label: const Text('重置为默认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,773 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/config/app_config.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
import 'package:ainoval/widgets/forms/change_password_form.dart';
|
||||
import 'package:ainoval/widgets/common/top_toast.dart';
|
||||
import 'package:ainoval/services/auth_service.dart';
|
||||
|
||||
/// 账户管理面板
|
||||
/// 集成在设置面板中的账户相关功能
|
||||
class AccountManagementPanel extends StatefulWidget {
|
||||
const AccountManagementPanel({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AccountManagementPanel> createState() => _AccountManagementPanelState();
|
||||
}
|
||||
|
||||
class _AccountManagementPanelState extends State<AccountManagementPanel> {
|
||||
int _selectedTabIndex = 0;
|
||||
Map<String, dynamic>? _userInfo;
|
||||
bool _isLoadingUserInfo = false;
|
||||
bool _isEditingPersonalInfo = false;
|
||||
bool _isSavingPersonalInfo = false;
|
||||
|
||||
final GlobalKey<FormState> _personalInfoFormKey = GlobalKey<FormState>();
|
||||
final TextEditingController _displayNameController = TextEditingController();
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
|
||||
final List<String> _tabs = ['个人信息', '修改密码', '安全设置'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserInfo();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_displayNameController.dispose();
|
||||
_emailController.dispose();
|
||||
_phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 加载用户信息
|
||||
Future<void> _loadUserInfo() async {
|
||||
setState(() {
|
||||
_isLoadingUserInfo = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final authService = AuthService();
|
||||
// 确保初始化以加载本地存储中的登录状态
|
||||
await authService.init();
|
||||
final userInfo = await authService.getCurrentUser();
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_userInfo = userInfo;
|
||||
_isLoadingUserInfo = false;
|
||||
});
|
||||
_populateControllersFromUserInfo(userInfo);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingUserInfo = false;
|
||||
});
|
||||
TopToast.error(context, '加载用户信息失败:${e.toString().replaceAll('AuthException: ', '')}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _populateControllersFromUserInfo(Map<String, dynamic> info) {
|
||||
try {
|
||||
_displayNameController.text = (info['displayName'] ?? '').toString();
|
||||
_emailController.text = (info['email'] ?? '').toString();
|
||||
_phoneController.text = (info['phone'] ?? '').toString();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _toggleEditing() {
|
||||
setState(() {
|
||||
_isEditingPersonalInfo = !_isEditingPersonalInfo;
|
||||
if (_isEditingPersonalInfo && _userInfo != null) {
|
||||
_populateControllersFromUserInfo(_userInfo!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _savePersonalInfo() async {
|
||||
if (!_isEditingPersonalInfo) return;
|
||||
final form = _personalInfoFormKey.currentState;
|
||||
if (form == null || !form.validate()) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isSavingPersonalInfo = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final authService = AuthService();
|
||||
await authService.init();
|
||||
final updated = await authService.updateUserProfile({
|
||||
'displayName': _displayNameController.text.trim(),
|
||||
'email': _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
|
||||
'phone': _phoneController.text.trim().isEmpty ? null : _phoneController.text.trim(),
|
||||
});
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_userInfo = updated;
|
||||
_isEditingPersonalInfo = false;
|
||||
_isSavingPersonalInfo = false;
|
||||
});
|
||||
TopToast.success(context, '个人信息已保存');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSavingPersonalInfo = false;
|
||||
});
|
||||
TopToast.error(context, e.toString().replaceAll('AuthException: ', '保存失败:'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题
|
||||
Text(
|
||||
'账户管理',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 用户信息概览卡片
|
||||
_buildUserOverviewCard(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Tab导航
|
||||
_buildTabNavigation(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tab内容
|
||||
Expanded(
|
||||
child: _buildTabContent(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建用户概览卡片
|
||||
Widget _buildUserOverviewCard() {
|
||||
final username = AppConfig.username ?? '游客';
|
||||
final userId = AppConfig.userId ?? '未知';
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: WebTheme.getShadowColor(context, opacity: 0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// 头像
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: WebTheme.getPrimaryColor(context).withOpacity(0.1),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 25,
|
||||
color: WebTheme.getPrimaryColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 用户信息
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
username,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'ID: $userId',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
if (_userInfo != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'积分: ${_userInfo!['credits'] ?? 0}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: WebTheme.getPrimaryColor(context),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
// 刷新按钮
|
||||
IconButton(
|
||||
onPressed: _isLoadingUserInfo ? null : _loadUserInfo,
|
||||
icon: _isLoadingUserInfo
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.refresh,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
tooltip: '刷新用户信息',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建Tab导航
|
||||
Widget _buildTabNavigation() {
|
||||
return Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: WebTheme.getBorderColor(context),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: _tabs.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final title = entry.value;
|
||||
final isSelected = _selectedTabIndex == index;
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedTabIndex = index;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? WebTheme.getPrimaryColor(context).withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isSelected
|
||||
? WebTheme.getPrimaryColor(context)
|
||||
: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建Tab内容
|
||||
Widget _buildTabContent() {
|
||||
switch (_selectedTabIndex) {
|
||||
case 0:
|
||||
return _buildPersonalInfoTab();
|
||||
case 1:
|
||||
return _buildChangePasswordTab();
|
||||
case 2:
|
||||
return _buildSecuritySettingsTab();
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
/// 个人信息Tab
|
||||
Widget _buildPersonalInfoTab() {
|
||||
return SingleChildScrollView(
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
shadowColor: WebTheme.getShadowColor(context, opacity: 0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_outline,
|
||||
color: WebTheme.getPrimaryColor(context),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'个人信息',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_userInfo != null && !_isLoadingUserInfo && !_isEditingPersonalInfo)
|
||||
OutlinedButton.icon(
|
||||
onPressed: _toggleEditing,
|
||||
icon: const Icon(Icons.edit, size: 16),
|
||||
label: const Text('编辑'),
|
||||
),
|
||||
if (_isEditingPersonalInfo) ...[
|
||||
TextButton(
|
||||
onPressed: _isSavingPersonalInfo ? null : _toggleEditing,
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isSavingPersonalInfo ? null : _savePersonalInfo,
|
||||
icon: _isSavingPersonalInfo
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.save, size: 16),
|
||||
label: const Text('保存'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
if (_isLoadingUserInfo)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (_userInfo != null && !_isEditingPersonalInfo) ...[
|
||||
_buildInfoField('用户名', AppConfig.username ?? '未知'),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoField('显示名称', (_userInfo!['displayName'] ?? '未设置').toString()),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoField('邮箱', (_userInfo!['email'] ?? '未设置').toString()),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoField('手机号', (_userInfo!['phone'] ?? '未设置').toString()),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoField('注册时间', _formatDateTime(_userInfo!['createdAt'])),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoField('最后登录', _formatDateTime(_userInfo!['lastLoginAt'])),
|
||||
] else if (_userInfo != null && _isEditingPersonalInfo) ...[
|
||||
Form(
|
||||
key: _personalInfoFormKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildEditableTextField(
|
||||
label: '显示名称',
|
||||
controller: _displayNameController,
|
||||
hintText: '请输入显示名称',
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) {
|
||||
return '显示名称不能为空';
|
||||
}
|
||||
if (v.trim().length > 32) {
|
||||
return '显示名称过长(最多32个字符)';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildEditableTextField(
|
||||
label: '邮箱',
|
||||
controller: _emailController,
|
||||
hintText: '请输入邮箱(可留空)',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (v) {
|
||||
final value = (v ?? '').trim();
|
||||
if (value.isEmpty) return null; // 允许空
|
||||
final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
|
||||
if (!emailRegex.hasMatch(value)) {
|
||||
return '邮箱格式不正确';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildEditableTextField(
|
||||
label: '手机号',
|
||||
controller: _phoneController,
|
||||
hintText: '请输入手机号(可留空)',
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (v) {
|
||||
final value = (v ?? '').trim();
|
||||
if (value.isEmpty) return null; // 允许空
|
||||
final phoneRegex = RegExp(r'^[0-9+\-\s]{6,20}$');
|
||||
if (!phoneRegex.hasMatch(value)) {
|
||||
return '手机号格式不正确';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'无法加载用户信息',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _loadUserInfo,
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 修改密码Tab
|
||||
Widget _buildChangePasswordTab() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: WebTheme.getShadowColor(context, opacity: 0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
child: ChangePasswordForm(
|
||||
showTitle: false,
|
||||
onSuccess: () {
|
||||
TopToast.success(context, '密码修改成功');
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 安全设置Tab
|
||||
Widget _buildSecuritySettingsTab() {
|
||||
return SingleChildScrollView(
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
shadowColor: WebTheme.getShadowColor(context, opacity: 0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.security,
|
||||
color: WebTheme.getPrimaryColor(context),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'安全设置',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildSecurityItem(
|
||||
icon: Icons.device_unknown,
|
||||
title: '登录设备管理',
|
||||
subtitle: '查看和管理登录设备',
|
||||
onTap: () {
|
||||
TopToast.info(context, '登录设备管理功能开发中');
|
||||
},
|
||||
),
|
||||
const Divider(height: 32),
|
||||
_buildSecurityItem(
|
||||
icon: Icons.history,
|
||||
title: '登录历史',
|
||||
subtitle: '查看最近的登录记录',
|
||||
onTap: () {
|
||||
TopToast.info(context, '登录历史功能开发中');
|
||||
},
|
||||
),
|
||||
const Divider(height: 32),
|
||||
_buildSecurityItem(
|
||||
icon: Icons.key,
|
||||
title: 'API密钥管理',
|
||||
subtitle: '管理第三方API访问密钥',
|
||||
onTap: () {
|
||||
TopToast.info(context, 'API密钥管理功能开发中');
|
||||
},
|
||||
),
|
||||
const Divider(height: 32),
|
||||
_buildSecurityItem(
|
||||
icon: Icons.privacy_tip,
|
||||
title: '隐私设置',
|
||||
subtitle: '管理数据使用和隐私偏好',
|
||||
onTap: () {
|
||||
TopToast.info(context, '隐私设置功能开发中');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建信息字段
|
||||
Widget _buildInfoField(String label, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getBackgroundColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: WebTheme.getBorderColor(context),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建可编辑文本字段
|
||||
Widget _buildEditableTextField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
String? hintText,
|
||||
TextInputType? keyboardType,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
filled: true,
|
||||
fillColor: WebTheme.getBackgroundColor(context),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: WebTheme.getBorderColor(context)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: WebTheme.getPrimaryColor(context), width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建安全设置项
|
||||
Widget _buildSecurityItem({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: WebTheme.getPrimaryColor(context).withOpacity(0.1),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: WebTheme.getPrimaryColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化日期时间(兼容多种后端返回格式)
|
||||
String _formatDateTime(dynamic value) {
|
||||
if (value == null) return '未知';
|
||||
try {
|
||||
DateTime dateTime;
|
||||
if (value is String) {
|
||||
dateTime = DateTime.parse(value);
|
||||
} else if (value is int) {
|
||||
// 兼容时间戳(秒/毫秒)
|
||||
if (value > 1000000000000) {
|
||||
dateTime = DateTime.fromMillisecondsSinceEpoch(value);
|
||||
} else if (value > 1000000000) {
|
||||
dateTime = DateTime.fromMillisecondsSinceEpoch(value * 1000);
|
||||
} else {
|
||||
return '未知';
|
||||
}
|
||||
} else if (value is List) {
|
||||
// 兼容 [year, month, day, hour?, minute?, second?]
|
||||
final year = _toInt(value, 0);
|
||||
final month = _toInt(value, 1);
|
||||
final day = _toInt(value, 2);
|
||||
final hour = _toInt(value, 3) ?? 0;
|
||||
final minute = _toInt(value, 4) ?? 0;
|
||||
final second = _toInt(value, 5) ?? 0;
|
||||
if (year != null && month != null && day != null) {
|
||||
dateTime = DateTime(year, month, day, hour, minute, second);
|
||||
} else {
|
||||
return '未知';
|
||||
}
|
||||
} else if (value is Map && value.containsKey('\$date')) {
|
||||
final d = value['\$date'];
|
||||
if (d is String) {
|
||||
dateTime = DateTime.parse(d);
|
||||
} else if (d is int) {
|
||||
dateTime = DateTime.fromMillisecondsSinceEpoch(d);
|
||||
} else {
|
||||
return '未知';
|
||||
}
|
||||
} else {
|
||||
return '未知';
|
||||
}
|
||||
|
||||
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')}';
|
||||
} catch (_) {
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
int? _toInt(List<dynamic> list, int index) {
|
||||
if (index >= list.length) return null;
|
||||
final v = list[index];
|
||||
if (v is int) return v;
|
||||
if (v is String) return int.tryParse(v);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
444
AINoval/lib/screens/settings/widgets/add_user_preset_dialog.dart
Normal file
444
AINoval/lib/screens/settings/widgets/add_user_preset_dialog.dart
Normal file
@@ -0,0 +1,444 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/preset_models.dart';
|
||||
import '../../../models/ai_request_models.dart';
|
||||
import '../../../services/ai_preset_service.dart';
|
||||
import '../../../utils/logger.dart';
|
||||
import '../../../models/prompt_models.dart';
|
||||
|
||||
/// 添加用户预设对话框
|
||||
class AddUserPresetDialog extends StatefulWidget {
|
||||
final VoidCallback? onSuccess;
|
||||
|
||||
const AddUserPresetDialog({
|
||||
Key? key,
|
||||
this.onSuccess,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddUserPresetDialog> createState() => _AddUserPresetDialogState();
|
||||
}
|
||||
|
||||
class _AddUserPresetDialogState extends State<AddUserPresetDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _systemPromptController = TextEditingController();
|
||||
final _userPromptController = TextEditingController();
|
||||
final _tagsController = TextEditingController();
|
||||
|
||||
String _selectedFeatureType = 'CHAT';
|
||||
bool _addToFavorites = false;
|
||||
bool _isLoading = false;
|
||||
|
||||
final AIPresetService _presetService = AIPresetService();
|
||||
// 功能类型动态来源:AIFeatureTypeHelper.allFeatures
|
||||
|
||||
// 功能类型标签由 AIFeatureType.displayName 提供
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_systemPromptController.dispose();
|
||||
_userPromptController.dispose();
|
||||
_tagsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 700,
|
||||
constraints: const BoxConstraints(maxHeight: 800),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.smart_button, size: 24, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'新建预设',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildBasicInfoSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildPromptSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSettingsSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBasicInfoSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'基本信息',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '预设名称 *',
|
||||
hintText: '请输入预设名称',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入预设名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '预设描述',
|
||||
hintText: '请简要描述此预设的用途和特点',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedFeatureType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '适用功能 *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: AIFeatureType.values.map((t) {
|
||||
final api = t.toApiString();
|
||||
return DropdownMenuItem(
|
||||
value: api,
|
||||
child: Text(t.displayName),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedFeatureType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _tagsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '标签',
|
||||
hintText: '请输入标签,用逗号分隔,如:创意写作,角色对话',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPromptSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'提示词配置',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: _showPromptHelper,
|
||||
icon: const Icon(Icons.help_outline, size: 16),
|
||||
label: const Text('写作技巧'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _systemPromptController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '系统提示词 *',
|
||||
hintText: '定义AI的角色和行为规则...',
|
||||
border: OutlineInputBorder(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
maxLines: 6,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入系统提示词';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _userPromptController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '用户提示词',
|
||||
hintText: '可选:为用户输入提供默认格式或示例...',
|
||||
border: OutlineInputBorder(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
maxLines: 4,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.lightbulb_outline, size: 16, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'提示词写作要点',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'• 系统提示词:定义AI的角色、专业领域和回答风格\n'
|
||||
'• 用户提示词:为用户提供输入的格式指导或示例\n'
|
||||
'• 使用清晰具体的描述,避免模糊的指令\n'
|
||||
'• 可以包含期望的输出格式和长度要求',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'其他设置',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
CheckboxListTile(
|
||||
title: const Text('添加到我的收藏'),
|
||||
subtitle: const Text('创建后自动添加到收藏夹'),
|
||||
value: _addToFavorites,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_addToFavorites = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _createPreset,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('创建预设'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showPromptHelper() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('提示词写作技巧'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildPromptTip('系统提示词示例', [
|
||||
'你是一个专业的小说编辑,擅长分析文学作品的情节结构和人物塑造。',
|
||||
'你是一位创意写作导师,能够提供具体而实用的写作建议。',
|
||||
'请以专业、友好的语气回答,并提供具体的例子和建议。',
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildPromptTip('用户提示词示例', [
|
||||
'请分析以下文本的:\n1. 主要角色特点\n2. 情节发展\n3. 写作技巧',
|
||||
'文本内容:[在这里粘贴要分析的文本]',
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildPromptTip('写作建议', [
|
||||
'• 明确定义AI的角色和专业领域',
|
||||
'• 指定期望的回答风格(正式/友好/专业等)',
|
||||
'• 提供具体的任务描述',
|
||||
'• 如果需要,指定输出格式',
|
||||
'• 使用具体而非抽象的描述',
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('知道了'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPromptTip(String title, List<String> items) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: items.map((item) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
item,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
)).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createPreset() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final tags = _tagsController.text
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.where((tag) => tag.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
final request = UniversalAIRequest(
|
||||
requestType: AIRequestType.chat,
|
||||
userId: '',
|
||||
instructions: _systemPromptController.text.trim(),
|
||||
prompt: _userPromptController.text.trim().isEmpty ? null : _userPromptController.text.trim(),
|
||||
);
|
||||
|
||||
final created = await _presetService.createPreset(
|
||||
CreatePresetRequest(
|
||||
presetName: _nameController.text.trim(),
|
||||
presetDescription: _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(),
|
||||
presetTags: tags.isEmpty ? null : tags,
|
||||
request: request,
|
||||
),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('预设 "${created.presetName ?? '已创建'}" 创建成功')),
|
||||
);
|
||||
widget.onSuccess?.call();
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('AddUserPresetDialog', '创建预设失败', e);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('创建失败: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 旧的图标/颜色映射方法已不再使用,移除以清理警告
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/prompt_models.dart';
|
||||
import '../../../services/api_service/repositories/impl/prompt_repository_impl.dart';
|
||||
import '../../../services/api_service/base/api_client.dart';
|
||||
import '../../../models/prompt_models.dart' show AIFeatureTypeHelper;
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../../utils/logger.dart';
|
||||
|
||||
/// 添加用户模板对话框
|
||||
class AddUserTemplateDialog extends StatefulWidget {
|
||||
final VoidCallback? onSuccess;
|
||||
|
||||
const AddUserTemplateDialog({
|
||||
Key? key,
|
||||
this.onSuccess,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddUserTemplateDialog> createState() => _AddUserTemplateDialogState();
|
||||
}
|
||||
|
||||
class _AddUserTemplateDialogState extends State<AddUserTemplateDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _templateContentController = TextEditingController();
|
||||
final _versionController = TextEditingController(text: '1.0.0');
|
||||
final _tagsController = TextEditingController();
|
||||
|
||||
String _selectedFeatureType = 'CHAT';
|
||||
bool _isPrivate = true;
|
||||
bool _addToFavorites = false;
|
||||
bool _isLoading = false;
|
||||
|
||||
final PromptRepositoryImpl _promptRepository = PromptRepositoryImpl(ApiClient());
|
||||
// 功能类型动态来源:AIFeatureTypeHelper.allFeatures
|
||||
|
||||
static const Map<String, String> _featureTypeLabels = {
|
||||
'CHAT': 'AI聊天',
|
||||
'SCENE_GENERATION': '场景生成',
|
||||
'CONTINUATION': '续写',
|
||||
'SUMMARY': '总结',
|
||||
'OUTLINE': '大纲',
|
||||
};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_templateContentController.dispose();
|
||||
_versionController.dispose();
|
||||
_tagsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 700,
|
||||
constraints: const BoxConstraints(maxHeight: 800),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.add_circle, size: 24, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'新建模板',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildBasicInfoSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildTemplateContentSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSettingsSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBasicInfoSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'基本信息',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '模板名称 *',
|
||||
hintText: '请输入模板名称',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入模板名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: TextFormField(
|
||||
controller: _versionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '版本号',
|
||||
hintText: '1.0.0',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '模板描述',
|
||||
hintText: '请简要描述此模板的用途和特点',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _selectedFeatureType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '适用功能 *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: AIFeatureTypeHelper.allFeatures.map((t) {
|
||||
final api = t.toApiString();
|
||||
return DropdownMenuItem(
|
||||
value: api,
|
||||
child: Text(t.displayName),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedFeatureType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _tagsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '标签',
|
||||
hintText: '请输入标签,用逗号分隔,如:创意写作,角色对话',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTemplateContentSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'模板内容',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: _showVariableHelper,
|
||||
icon: const Icon(Icons.help_outline, size: 16),
|
||||
label: const Text('变量使用帮助'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _templateContentController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '模板内容 *',
|
||||
hintText: '请输入模板内容,可以使用 {{变量名}} 作为占位符\n\n示例:\n你是一个专业的{{角色}},请帮我{{任务描述}}。\n要求:\n1. {{要求1}}\n2. {{要求2}}',
|
||||
border: OutlineInputBorder(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
maxLines: 12,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入模板内容';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.lightbulb_outline, size: 16, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'使用提示',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'• 使用 {{变量名}} 创建可填写的占位符\n• 变量名应该简洁明了,如 {{角色}}、{{任务}}、{{风格}}\n• 用户使用时可以替换这些变量为具体内容',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'隐私设置',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
RadioListTile<bool>(
|
||||
title: const Text('私有模板'),
|
||||
subtitle: const Text('仅自己可见和使用'),
|
||||
value: true,
|
||||
groupValue: _isPrivate,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isPrivate = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
RadioListTile<bool>(
|
||||
title: const Text('公开模板'),
|
||||
subtitle: const Text('分享到社区,其他用户也可以使用'),
|
||||
value: false,
|
||||
groupValue: _isPrivate,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isPrivate = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
CheckboxListTile(
|
||||
title: const Text('添加到我的收藏'),
|
||||
subtitle: const Text('创建后自动添加到收藏夹'),
|
||||
value: _addToFavorites,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_addToFavorites = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _createTemplate,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('创建模板'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showVariableHelper() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('变量使用帮助'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'变量语法:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: Colors.grey[100],
|
||||
child: const Text(
|
||||
'{{变量名}}',
|
||||
style: TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const Text(
|
||||
'常用变量示例:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...const [
|
||||
'{{角色}} - 如:专业编剧、资深编辑',
|
||||
'{{任务}} - 如:写一个故事、分析文本',
|
||||
'{{风格}} - 如:正式、幽默、诗意',
|
||||
'{{主题}} - 如:科幻、爱情、悬疑',
|
||||
'{{长度}} - 如:500字、简短、详细',
|
||||
'{{语言}} - 如:中文、英文、双语',
|
||||
].map((example) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 4),
|
||||
child: Text('• $example'),
|
||||
)),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'使用建议:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'• 变量名要简洁明了\n'
|
||||
'• 避免使用特殊字符\n'
|
||||
'• 可以使用中文变量名\n'
|
||||
'• 合理组织变量顺序',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('知道了'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createTemplate() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final feature = AIFeatureTypeHelper.fromApiString(_selectedFeatureType.toUpperCase());
|
||||
final tags = _tagsController.text
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.where((tag) => tag.isNotEmpty)
|
||||
.toList();
|
||||
await _promptRepository.createPromptTemplate(
|
||||
name: _nameController.text.trim(),
|
||||
content: _templateContentController.text.trim(),
|
||||
featureType: feature,
|
||||
authorId: (AppConfig.userId ?? '').toString(),
|
||||
description: _descriptionController.text.trim().isEmpty
|
||||
? null
|
||||
: _descriptionController.text.trim(),
|
||||
tags: tags.isEmpty ? null : tags,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('模板创建成功')),
|
||||
);
|
||||
widget.onSuccess?.call();
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.e('AddUserTemplateDialog', '创建模板失败', e);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('创建失败: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
173
AINoval/lib/screens/settings/widgets/ai_assist_toolbar.dart
Normal file
173
AINoval/lib/screens/settings/widgets/ai_assist_toolbar.dart
Normal file
@@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/models/prompt_models.dart';
|
||||
|
||||
/// AI辅助工具栏组件
|
||||
class AIAssistToolbar extends StatelessWidget {
|
||||
/// 是否正在处理中
|
||||
final bool isProcessing;
|
||||
|
||||
/// 当前选择的优化风格
|
||||
final OptimizationStyle selectedStyle;
|
||||
|
||||
/// 风格变更回调
|
||||
final Function(OptimizationStyle) onStyleChanged;
|
||||
|
||||
/// 当前保留比例 (0.0-1.0)
|
||||
final double preserveRatio;
|
||||
|
||||
/// 保留比例变更回调
|
||||
final Function(double) onRatioChanged;
|
||||
|
||||
/// 点击优化按钮的回调
|
||||
final VoidCallback onOptimizeRequested;
|
||||
|
||||
const AIAssistToolbar({
|
||||
Key? key,
|
||||
this.isProcessing = false,
|
||||
required this.selectedStyle,
|
||||
required this.onStyleChanged,
|
||||
required this.preserveRatio,
|
||||
required this.onRatioChanged,
|
||||
required this.onOptimizeRequested,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isLight = theme.brightness == Brightness.light;
|
||||
final foregroundOnDark = Colors.white;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
// 浅色主题下,工具栏使用黑色背景、白色文字
|
||||
color: isLight
|
||||
? Colors.black
|
||||
: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isLight
|
||||
? Colors.white.withOpacity(0.2)
|
||||
: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_awesome,
|
||||
size: 18,
|
||||
color: isLight ? foregroundOnDark : Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'AI 辅助优化',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isLight ? foregroundOnDark : Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 优化风格选择和保留比例设置
|
||||
Row(
|
||||
children: [
|
||||
// 优化风格选择
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'优化风格:',
|
||||
style: TextStyle(color: isLight ? foregroundOnDark : null),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildStyleSelector(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
|
||||
// 保留比例设置
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'保留原始内容: ${(preserveRatio * 100).toInt()}%',
|
||||
style: TextStyle(color: isLight ? foregroundOnDark : null),
|
||||
),
|
||||
Slider(
|
||||
value: preserveRatio,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
divisions: 10,
|
||||
label: '${(preserveRatio * 100).toInt()}%',
|
||||
onChanged: isProcessing ? null : onRatioChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 优化按钮
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton.icon(
|
||||
icon: isProcessing
|
||||
? Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.auto_fix_high, size: 16),
|
||||
label: Text(isProcessing ? '正在优化...' : 'AI优化'),
|
||||
onPressed: isProcessing ? null : onOptimizeRequested,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建风格选择器
|
||||
Widget _buildStyleSelector(BuildContext context) {
|
||||
return SegmentedButton<OptimizationStyle>(
|
||||
segments: [
|
||||
ButtonSegment<OptimizationStyle>(
|
||||
value: OptimizationStyle.professional,
|
||||
label: const Text('专业'),
|
||||
icon: const Icon(Icons.business),
|
||||
),
|
||||
ButtonSegment<OptimizationStyle>(
|
||||
value: OptimizationStyle.creative,
|
||||
label: const Text('创意'),
|
||||
icon: const Icon(Icons.lightbulb),
|
||||
),
|
||||
ButtonSegment<OptimizationStyle>(
|
||||
value: OptimizationStyle.concise,
|
||||
label: const Text('简洁'),
|
||||
icon: const Icon(Icons.short_text),
|
||||
),
|
||||
],
|
||||
selected: {selectedStyle},
|
||||
onSelectionChanged: isProcessing
|
||||
? null
|
||||
: (Set<OptimizationStyle> selection) {
|
||||
if (selection.isNotEmpty) {
|
||||
onStyleChanged(selection.first);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
1305
AINoval/lib/screens/settings/widgets/ai_config_form.dart
Normal file
1305
AINoval/lib/screens/settings/widgets/ai_config_form.dart
Normal file
File diff suppressed because it is too large
Load Diff
144
AINoval/lib/screens/settings/widgets/custom_model_dialog.dart
Normal file
144
AINoval/lib/screens/settings/widgets/custom_model_dialog.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 自定义模型输入对话框
|
||||
/// 允许用户手动输入不在预定义列表中的模型信息
|
||||
class CustomModelDialog extends StatefulWidget {
|
||||
/// 提供商名称
|
||||
final String providerName;
|
||||
|
||||
/// 确认添加回调
|
||||
final Function(String modelName, String modelAlias, String? apiEndpoint) onConfirm;
|
||||
|
||||
const CustomModelDialog({
|
||||
Key? key,
|
||||
required this.providerName,
|
||||
required this.onConfirm,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CustomModelDialog> createState() => _CustomModelDialogState();
|
||||
}
|
||||
|
||||
class _CustomModelDialogState extends State<CustomModelDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _modelNameController = TextEditingController();
|
||||
final _modelAliasController = TextEditingController();
|
||||
final _apiEndpointController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_modelNameController.dispose();
|
||||
_modelAliasController.dispose();
|
||||
_apiEndpointController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('添加自定义${widget.providerName}模型'),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'请输入您想添加的${widget.providerName}模型信息',
|
||||
style: TextStyle(fontSize: 13, color: theme.colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 模型名称输入框
|
||||
TextFormField(
|
||||
controller: _modelNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '模型名称 *',
|
||||
hintText: '例如: gpt-4-vision-preview',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入模型名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 模型别名输入框
|
||||
TextFormField(
|
||||
controller: _modelAliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '模型别名 *',
|
||||
hintText: '例如: GPT-4 Vision',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入模型别名';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// API 接口地址输入框
|
||||
TextFormField(
|
||||
controller: _apiEndpointController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'API 接口地址(可选)',
|
||||
hintText: '例如: https://api.openai.com/v1',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
// API Endpoint是可选的,不需要验证
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'* 表示必填字段',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.error,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
widget.onConfirm(
|
||||
_modelNameController.text.trim(),
|
||||
_modelAliasController.text.trim(),
|
||||
_apiEndpointController.text.trim().isEmpty ? null : _apiEndpointController.text.trim(),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: const Text('确认添加'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/preset_models.dart';
|
||||
import '../../../services/ai_preset_service.dart';
|
||||
import '../../../utils/logger.dart';
|
||||
import '../../../models/prompt_models.dart';
|
||||
|
||||
/// 编辑用户预设对话框
|
||||
class EditUserPresetDialog extends StatefulWidget {
|
||||
final AIPromptPreset preset;
|
||||
final VoidCallback? onSuccess;
|
||||
|
||||
const EditUserPresetDialog({
|
||||
Key? key,
|
||||
required this.preset,
|
||||
this.onSuccess,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<EditUserPresetDialog> createState() => _EditUserPresetDialogState();
|
||||
}
|
||||
|
||||
class _EditUserPresetDialogState extends State<EditUserPresetDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _descriptionController;
|
||||
late final TextEditingController _systemPromptController;
|
||||
late final TextEditingController _userPromptController;
|
||||
late final TextEditingController _tagsController;
|
||||
|
||||
late String _selectedFeatureType;
|
||||
late bool _isFavorite;
|
||||
bool _isLoading = false;
|
||||
|
||||
final AIPresetService _presetService = AIPresetService();
|
||||
// 功能类型动态来源:AIFeatureTypeHelper.allFeatures
|
||||
|
||||
// 功能类型标签由 AIFeatureType.displayName 提供
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeControllers();
|
||||
}
|
||||
|
||||
void _initializeControllers() {
|
||||
_nameController = TextEditingController(text: widget.preset.presetName ?? '');
|
||||
_descriptionController = TextEditingController(text: widget.preset.presetDescription ?? '');
|
||||
_systemPromptController = TextEditingController(text: widget.preset.systemPrompt);
|
||||
_userPromptController = TextEditingController(text: widget.preset.userPrompt);
|
||||
_tagsController = TextEditingController(
|
||||
text: widget.preset.presetTags?.join(', ') ?? '',
|
||||
);
|
||||
|
||||
final allApi = AIFeatureType.values.map((e) => e.toApiString()).toList();
|
||||
_selectedFeatureType = allApi.contains(widget.preset.aiFeatureType)
|
||||
? widget.preset.aiFeatureType
|
||||
: AIFeatureType.aiChat.toApiString();
|
||||
_isFavorite = widget.preset.isFavorite;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_systemPromptController.dispose();
|
||||
_userPromptController.dispose();
|
||||
_tagsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 700,
|
||||
constraints: const BoxConstraints(maxHeight: 800),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.edit, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'编辑预设',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildPresetInfo(),
|
||||
const SizedBox(height: 24),
|
||||
_buildBasicInfoSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildPromptSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSettingsSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPresetInfo() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'预设信息',
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildInfoItem('预设ID', widget.preset.presetId),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildInfoItem('使用次数', '${widget.preset.useCount}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildInfoItem('创建时间', _formatDateTime(widget.preset.createdAt) ?? ''),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildInfoItem('最后使用', _formatDateTime(widget.preset.lastUsedAt) ?? '从未使用'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem(String label, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBasicInfoSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'基本信息',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '预设名称 *',
|
||||
hintText: '请输入预设名称',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入预设名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '预设描述',
|
||||
hintText: '请简要描述此预设的用途和特点',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedFeatureType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '适用功能 *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: AIFeatureType.values.map((t) {
|
||||
final api = t.toApiString();
|
||||
return DropdownMenuItem(
|
||||
value: api,
|
||||
child: Text(t.displayName),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedFeatureType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _tagsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '标签',
|
||||
hintText: '请输入标签,用逗号分隔',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPromptSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'提示词配置',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: _showPromptHelper,
|
||||
icon: const Icon(Icons.help_outline, size: 16),
|
||||
label: const Text('写作技巧'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _systemPromptController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '系统提示词 *',
|
||||
hintText: '定义AI的角色和行为规则...',
|
||||
border: OutlineInputBorder(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
maxLines: 6,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入系统提示词';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _userPromptController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '用户提示词',
|
||||
hintText: '可选:为用户输入提供默认格式或示例...',
|
||||
border: OutlineInputBorder(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
maxLines: 4,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'设置选项',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
CheckboxListTile(
|
||||
title: const Text('添加到我的收藏'),
|
||||
subtitle: const Text('在收藏夹中显示此预设'),
|
||||
value: _isFavorite,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isFavorite = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _updatePreset,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('保存更改'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showPromptHelper() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('提示词写作技巧'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildPromptTip('优化建议', [
|
||||
'• 使用具体而非抽象的描述',
|
||||
'• 明确定义期望的输出格式',
|
||||
'• 提供具体的例子和情境',
|
||||
'• 避免过于复杂的指令',
|
||||
'• 根据功能类型调整提示词风格',
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildPromptTip('功能特定建议', [
|
||||
'聊天: 强调对话风格和个性',
|
||||
'场景生成: 注重描述细节和氛围',
|
||||
'续写: 保持风格一致性',
|
||||
'总结: 明确长度和要点',
|
||||
'大纲: 指定结构和层次',
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('知道了'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPromptTip(String title, List<String> items) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...items.map((item) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text('$item', style: const TextStyle(fontSize: 12)),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updatePreset() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final tags = _tagsController.text
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.where((tag) => tag.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
final updatedPreset = widget.preset.copyWith(
|
||||
presetName: _nameController.text.trim(),
|
||||
presetDescription: _descriptionController.text.trim().isEmpty
|
||||
? null : _descriptionController.text.trim(),
|
||||
aiFeatureType: _selectedFeatureType,
|
||||
systemPrompt: _systemPromptController.text.trim(),
|
||||
userPrompt: _userPromptController.text.trim().isEmpty
|
||||
? null : _userPromptController.text.trim(),
|
||||
presetTags: tags.isEmpty ? null : tags,
|
||||
isFavorite: _isFavorite,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await _presetService.updatePresetInfo(
|
||||
updatedPreset.presetId,
|
||||
UpdatePresetInfoRequest(
|
||||
presetName: updatedPreset.presetName ?? '未命名预设',
|
||||
presetDescription: updatedPreset.presetDescription,
|
||||
presetTags: updatedPreset.presetTags,
|
||||
),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('预设 "${updatedPreset.presetName}" 更新成功')),
|
||||
);
|
||||
widget.onSuccess?.call();
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('更新预设失败', e.toString());
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('更新失败: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 已废弃的图标/颜色映射方法已移除
|
||||
|
||||
String? _formatDateTime(DateTime? dateTime) {
|
||||
if (dateTime == null) return null;
|
||||
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inDays > 0) {
|
||||
return '${difference.inDays}天前';
|
||||
} else if (difference.inHours > 0) {
|
||||
return '${difference.inHours}小时前';
|
||||
} else if (difference.inMinutes > 0) {
|
||||
return '${difference.inMinutes}分钟前';
|
||||
} else {
|
||||
return '刚刚';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/prompt_models.dart';
|
||||
import '../../../services/api_service/repositories/impl/prompt_repository_impl.dart';
|
||||
import '../../../services/api_service/base/api_client.dart';
|
||||
import '../../../utils/logger.dart';
|
||||
|
||||
/// 编辑用户模板对话框
|
||||
class EditUserTemplateDialog extends StatefulWidget {
|
||||
final PromptTemplate template;
|
||||
final VoidCallback? onSuccess;
|
||||
|
||||
const EditUserTemplateDialog({
|
||||
Key? key,
|
||||
required this.template,
|
||||
this.onSuccess,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<EditUserTemplateDialog> createState() => _EditUserTemplateDialogState();
|
||||
}
|
||||
|
||||
class _EditUserTemplateDialogState extends State<EditUserTemplateDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _descriptionController;
|
||||
late final TextEditingController _templateContentController;
|
||||
late final TextEditingController _versionController;
|
||||
late final TextEditingController _tagsController;
|
||||
|
||||
late String _selectedFeatureType;
|
||||
late bool _isPrivate;
|
||||
late bool _isFavorite;
|
||||
bool _isLoading = false;
|
||||
|
||||
final PromptRepositoryImpl _promptRepository = PromptRepositoryImpl(ApiClient());
|
||||
// 功能类型动态来源:AIFeatureTypeHelper.allFeatures
|
||||
|
||||
// 功能类型标签由 AIFeatureType.displayName 提供
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeControllers();
|
||||
}
|
||||
|
||||
void _initializeControllers() {
|
||||
_nameController = TextEditingController(text: widget.template.name);
|
||||
_descriptionController = TextEditingController(text: widget.template.description ?? '');
|
||||
_templateContentController = TextEditingController(text: widget.template.content);
|
||||
_versionController = TextEditingController(text: '1.0.0');
|
||||
_tagsController = TextEditingController(
|
||||
text: (widget.template.templateTags ?? const <String>[]) .join(', '),
|
||||
);
|
||||
|
||||
_selectedFeatureType = widget.template.featureType.toApiString();
|
||||
_isPrivate = !widget.template.isPublic;
|
||||
_isFavorite = widget.template.isFavorite ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_templateContentController.dispose();
|
||||
_versionController.dispose();
|
||||
_tagsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 700,
|
||||
constraints: const BoxConstraints(maxHeight: 800),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.edit, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'编辑模板',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTemplateInfo(),
|
||||
const SizedBox(height: 24),
|
||||
_buildBasicInfoSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildTemplateContentSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSettingsSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTemplateInfo() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'模板信息',
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildInfoItem('模板ID', widget.template.id),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildInfoItem('使用次数', '${widget.template.useCount ?? 0}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildInfoItem('创建时间', _formatDateTime(widget.template.createdAt)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildInfoItem('最后更新', _formatDateTime(widget.template.updatedAt)),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.template.averageRating != null && widget.template.averageRating! > 0) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildInfoItem('平均评分', '${(widget.template.averageRating ?? 0).toStringAsFixed(1)} ⭐'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildInfoItem('评分人数', '${widget.template.ratingCount ?? 0}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem(String label, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBasicInfoSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'基本信息',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '模板名称 *',
|
||||
hintText: '请输入模板名称',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入模板名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: TextFormField(
|
||||
controller: _versionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '版本号',
|
||||
hintText: '1.0.0',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '模板描述',
|
||||
hintText: '请简要描述此模板的用途和特点',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _selectedFeatureType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '适用功能 *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: AIFeatureTypeHelper.allFeatures.map((t) {
|
||||
final api = t.toApiString();
|
||||
return DropdownMenuItem(
|
||||
value: api,
|
||||
child: Text(t.displayName),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedFeatureType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _tagsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '标签',
|
||||
hintText: '请输入标签,用逗号分隔',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTemplateContentSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'模板内容',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: _showVariableHelper,
|
||||
icon: const Icon(Icons.help_outline, size: 16),
|
||||
label: const Text('变量使用帮助'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _templateContentController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '模板内容 *',
|
||||
hintText: '请输入模板内容,可以使用 {{变量名}} 作为占位符',
|
||||
border: OutlineInputBorder(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
maxLines: 12,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入模板内容';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.lightbulb_outline, size: 16, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'使用 {{变量名}} 创建可填写的占位符,用户使用时可以替换为具体内容',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'设置选项',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
RadioListTile<bool>(
|
||||
title: const Text('私有模板'),
|
||||
subtitle: const Text('仅自己可见和使用'),
|
||||
value: true,
|
||||
groupValue: _isPrivate,
|
||||
onChanged: widget.template.isPublic == true ? null : (value) {
|
||||
setState(() {
|
||||
_isPrivate = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
RadioListTile<bool>(
|
||||
title: const Text('公开模板'),
|
||||
subtitle: widget.template.isPublic == true
|
||||
? const Text('已分享到社区,无法改为私有')
|
||||
: const Text('分享到社区,其他用户也可以使用'),
|
||||
value: false,
|
||||
groupValue: _isPrivate,
|
||||
onChanged: widget.template.isPublic == true ? null : (value) {
|
||||
setState(() {
|
||||
_isPrivate = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
CheckboxListTile(
|
||||
title: const Text('添加到我的收藏'),
|
||||
subtitle: const Text('在收藏夹中显示此模板'),
|
||||
value: _isFavorite,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isFavorite = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _updateTemplate,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('保存更改'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showVariableHelper() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('变量使用帮助'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'变量语法:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: Colors.grey[100],
|
||||
child: const Text(
|
||||
'{{变量名}}',
|
||||
style: TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const Text(
|
||||
'常用变量示例:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...const [
|
||||
'{{角色}} - 如:专业编剧、资深编辑',
|
||||
'{{任务}} - 如:写一个故事、分析文本',
|
||||
'{{风格}} - 如:正式、幽默、诗意',
|
||||
'{{主题}} - 如:科幻、爱情、悬疑',
|
||||
'{{长度}} - 如:500字、简短、详细',
|
||||
'{{语言}} - 如:中文、英文、双语',
|
||||
].map((example) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 4),
|
||||
child: Text('• $example'),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('知道了'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateTemplate() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final tags = _tagsController.text
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.where((tag) => tag.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
final updatedTemplate = widget.template.copyWith(
|
||||
name: _nameController.text.trim(),
|
||||
description: _descriptionController.text.trim().isEmpty
|
||||
? null : _descriptionController.text.trim(),
|
||||
content: _templateContentController.text.trim(),
|
||||
featureType: AIFeatureTypeHelper.fromApiString(_selectedFeatureType),
|
||||
templateTags: tags.isEmpty ? null : tags,
|
||||
isFavorite: _isFavorite,
|
||||
);
|
||||
|
||||
await _promptRepository.updatePromptTemplate(
|
||||
templateId: updatedTemplate.id,
|
||||
name: updatedTemplate.name,
|
||||
content: updatedTemplate.content,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('模板 "${updatedTemplate.name}" 更新成功')),
|
||||
);
|
||||
widget.onSuccess?.call();
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('EditUserTemplateDialog', '更新模板失败', e);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('更新失败: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime? dateTime) {
|
||||
if (dateTime == null) return '';
|
||||
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inDays > 0) {
|
||||
return '${difference.inDays}天前';
|
||||
} else if (difference.inHours > 0) {
|
||||
return '${difference.inHours}小时前';
|
||||
} else if (difference.inMinutes > 0) {
|
||||
return '${difference.inMinutes}分钟前';
|
||||
} else {
|
||||
return '刚刚';
|
||||
}
|
||||
}
|
||||
}
|
||||
853
AINoval/lib/screens/settings/widgets/editor_settings_panel.dart
Normal file
853
AINoval/lib/screens/settings/widgets/editor_settings_panel.dart
Normal file
@@ -0,0 +1,853 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/models/editor_settings.dart';
|
||||
// import 'package:ainoval/widgets/common/settings_widgets.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
|
||||
/// 编辑器设置面板 - 紧凑版
|
||||
/// 提供完整的编辑器配置选项,优化为一页显示
|
||||
class EditorSettingsPanel extends StatefulWidget {
|
||||
const EditorSettingsPanel({
|
||||
super.key,
|
||||
required this.settings,
|
||||
required this.onSettingsChanged,
|
||||
this.onSave,
|
||||
this.onReset,
|
||||
});
|
||||
|
||||
final EditorSettings settings;
|
||||
final ValueChanged<EditorSettings> onSettingsChanged;
|
||||
final VoidCallback? onSave;
|
||||
final VoidCallback? onReset;
|
||||
|
||||
@override
|
||||
State<EditorSettingsPanel> createState() => _EditorSettingsPanelState();
|
||||
}
|
||||
|
||||
class _EditorSettingsPanelState extends State<EditorSettingsPanel> {
|
||||
late EditorSettings _currentSettings;
|
||||
bool _hasUnsavedChanges = false;
|
||||
bool _isSaving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentSettings = widget.settings;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(EditorSettingsPanel oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// 🚀 修复:只有当外部设置真正改变且不是用户操作导致的时,才重置状态
|
||||
if (oldWidget.settings != widget.settings) {
|
||||
// 如果当前设置与新的widget设置相同,说明设置已被外部保存
|
||||
if (_currentSettings == widget.settings) {
|
||||
setState(() {
|
||||
_hasUnsavedChanges = false;
|
||||
});
|
||||
} else {
|
||||
// 如果不同,更新基础设置但保持未保存状态
|
||||
setState(() {
|
||||
_currentSettings = widget.settings;
|
||||
_hasUnsavedChanges = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateSettings(EditorSettings newSettings) {
|
||||
setState(() {
|
||||
_currentSettings = newSettings;
|
||||
// 🚀 修复保存按钮逻辑:先设置未保存状态,再调用回调
|
||||
_hasUnsavedChanges = true;
|
||||
});
|
||||
// 通知父组件设置已更改(用于实时预览),但不影响保存状态
|
||||
widget.onSettingsChanged(newSettings);
|
||||
}
|
||||
|
||||
Future<void> _handleSave() async {
|
||||
if (_isSaving) return; // 🚀 简化:只检查是否正在保存
|
||||
|
||||
setState(() {
|
||||
_isSaving = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// 🚀 实际调用保存回调
|
||||
widget.onSave?.call();
|
||||
|
||||
// 等待一小段时间确保保存操作完成
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
setState(() {
|
||||
_hasUnsavedChanges = false;
|
||||
});
|
||||
|
||||
// 显示保存成功提示
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('编辑器设置已保存'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('保存失败: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleReset() {
|
||||
setState(() {
|
||||
_currentSettings = const EditorSettings();
|
||||
_hasUnsavedChanges = true;
|
||||
});
|
||||
widget.onSettingsChanged(_currentSettings);
|
||||
widget.onReset?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 固定顶部:标题和操作按钮
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getBackgroundColor(context),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: WebTheme.grey200, width: 1),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 标题行
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.edit_note, size: 24, color: WebTheme.getTextColor(context)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'编辑器设置',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: WebTheme.getTextColor(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 保存状态指示
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: (_hasUnsavedChanges
|
||||
? WebTheme.getPrimaryColor(context)
|
||||
: WebTheme.getSecondaryTextColor(context))
|
||||
.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: (_hasUnsavedChanges
|
||||
? WebTheme.getPrimaryColor(context)
|
||||
: WebTheme.getSecondaryTextColor(context))
|
||||
.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_hasUnsavedChanges ? Icons.settings : Icons.check_circle,
|
||||
size: 12,
|
||||
color: _hasUnsavedChanges
|
||||
? WebTheme.getPrimaryColor(context)
|
||||
: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_hasUnsavedChanges ? '可保存' : '已保存',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _hasUnsavedChanges
|
||||
? WebTheme.getPrimaryColor(context)
|
||||
: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 操作按钮行
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'自定义编辑器外观和行为',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 重置按钮
|
||||
TextButton.icon(
|
||||
onPressed: _handleReset,
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
label: const Text('重置'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: WebTheme.getSecondaryTextColor(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 保存按钮 - 🚀 修改为一直可点击
|
||||
ElevatedButton.icon(
|
||||
onPressed: !_isSaving ? _handleSave : null,
|
||||
icon: _isSaving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save, size: 16),
|
||||
label: Text(_isSaving ? '保存中...' : '保存设置'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: WebTheme.getPrimaryColor(context),
|
||||
foregroundColor: WebTheme.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
elevation: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 可滚动的设置内容
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// 紧凑的双列布局
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 左列
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildCompactCard(
|
||||
title: '字体设置',
|
||||
icon: Icons.text_fields,
|
||||
children: [
|
||||
_buildCompactSlider(
|
||||
'字体大小',
|
||||
_currentSettings.fontSize,
|
||||
12, 32, '像素',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(fontSize: value)),
|
||||
),
|
||||
_buildCompactDropdown(
|
||||
'字体',
|
||||
_currentSettings.fontFamily,
|
||||
EditorSettings.availableFontFamilies,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(fontFamily: value)),
|
||||
itemBuilder: (font) {
|
||||
switch (font) {
|
||||
case 'Roboto': return 'Roboto(英文推荐)';
|
||||
case 'serif': return '衬线字体(中文推荐)';
|
||||
case 'sans-serif': return '无衬线字体(中文推荐)';
|
||||
case 'monospace': return '等宽字体';
|
||||
case 'Noto Sans SC': return 'Noto Sans SC(思源黑体)';
|
||||
case 'PingFang SC': return 'PingFang SC(苹方)';
|
||||
case 'Microsoft YaHei': return 'Microsoft YaHei(微软雅黑)';
|
||||
case 'SimHei': return 'SimHei(黑体)';
|
||||
case 'SimSun': return 'SimSun(宋体)';
|
||||
case 'Times New Roman': return 'Times New Roman(英文衬线)';
|
||||
case 'Arial': return 'Arial(英文无衬线)';
|
||||
default: return font;
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildCompactDropdown(
|
||||
'字体粗细',
|
||||
_currentSettings.fontWeight,
|
||||
EditorSettings.availableFontWeights,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(fontWeight: value)),
|
||||
itemBuilder: (weight) {
|
||||
switch (weight) {
|
||||
case FontWeight.w300: return '细体 (300)';
|
||||
case FontWeight.w400: return '正常 (400)';
|
||||
case FontWeight.w500: return '中等 (500)';
|
||||
case FontWeight.w600: return '半粗 (600)';
|
||||
case FontWeight.w700: return '粗体 (700)';
|
||||
default: return '正常 (400)';
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildCompactSlider(
|
||||
'行间距',
|
||||
_currentSettings.lineSpacing,
|
||||
1.0, 3.0, '倍',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(lineSpacing: value)),
|
||||
formatValue: (value) => '${value.toStringAsFixed(1)}x',
|
||||
),
|
||||
_buildCompactSlider(
|
||||
'字符间距',
|
||||
_currentSettings.letterSpacing,
|
||||
-1.0, 2.0, '像素', // 🚀 缩小调整范围,更适合中文
|
||||
(value) => _updateSettings(_currentSettings.copyWith(letterSpacing: value)),
|
||||
formatValue: (value) => value == 0
|
||||
? '标准'
|
||||
: (value > 0 ? '+${value.toStringAsFixed(1)}px' : '${value.toStringAsFixed(1)}px'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildCompactCard(
|
||||
title: '编辑器行为',
|
||||
icon: Icons.settings,
|
||||
children: [
|
||||
_buildCompactSwitch('自动保存', _currentSettings.autoSaveEnabled,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(autoSaveEnabled: value))),
|
||||
if (_currentSettings.autoSaveEnabled)
|
||||
_buildCompactSlider(
|
||||
'保存间隔',
|
||||
_currentSettings.autoSaveIntervalMinutes.toDouble(),
|
||||
1, 15, '分钟',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(autoSaveIntervalMinutes: value.round())),
|
||||
formatValue: (value) => '${value.toInt()}分钟',
|
||||
),
|
||||
_buildCompactSwitch('拼写检查', _currentSettings.spellCheckEnabled,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(spellCheckEnabled: value))),
|
||||
_buildCompactSwitch('显示字数', _currentSettings.showWordCount,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(showWordCount: value))),
|
||||
_buildCompactSwitch('显示行号', _currentSettings.showLineNumbers,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(showLineNumbers: value))),
|
||||
_buildCompactSwitch('高亮当前行', _currentSettings.highlightActiveLine,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(highlightActiveLine: value))),
|
||||
_buildCompactSwitch('Vim模式', _currentSettings.enableVimMode,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(enableVimMode: value))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// 🚀 移动导出设置到左列
|
||||
_buildCompactCard(
|
||||
title: '导出设置',
|
||||
icon: Icons.download,
|
||||
children: [
|
||||
_buildCompactDropdown(
|
||||
'默认导出格式',
|
||||
_currentSettings.defaultExportFormat,
|
||||
EditorSettings.availableExportFormats,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(defaultExportFormat: value)),
|
||||
itemBuilder: (format) {
|
||||
switch (format) {
|
||||
case 'markdown': return 'Markdown (.md)';
|
||||
case 'docx': return 'Word文档 (.docx)';
|
||||
case 'pdf': return 'PDF文档 (.pdf)';
|
||||
case 'txt': return '纯文本 (.txt)';
|
||||
case 'html': return 'HTML文档 (.html)';
|
||||
default: return format.toUpperCase();
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildCompactSwitch('包含元数据', _currentSettings.includeMetadata,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(includeMetadata: value))),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 右列
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildCompactCard(
|
||||
title: '布局间距',
|
||||
icon: Icons.format_align_center,
|
||||
children: [
|
||||
_buildCompactSlider(
|
||||
'水平边距',
|
||||
_currentSettings.paddingHorizontal,
|
||||
8, 48, '像素',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(paddingHorizontal: value)),
|
||||
),
|
||||
_buildCompactSlider(
|
||||
'垂直边距',
|
||||
_currentSettings.paddingVertical,
|
||||
8, 32, '像素',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(paddingVertical: value)),
|
||||
),
|
||||
_buildCompactSlider(
|
||||
'段落间距',
|
||||
_currentSettings.paragraphSpacing,
|
||||
4, 24, '像素',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(paragraphSpacing: value)),
|
||||
),
|
||||
_buildCompactSlider(
|
||||
'缩进大小',
|
||||
_currentSettings.indentSize,
|
||||
16, 64, '像素',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(indentSize: value)),
|
||||
),
|
||||
_buildCompactSlider(
|
||||
'最大行宽',
|
||||
_currentSettings.maxLineWidth,
|
||||
400, 1500, '像素',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(maxLineWidth: value)),
|
||||
),
|
||||
_buildCompactSlider(
|
||||
'最小编辑器高度',
|
||||
_currentSettings.minEditorHeight,
|
||||
1200, 3000, '像素',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(minEditorHeight: value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildCompactCard(
|
||||
title: '视觉效果',
|
||||
icon: Icons.visibility,
|
||||
children: [
|
||||
_buildCompactSwitch('暗色模式', _currentSettings.darkModeEnabled,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(darkModeEnabled: value))),
|
||||
_buildCompactSwitch('平滑滚动', _currentSettings.smoothScrolling,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(smoothScrolling: value))),
|
||||
_buildCompactSwitch('淡入动画', _currentSettings.fadeInAnimation,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(fadeInAnimation: value))),
|
||||
_buildCompactSwitch('打字机模式', _currentSettings.useTypewriterMode,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(useTypewriterMode: value))),
|
||||
_buildCompactSwitch('显示小地图', _currentSettings.showMiniMap,
|
||||
(value) => _updateSettings(_currentSettings.copyWith(showMiniMap: value))),
|
||||
_buildCompactSlider(
|
||||
'光标闪烁速度',
|
||||
_currentSettings.cursorBlinkRate,
|
||||
0.5, 3.0, '秒',
|
||||
(value) => _updateSettings(_currentSettings.copyWith(cursorBlinkRate: value)),
|
||||
formatValue: (value) => '${value.toStringAsFixed(1)}s',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// 🚀 保留选择和光标设置卡片在右列
|
||||
_buildCompactCard(
|
||||
title: '选择和光标',
|
||||
icon: Icons.colorize,
|
||||
children: [
|
||||
_buildColorPicker(
|
||||
'选择高亮颜色',
|
||||
Color(_currentSettings.selectionHighlightColor),
|
||||
(color) => _updateSettings(_currentSettings.copyWith(selectionHighlightColor: color.value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 预览区域
|
||||
_buildPreviewCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactCard({
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required List<Widget> children,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: WebTheme.grey200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 卡片标题 - 🚀 减少内边距
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.grey50,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: WebTheme.getTextColor(context)),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 卡片内容 - 🚀 减少内边距
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactSlider(
|
||||
String label,
|
||||
double value,
|
||||
double min,
|
||||
double max,
|
||||
String unit,
|
||||
ValueChanged<double> onChanged, {
|
||||
String Function(double)? formatValue,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formatValue?.call(value) ?? '${value.toStringAsFixed(value % 1 == 0 ? 0 : 1)}$unit',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 26,
|
||||
child: Slider(
|
||||
value: value.clamp(min, max).toDouble(),
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: ((max - min) * (unit == '倍' ? 10 : 1)).round(),
|
||||
onChanged: onChanged,
|
||||
activeColor: WebTheme.getPrimaryColor(context),
|
||||
inactiveColor: WebTheme.grey300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactSwitch(
|
||||
String label,
|
||||
bool value,
|
||||
ValueChanged<bool> onChanged,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center, // 🚀 对齐优化
|
||||
children: [
|
||||
Expanded( // 🚀 让文字可以自动换行
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8), // 🚀 添加间距
|
||||
// 🚀 优化开关大小,与文字高度匹配
|
||||
Transform.scale(
|
||||
scale: 0.8, // 缩小开关
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
activeColor: WebTheme.getPrimaryColor(context),
|
||||
inactiveThumbColor: WebTheme.grey400,
|
||||
inactiveTrackColor: Colors.grey[300],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactDropdown<T>(
|
||||
String label,
|
||||
T value,
|
||||
List<T> items,
|
||||
ValueChanged<T?> onChanged, {
|
||||
String Function(T)? itemBuilder,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: DropdownButtonFormField<T>(
|
||||
value: value,
|
||||
items: items.map((item) {
|
||||
return DropdownMenuItem<T>(
|
||||
value: item,
|
||||
child: Text(
|
||||
itemBuilder?.call(item) ?? item.toString(),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: WebTheme.grey300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: WebTheme.grey300),
|
||||
),
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🚀 构建颜色选择器
|
||||
Widget _buildColorPicker(
|
||||
String label,
|
||||
Color currentColor,
|
||||
ValueChanged<Color> onColorChanged,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
GestureDetector(
|
||||
onTap: () => _showColorPicker(currentColor, onColorChanged),
|
||||
child: Container(
|
||||
height: 30,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: currentColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: WebTheme.grey300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: currentColor,
|
||||
borderRadius: const BorderRadius.horizontal(left: Radius.circular(4)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
borderRadius: const BorderRadius.horizontal(right: Radius.circular(4)),
|
||||
),
|
||||
child: Text(
|
||||
'#${currentColor.value.toRadixString(16).substring(2).toUpperCase()}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示颜色选择对话框
|
||||
void _showColorPicker(Color currentColor, ValueChanged<Color> onColorChanged) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('选择颜色'),
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
Colors.red,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.deepPurple,
|
||||
Colors.indigo,
|
||||
Colors.blue,
|
||||
Colors.lightBlue,
|
||||
Colors.cyan,
|
||||
Colors.teal,
|
||||
Colors.green,
|
||||
Colors.lightGreen,
|
||||
Colors.lime,
|
||||
Colors.yellow,
|
||||
Colors.amber,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.brown,
|
||||
Colors.grey,
|
||||
Colors.blueGrey,
|
||||
Colors.black,
|
||||
].map((color) => GestureDetector(
|
||||
onTap: () {
|
||||
onColorChanged(color);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: currentColor == color ? Colors.white : Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)).toList(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewCard() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getSurfaceColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: WebTheme.grey200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.grey50,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.preview, size: 18, color: WebTheme.getTextColor(context)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'预览效果',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: _currentSettings.paddingHorizontal,
|
||||
vertical: _currentSettings.paddingVertical,
|
||||
),
|
||||
child: Text(
|
||||
'这是预览文本,展示当前字体设置的效果。您可以看到字体大小、行间距、字体样式等设置的实际显示效果。',
|
||||
style: TextStyle(
|
||||
fontFamily: _currentSettings.fontFamily,
|
||||
fontSize: _currentSettings.fontSize,
|
||||
fontWeight: _currentSettings.fontWeight,
|
||||
height: _currentSettings.lineSpacing,
|
||||
letterSpacing: _currentSettings.letterSpacing,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
169
AINoval/lib/screens/settings/widgets/membership_panel.dart
Normal file
169
AINoval/lib/screens/settings/widgets/membership_panel.dart
Normal file
@@ -0,0 +1,169 @@
|
||||
import 'package:ainoval/models/admin/subscription_models.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/payment_repository.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/subscription_repository.dart';
|
||||
import 'package:ainoval/utils/logger.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class MembershipPanel extends StatefulWidget {
|
||||
const MembershipPanel({super.key});
|
||||
|
||||
@override
|
||||
State<MembershipPanel> createState() => _MembershipPanelState();
|
||||
}
|
||||
|
||||
class _MembershipPanelState extends State<MembershipPanel> {
|
||||
final _subRepo = PublicSubscriptionRepository();
|
||||
final _payRepo = PaymentRepository();
|
||||
|
||||
final String _tag = 'MembershipPanel';
|
||||
bool _loading = true;
|
||||
List<SubscriptionPlan> _plans = const [];
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchPlans();
|
||||
}
|
||||
|
||||
Future<void> _fetchPlans() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final plans = await _subRepo.listActivePlans();
|
||||
setState(() {
|
||||
_plans = plans;
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.e(_tag, '获取订阅计划失败', e);
|
||||
setState(() {
|
||||
_error = '获取订阅计划失败';
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _buy(SubscriptionPlan plan, PayChannel channel) async {
|
||||
try {
|
||||
final order = await _payRepo.createPayment(planId: plan.id!, channel: channel);
|
||||
if (order.paymentUrl.isNotEmpty) {
|
||||
final uri = Uri.parse(order.paymentUrl);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.e(_tag, '创建支付失败', e);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('创建支付失败: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_error!),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(onPressed: _fetchPlans, child: const Text('重试')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_plans.isEmpty) {
|
||||
return const Center(child: Text('暂无可购买的会员计划'));
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _plans.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final p = _plans[index];
|
||||
final feats = p.features ?? const {};
|
||||
final aiDaily = feats['ai.daily.calls']?.toString();
|
||||
final importDaily = feats['import.daily.limit']?.toString();
|
||||
final novelMax = feats['novel.max.count']?.toString();
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(p.planName, style: Theme.of(context).textTheme.titleLarge),
|
||||
Text('${p.price.toStringAsFixed(2)} ${p.currency}')
|
||||
],
|
||||
),
|
||||
if (p.description != null && p.description!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(p.description!),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (aiDaily != null) _badge(context, 'AI每日次数 $aiDaily'),
|
||||
if (importDaily != null) _badge(context, '导入每日次数 $importDaily'),
|
||||
if (novelMax != null) _badge(context, '可创作小说数 $novelMax'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => _buy(p, PayChannel.wechat),
|
||||
child: const Text('微信支付'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
OutlinedButton(
|
||||
onPressed: () => _buy(p, PayChannel.alipay),
|
||||
child: const Text('支付宝'),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _badge(BuildContext context, String text) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(text),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
247
AINoval/lib/screens/settings/widgets/model_group_list.dart
Normal file
247
AINoval/lib/screens/settings/widgets/model_group_list.dart
Normal file
@@ -0,0 +1,247 @@
|
||||
import 'package:ainoval/models/ai_model_group.dart';
|
||||
import 'package:ainoval/models/model_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 模型分组列表组件
|
||||
/// 在提供商内显示按前缀分组的模型列表
|
||||
class ModelGroupList extends StatelessWidget {
|
||||
const ModelGroupList({
|
||||
super.key,
|
||||
required this.modelGroup,
|
||||
required this.onModelSelected,
|
||||
this.selectedModel,
|
||||
this.verifiedModels = const [],
|
||||
});
|
||||
|
||||
final AIModelGroup modelGroup;
|
||||
final ValueChanged<String> onModelSelected;
|
||||
final String? selectedModel;
|
||||
final List<String> verifiedModels;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
// final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withOpacity(0.15),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: modelGroup.groups.length,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1,
|
||||
color: theme.colorScheme.outline.withOpacity(0.1),
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final group = modelGroup.groups[index];
|
||||
return _buildModelPrefixGroup(context, group);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModelPrefixGroup(BuildContext context, ModelPrefixGroup group) {
|
||||
final theme = Theme.of(context);
|
||||
// final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
title: Text(
|
||||
group.prefix,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
iconColor: theme.colorScheme.onSurface,
|
||||
collapsedIconColor: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
initiallyExpanded: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
collapsedBackgroundColor: Colors.transparent,
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
childrenPadding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
||||
children: group.modelsInfo.map((modelInfo) {
|
||||
final isSelected = modelInfo.id == selectedModel;
|
||||
final isVerified = verifiedModels.contains(modelInfo.id);
|
||||
return _buildModelItem(context, modelInfo, isSelected, isVerified);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModelItem(BuildContext context, ModelInfo modelInfo, bool isSelected, bool isVerified) {
|
||||
final theme = Theme.of(context);
|
||||
// final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
String displayName = modelInfo.name.isNotEmpty ? modelInfo.name : modelInfo.id;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? theme.colorScheme.surfaceContainerHigh
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? theme.colorScheme.outline.withOpacity(0.3)
|
||||
: Colors.transparent,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
title: Row(
|
||||
children: [
|
||||
// 模型状态图标
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: isVerified
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isVerified
|
||||
? Colors.green.withOpacity(0.3)
|
||||
: theme.colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: isVerified
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: theme.colorScheme.secondary,
|
||||
size: 12,
|
||||
)
|
||||
: Text(
|
||||
_getModelInitial(modelInfo.id),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 模型名称
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (modelInfo.id != displayName)
|
||||
Text(
|
||||
modelInfo.id,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 标签
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 已验证标记
|
||||
if (isVerified)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.secondary.withOpacity(0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'✓',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 4),
|
||||
|
||||
// 免费标签
|
||||
if (modelInfo.id.toLowerCase().contains('free'))
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.secondary.withOpacity(0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'FREE',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => onModelSelected(modelInfo.id),
|
||||
selected: isSelected,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 获取模型的首字母作为图标
|
||||
String _getModelInitial(String modelId) {
|
||||
if (modelId.contains('/')) {
|
||||
return modelId.split('/').first[0].toUpperCase();
|
||||
} else if (modelId.contains('-')) {
|
||||
return modelId.split('-').first[0].toUpperCase();
|
||||
} else {
|
||||
return modelId.isNotEmpty ? modelId[0].toUpperCase() : '?';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/models/user_ai_model_config_model.dart';
|
||||
import 'package:ainoval/screens/editor/widgets/menu_builder.dart';
|
||||
import '../../../config/provider_icons.dart';
|
||||
|
||||
/// 模型提供商分组卡片
|
||||
/// 显示提供商信息和其下的模型列表
|
||||
class ModelProviderGroupCard extends StatelessWidget {
|
||||
const ModelProviderGroupCard({
|
||||
super.key,
|
||||
required this.provider,
|
||||
required this.providerName,
|
||||
required this.description,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.configs,
|
||||
required this.isExpanded,
|
||||
required this.onToggleExpanded,
|
||||
required this.onAddModel,
|
||||
required this.onSetDefault,
|
||||
required this.onValidate,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
final String provider;
|
||||
final String providerName;
|
||||
final String description;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final List<UserAIModelConfigModel> configs;
|
||||
final bool isExpanded;
|
||||
final VoidCallback onToggleExpanded;
|
||||
final VoidCallback onAddModel;
|
||||
final Function(String) onSetDefault;
|
||||
final Function(String) onValidate;
|
||||
final Function(String) onEdit;
|
||||
final Function(String) onDelete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
// 统计验证状态
|
||||
final verifiedCount = configs.where((c) => c.isValidated).length;
|
||||
final totalCount = configs.length;
|
||||
|
||||
// 查找在当前提供商组内的默认模型
|
||||
final defaultConfig = configs.firstWhere(
|
||||
(c) => c.isDefault,
|
||||
orElse: () => UserAIModelConfigModel.empty(),
|
||||
);
|
||||
|
||||
// 只有当默认模型真正在当前组内时才显示
|
||||
final hasDefaultInThisGroup = defaultConfig.id.isNotEmpty;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withOpacity(0.15),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(isDark ? 0.3 : 0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 提供商头部
|
||||
InkWell(
|
||||
onTap: onToggleExpanded,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// 提供商图标
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: ProviderIcons.getProviderIconForContext(
|
||||
provider,
|
||||
iconSize: IconSize.large,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 提供商信息
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
providerName,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 右侧状态信息(根据HTML样式改进)
|
||||
_buildRightSideInfo(context, verifiedCount, totalCount, defaultConfig, hasDefaultInThisGroup),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 分隔线
|
||||
if (isExpanded)
|
||||
Divider(
|
||||
height: 1,
|
||||
color: theme.colorScheme.outline.withOpacity(0.2),
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
),
|
||||
|
||||
// 模型列表
|
||||
if (isExpanded)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// 模型项列表
|
||||
...configs.map((config) => _buildModelItem(context, config)),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 添加模型按钮
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onAddModel,
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('添加模型'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
side: BorderSide(
|
||||
color: theme.colorScheme.outline.withOpacity(0.5),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 构建右侧状态信息,参考HTML结构
|
||||
Widget _buildRightSideInfo(BuildContext context, int verifiedCount, int totalCount,
|
||||
UserAIModelConfigModel defaultConfig, bool hasDefaultInThisGroup) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minWidth: 120),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 桌面端显示(sm及以上)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isSmallScreen = MediaQuery.of(context).size.width < 640;
|
||||
|
||||
if (isSmallScreen) {
|
||||
// 移动端简化显示
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'$verifiedCount/$totalCount',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildChevronIcon(isDark),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// 桌面端完整显示
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 状态显示
|
||||
Text(
|
||||
'$verifiedCount/$totalCount 已启用',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 默认模型显示(只有当前组有默认模型时才显示)
|
||||
if (hasDefaultInThisGroup)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
child: Text(
|
||||
'默认: ${defaultConfig.alias}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
_buildChevronIcon(isDark),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 构建Chevron图标
|
||||
Widget _buildChevronIcon(bool isDark) {
|
||||
return AnimatedRotation(
|
||||
turns: isExpanded ? 0.25 : 0, // 90度旋转
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
size: 16,
|
||||
color: isDark ? Colors.white.withOpacity(0.7) : Colors.black.withOpacity(0.7),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModelItem(BuildContext context, UserAIModelConfigModel config) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: config.isDefault
|
||||
? theme.colorScheme.primary.withOpacity(0.1)
|
||||
: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: config.isDefault
|
||||
? theme.colorScheme.primary.withOpacity(0.3)
|
||||
: theme.colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 模型状态图标
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: config.isValidated
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.orange.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: config.isValidated
|
||||
? Colors.green.withOpacity(0.3)
|
||||
: Colors.orange.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
config.isValidated ? Icons.check_circle : Icons.access_time,
|
||||
color: config.isValidated ? Colors.green : Colors.orange,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 模型信息
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
config.alias,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
if (config.isDefault) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'默认',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
config.modelName,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 价格信息(模拟数据)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'\$0.03',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'输入',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'\$0.06',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'输出',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'每千标记',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 操作按钮
|
||||
MenuBuilder.buildModelMenu(
|
||||
context: context,
|
||||
configId: config.id,
|
||||
isValidated: config.isValidated,
|
||||
isDefault: config.isDefault,
|
||||
onValidate: (configId) async => onValidate(configId),
|
||||
onSetDefault: (configId) async => onSetDefault(configId),
|
||||
onEdit: (configId) async => onEdit(configId),
|
||||
onDelete: (configId) async => onDelete(configId),
|
||||
width: 180,
|
||||
align: 'right',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
616
AINoval/lib/screens/settings/widgets/model_service_card.dart
Normal file
616
AINoval/lib/screens/settings/widgets/model_service_card.dart
Normal file
@@ -0,0 +1,616 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/provider_icons.dart';
|
||||
|
||||
/// 模型服务卡片的数据模型
|
||||
class ModelServiceData {
|
||||
final String id;
|
||||
final String name;
|
||||
final String provider;
|
||||
final String path;
|
||||
final bool verified;
|
||||
final bool isDefault;
|
||||
final String? status;
|
||||
final DateTime timestamp;
|
||||
final String? description;
|
||||
final List<String>? tags;
|
||||
final String? apiEndpoint;
|
||||
final ModelPerformance? performance;
|
||||
|
||||
ModelServiceData({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.provider,
|
||||
required this.path,
|
||||
required this.verified,
|
||||
required this.isDefault,
|
||||
this.status,
|
||||
required this.timestamp,
|
||||
this.description,
|
||||
this.tags,
|
||||
this.apiEndpoint,
|
||||
this.performance,
|
||||
});
|
||||
}
|
||||
|
||||
/// 模型性能数据
|
||||
class ModelPerformance {
|
||||
final int latency; // 毫秒
|
||||
final double throughput; // 请求/秒
|
||||
|
||||
ModelPerformance({
|
||||
required this.latency,
|
||||
required this.throughput,
|
||||
});
|
||||
}
|
||||
|
||||
/// 模型服务卡片组件
|
||||
class ModelServiceCard extends StatefulWidget {
|
||||
const ModelServiceCard({
|
||||
super.key,
|
||||
required this.model,
|
||||
required this.onSetDefault,
|
||||
required this.onValidate,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
final ModelServiceData model;
|
||||
final Function(String) onSetDefault;
|
||||
final Function(String) onValidate;
|
||||
final Function(String) onEdit;
|
||||
final Function(String) onDelete;
|
||||
|
||||
@override
|
||||
State<ModelServiceCard> createState() => _ModelServiceCardState();
|
||||
}
|
||||
|
||||
class _ModelServiceCardState extends State<ModelServiceCard> {
|
||||
bool _expanded = false;
|
||||
// 未使用的变量已移除
|
||||
|
||||
// 获取提供商图标
|
||||
Widget _getProviderLogo(String provider) {
|
||||
return ProviderIcons.getProviderIconForContext(
|
||||
provider,
|
||||
iconSize: IconSize.medium,
|
||||
);
|
||||
}
|
||||
|
||||
// 获取状态颜色(未使用,保留以备后续扩展)
|
||||
Color _getStatusColor(String status) {
|
||||
final statusLower = status.toLowerCase();
|
||||
if (statusLower.contains('error') || statusLower.contains('失败')) {
|
||||
return Theme.of(context).colorScheme.error;
|
||||
} else if (statusLower.contains('warning') || statusLower.contains('警告')) {
|
||||
return Theme.of(context).colorScheme.tertiary;
|
||||
} else {
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本(未使用,保留以备后续扩展)
|
||||
String _getStatusText(String status) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// 获取性能颜色
|
||||
Color _getPerformanceColor(int latency) {
|
||||
if (latency < 100) {
|
||||
return Theme.of(context).colorScheme.secondary;
|
||||
} else if (latency < 300) {
|
||||
return Theme.of(context).colorScheme.tertiary;
|
||||
} else {
|
||||
return Theme.of(context).colorScheme.error;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: widget.model.verified
|
||||
? theme.colorScheme.outline.withAlpha(51)
|
||||
: theme.colorScheme.outline.withAlpha(77),
|
||||
width: widget.model.verified ? 0.5 : 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.shadow.withOpacity(0.08),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 卡片主体内容
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 头部:图标、名称和操作菜单
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 提供商图标
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withAlpha(128),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: _getProviderLogo(widget.model.provider),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 名称和路径
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.model.name,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.model.provider,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'•',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface.withAlpha(77),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withAlpha(128),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
widget.model.path,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 操作菜单
|
||||
PopupMenuButton(
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onSurface.withAlpha(153),
|
||||
),
|
||||
itemBuilder: (context) => <PopupMenuEntry<String>>[
|
||||
const PopupMenuItem<String>(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('编辑', style: TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'copy_path',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.copy, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('复制模型路径', style: TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.model.apiEndpoint != null)
|
||||
const PopupMenuItem<String>(
|
||||
value: 'visit_api',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.open_in_new, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('访问API', style: TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete_outline, size: 16, color: theme.colorScheme.error),
|
||||
const SizedBox(width: 8),
|
||||
Text('删除', style: TextStyle(fontSize: 13, color: theme.colorScheme.error)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (String value) {
|
||||
switch (value) {
|
||||
case 'edit':
|
||||
widget.onEdit(widget.model.id);
|
||||
break;
|
||||
case 'copy_path':
|
||||
// 复制路径逻辑
|
||||
break;
|
||||
case 'visit_api':
|
||||
// 访问API逻辑
|
||||
break;
|
||||
case 'delete':
|
||||
widget.onDelete(widget.model.id);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 状态标签和时间戳
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 状态标签
|
||||
Row(
|
||||
children: [
|
||||
// 验证状态
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.model.verified
|
||||
? theme.colorScheme.secondaryContainer
|
||||
: theme.colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: widget.model.verified
|
||||
? theme.colorScheme.secondary.withOpacity(0.5)
|
||||
: theme.colorScheme.tertiary.withOpacity(0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
widget.model.verified
|
||||
? Icons.check_circle_outline
|
||||
: Icons.access_time,
|
||||
size: 12,
|
||||
color: widget.model.verified
|
||||
? theme.colorScheme.secondary
|
||||
: theme.colorScheme.tertiary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.model.verified ? '已验证' : '未验证',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: widget.model.verified
|
||||
? theme.colorScheme.onSecondaryContainer
|
||||
: theme.colorScheme.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 默认状态标签
|
||||
if (widget.model.isDefault)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withAlpha(26),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary.withAlpha(77),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.star,
|
||||
size: 12,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'默认',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 时间戳
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 12,
|
||||
color: theme.colorScheme.onSurface.withAlpha(128),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatDate(widget.model.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: theme.colorScheme.onSurface.withAlpha(128),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 性能指标
|
||||
if (widget.model.performance != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withAlpha(77),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 延迟
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'延迟',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: theme.colorScheme.onSurface.withAlpha(153),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bolt,
|
||||
size: 14,
|
||||
color: _getPerformanceColor(widget.model.performance!.latency),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${widget.model.performance!.latency}ms',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _getPerformanceColor(widget.model.performance!.latency),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 吞吐量
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'吞吐量',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: theme.colorScheme.onSurface.withAlpha(153),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${widget.model.performance!.throughput} 次/秒',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 展开的详情内容
|
||||
if (_expanded && widget.model.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Divider(
|
||||
color: theme.colorScheme.outline.withAlpha(26),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.model.description!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.colorScheme.onSurface.withAlpha(204),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
|
||||
// 标签
|
||||
if (widget.model.tags != null && widget.model.tags!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: widget.model.tags!.map((tag) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withAlpha(26),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary.withAlpha(77),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 底部操作区
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.outline.withAlpha(26),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 查看详情按钮
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_expanded = !_expanded;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withAlpha(77),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_expanded ? '收起详情' : '查看详情',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.colorScheme.onSurface.withAlpha(179),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 设为默认按钮(仅未验证时显示)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// 如果未验证,则执行验证逻辑
|
||||
if (!widget.model.verified) {
|
||||
widget.onValidate(widget.model.id);
|
||||
} else {
|
||||
// 如果已验证,则执行设为默认逻辑
|
||||
if (!widget.model.isDefault) {
|
||||
widget.onSetDefault(widget.model.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: theme.colorScheme.outline.withAlpha(26),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
widget.model.verified
|
||||
? (widget.model.isDefault ? '默认模型' : '设为默认')
|
||||
: '验证连接', // 未验证时显示验证
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: widget.model.verified && widget.model.isDefault
|
||||
? theme.colorScheme.onSurface.withAlpha(100) // 如果是默认,灰色显示
|
||||
: theme.colorScheme.primary, // 否则高亮
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
213
AINoval/lib/screens/settings/widgets/model_service_header.dart
Normal file
213
AINoval/lib/screens/settings/widgets/model_service_header.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/widgets/common/app_search_field.dart';
|
||||
|
||||
/// 模型服务列表页面的头部组件
|
||||
/// 包含标题、描述、搜索框、筛选下拉框和添加按钮
|
||||
class ModelServiceHeader extends StatelessWidget {
|
||||
const ModelServiceHeader({
|
||||
super.key,
|
||||
required this.onSearch,
|
||||
required this.onAddNew,
|
||||
required this.onFilterChange,
|
||||
});
|
||||
|
||||
final Function(String) onSearch;
|
||||
final VoidCallback onAddNew;
|
||||
final Function(String) onFilterChange;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 主标题区域
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'模型服务管理',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'管理和配置你的 AI 模型提供商及其可用模型。',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 添加按钮
|
||||
ElevatedButton.icon(
|
||||
onPressed: onAddNew,
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
label: Text(
|
||||
'添加模型',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 控制栏
|
||||
Row(
|
||||
children: [
|
||||
// 搜索框
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: AppSearchField(
|
||||
hintText: '搜索模型提供商...',
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
onChanged: onSearch,
|
||||
controller: TextEditingController(),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 筛选下拉框
|
||||
SizedBox(
|
||||
width: 140,
|
||||
height: 40,
|
||||
child: DropdownButtonFormField<String>(
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.surface,
|
||||
),
|
||||
value: 'all',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
dropdownColor: theme.colorScheme.surface,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'all',
|
||||
child: Text(
|
||||
'全部模型',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'verified',
|
||||
child: Text(
|
||||
'已验证',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'unverified',
|
||||
child: Text(
|
||||
'未验证',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
onFilterChange(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 设置按钮
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.settings, size: 20),
|
||||
style: IconButton.styleFrom(
|
||||
padding: const EdgeInsets.all(10),
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
side: BorderSide(
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
import 'package:ainoval/utils/logger.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:ainoval/widgets/common/top_toast.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart';
|
||||
import 'package:ainoval/models/user_ai_model_config_model.dart';
|
||||
import 'package:ainoval/screens/settings/widgets/model_provider_group_card.dart';
|
||||
import 'package:ainoval/screens/settings/widgets/model_service_header.dart';
|
||||
import 'package:ainoval/screens/editor/managers/editor_state_manager.dart';
|
||||
import 'package:ainoval/config/provider_icons.dart';
|
||||
|
||||
/// 模型服务列表页面
|
||||
/// 显示按提供商分组的模型服务列表
|
||||
class ModelServiceListPage extends StatefulWidget {
|
||||
const ModelServiceListPage({
|
||||
super.key,
|
||||
required this.userId,
|
||||
required this.onAddNew,
|
||||
required this.onEditConfig,
|
||||
required this.editorStateManager,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
final VoidCallback onAddNew;
|
||||
final Function(UserAIModelConfigModel) onEditConfig;
|
||||
final EditorStateManager editorStateManager;
|
||||
|
||||
@override
|
||||
State<ModelServiceListPage> createState() => _ModelServiceListPageState();
|
||||
}
|
||||
|
||||
class _ModelServiceListPageState extends State<ModelServiceListPage> {
|
||||
String _searchQuery = '';
|
||||
String _filterValue = 'all';
|
||||
Map<String, bool> _expandedProviders = {};
|
||||
|
||||
// 添加缓存机制
|
||||
DateTime? _lastLoadTime;
|
||||
static const Duration _cacheValidDuration = Duration(minutes: 3);
|
||||
bool _isInitialLoad = true;
|
||||
|
||||
bool get _shouldRefreshConfigs {
|
||||
if (_lastLoadTime == null || _isInitialLoad) return true;
|
||||
return DateTime.now().difference(_lastLoadTime!) > _cacheValidDuration;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserConfigs();
|
||||
}
|
||||
|
||||
void _loadUserConfigs() {
|
||||
// 检查是否需要刷新
|
||||
if (!_shouldRefreshConfigs) {
|
||||
AppLogger.d('ModelServiceListPage', '使用缓存数据,跳过重新加载');
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.i('ModelServiceListPage', '开始加载用户配置');
|
||||
_lastLoadTime = DateTime.now();
|
||||
_isInitialLoad = false;
|
||||
|
||||
context.read<AiConfigBloc>().add(LoadAiConfigs(userId: widget.userId));
|
||||
}
|
||||
|
||||
void _handleSearch(String query) {
|
||||
setState(() {
|
||||
_searchQuery = query.toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
void _handleFilterChange(String value) {
|
||||
setState(() {
|
||||
_filterValue = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleSetDefault(String configId) {
|
||||
AppLogger.i('ModelServiceListPage', '设置默认配置: $configId');
|
||||
widget.editorStateManager.setModelOperationInProgress(true);
|
||||
context.read<AiConfigBloc>().add(SetDefaultAiConfig(
|
||||
userId: widget.userId,
|
||||
configId: configId,
|
||||
));
|
||||
}
|
||||
|
||||
void _handleValidate(String configId) {
|
||||
AppLogger.i('ModelServiceListPage', '验证配置: $configId');
|
||||
widget.editorStateManager.setModelOperationInProgress(true);
|
||||
context.read<AiConfigBloc>().add(ValidateAiConfig(
|
||||
userId: widget.userId,
|
||||
configId: configId,
|
||||
));
|
||||
}
|
||||
|
||||
void _handleEdit(String configId) {
|
||||
final config = context.read<AiConfigBloc>().state.configs.firstWhereOrNull((c) => c.id == configId);
|
||||
if (config != null) {
|
||||
widget.onEditConfig(config);
|
||||
} else {
|
||||
TopToast.warning(context, "未找到要编辑的配置");
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDelete(String configId) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这个模型服务配置吗?此操作无法撤销。'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('取消'),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: const Text('删除'),
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
AppLogger.i('ModelServiceListPage', '删除配置: $configId');
|
||||
|
||||
// 使缓存失效
|
||||
_lastLoadTime = null;
|
||||
|
||||
context.read<AiConfigBloc>().add(DeleteAiConfig(
|
||||
userId: widget.userId,
|
||||
configId: configId,
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleAddModel(String provider) {
|
||||
// 调用父组件的回调,并传递选中的提供商
|
||||
widget.onAddNew();
|
||||
}
|
||||
|
||||
void _handleToggleProvider(String provider) {
|
||||
setState(() {
|
||||
_expandedProviders[provider] = !(_expandedProviders[provider] ?? true);
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤配置列表
|
||||
List<UserAIModelConfigModel> _getFilteredConfigs(List<UserAIModelConfigModel> configs) {
|
||||
return configs.where((config) {
|
||||
final matchesSearch = _searchQuery.isEmpty ||
|
||||
config.alias.toLowerCase().contains(_searchQuery) ||
|
||||
config.provider.toLowerCase().contains(_searchQuery) ||
|
||||
config.modelName.toLowerCase().contains(_searchQuery);
|
||||
|
||||
bool matchesFilter = true;
|
||||
if (_filterValue == 'verified') {
|
||||
matchesFilter = config.isValidated;
|
||||
} else if (_filterValue == 'unverified') {
|
||||
matchesFilter = !config.isValidated;
|
||||
}
|
||||
|
||||
return matchesSearch && matchesFilter;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// 按提供商分组配置
|
||||
Map<String, List<UserAIModelConfigModel>> _groupConfigsByProvider(List<UserAIModelConfigModel> configs) {
|
||||
final Map<String, List<UserAIModelConfigModel>> grouped = {};
|
||||
|
||||
for (final config in configs) {
|
||||
final provider = config.provider;
|
||||
if (!grouped.containsKey(provider)) {
|
||||
grouped[provider] = [];
|
||||
}
|
||||
grouped[provider]!.add(config);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// 获取提供商信息
|
||||
Map<String, dynamic> _getProviderInfo(String provider) {
|
||||
return {
|
||||
'name': ProviderIcons.getProviderDisplayName(provider),
|
||||
'description': _getProviderDescription(provider),
|
||||
'icon': Icons.api, // 保留作为备用,但实际使用ProviderIcons
|
||||
'color': ProviderIcons.getProviderColor(provider),
|
||||
};
|
||||
}
|
||||
|
||||
// 获取提供商描述
|
||||
String _getProviderDescription(String provider) {
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'openai':
|
||||
return '适用于多种场景的先进语言模型';
|
||||
case 'anthropic':
|
||||
return '注重安全性的 Constitutional AI 模型';
|
||||
case 'google':
|
||||
case 'gemini':
|
||||
return 'Gemini 模型与 PaLM 系列';
|
||||
case 'openrouter':
|
||||
return '聚合多家模型的统一 API';
|
||||
case 'ollama':
|
||||
return '本地模型运行环境';
|
||||
case 'microsoft':
|
||||
case 'azure':
|
||||
return '微软 Azure OpenAI 服务';
|
||||
case 'meta':
|
||||
case 'llama':
|
||||
return 'Meta 大语言模型';
|
||||
case 'deepseek':
|
||||
return 'DeepSeek 语言模型';
|
||||
case 'zhipu':
|
||||
case 'glm':
|
||||
return 'GLM/ChatGLM 系列模型';
|
||||
case 'qwen':
|
||||
case 'tongyi':
|
||||
return '阿里云通义千问模型';
|
||||
case 'doubao':
|
||||
case 'bytedance':
|
||||
return '字节跳动豆包模型';
|
||||
case 'mistral':
|
||||
return 'Mistral 语言模型';
|
||||
case 'perplexity':
|
||||
return 'Perplexity 搜索与推理';
|
||||
case 'huggingface':
|
||||
case 'hf':
|
||||
return 'Hugging Face 模型库与推理';
|
||||
case 'stability':
|
||||
return 'Stability AI 生成模型';
|
||||
case 'xai':
|
||||
case 'grok':
|
||||
return 'xAI Grok 对话模型';
|
||||
case 'siliconcloud':
|
||||
case 'siliconflow':
|
||||
return '硅基流动模型服务';
|
||||
default:
|
||||
return 'AI 模型提供商';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
// 头部
|
||||
ModelServiceHeader(
|
||||
onSearch: _handleSearch,
|
||||
onAddNew: widget.onAddNew,
|
||||
onFilterChange: _handleFilterChange,
|
||||
),
|
||||
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: BlocListener<AiConfigBloc, AiConfigState>(
|
||||
listener: (context, state) {
|
||||
// 处理验证成功后的状态重置
|
||||
if (state.actionStatus == AiConfigActionStatus.success ||
|
||||
state.actionStatus == AiConfigActionStatus.error) {
|
||||
widget.editorStateManager.setModelOperationInProgress(false);
|
||||
|
||||
// 在操作成功后,标记需要刷新缓存
|
||||
if (state.actionStatus == AiConfigActionStatus.success) {
|
||||
_lastLoadTime = null; // 使缓存失效
|
||||
}
|
||||
}
|
||||
|
||||
// 显示操作结果提示 - 但排除API Key验证成功(由ai_config_form处理)
|
||||
if (state.actionStatus == AiConfigActionStatus.error &&
|
||||
state.actionErrorMessage != null) {
|
||||
TopToast.error(context, state.actionErrorMessage!);
|
||||
}
|
||||
// 注意:success状态的提示由具体的表单组件处理,避免重复提示
|
||||
},
|
||||
child: BlocBuilder<AiConfigBloc, AiConfigState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == AiConfigStatus.loading && state.configs.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.errorMessage != null && state.configs.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'加载失败',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_lastLoadTime = null; // 强制刷新
|
||||
_loadUserConfigs();
|
||||
},
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final filteredConfigs = _getFilteredConfigs(state.configs);
|
||||
final groupedConfigs = _groupConfigsByProvider(filteredConfigs);
|
||||
|
||||
if (groupedConfigs.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 48,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.4),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_searchQuery.isNotEmpty || _filterValue != 'all'
|
||||
? '没有找到匹配的模型服务'
|
||||
: '您还没有配置任何模型服务',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_searchQuery.isEmpty && _filterValue == 'all')
|
||||
ElevatedButton.icon(
|
||||
onPressed: widget.onAddNew,
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('添加模型服务'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: groupedConfigs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final provider = groupedConfigs.keys.elementAt(index);
|
||||
final configs = groupedConfigs[provider]!;
|
||||
final providerInfo = _getProviderInfo(provider);
|
||||
final isExpanded = _expandedProviders[provider] ?? true;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: ModelProviderGroupCard(
|
||||
provider: provider,
|
||||
providerName: providerInfo['name'],
|
||||
description: providerInfo['description'],
|
||||
icon: providerInfo['icon'],
|
||||
color: providerInfo['color'],
|
||||
configs: configs,
|
||||
isExpanded: isExpanded,
|
||||
onToggleExpanded: () => _handleToggleProvider(provider),
|
||||
onAddModel: () => _handleAddModel(provider),
|
||||
onSetDefault: _handleSetDefault,
|
||||
onValidate: _handleValidate,
|
||||
onEdit: _handleEdit,
|
||||
onDelete: _handleDelete,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/models/prompt_models.dart';
|
||||
|
||||
/// 优化结果视图组件
|
||||
class OptimizationResultView extends StatefulWidget {
|
||||
/// 原始内容
|
||||
final String original;
|
||||
|
||||
/// 优化后的内容
|
||||
final String optimized;
|
||||
|
||||
/// 优化区块
|
||||
final List<OptimizationSection> sections;
|
||||
|
||||
/// 统计信息
|
||||
final OptimizationStatistics statistics;
|
||||
|
||||
/// 接受全部优化的回调
|
||||
final VoidCallback onAccept;
|
||||
|
||||
/// 拒绝优化的回调
|
||||
final VoidCallback onReject;
|
||||
|
||||
/// 部分接受优化的回调(传入接受的区块索引列表)
|
||||
final Function(List<int>) onPartialAccept;
|
||||
|
||||
const OptimizationResultView({
|
||||
Key? key,
|
||||
required this.original,
|
||||
required this.optimized,
|
||||
required this.sections,
|
||||
required this.statistics,
|
||||
required this.onAccept,
|
||||
required this.onReject,
|
||||
required this.onPartialAccept,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<OptimizationResultView> createState() => _OptimizationResultViewState();
|
||||
}
|
||||
|
||||
class _OptimizationResultViewState extends State<OptimizationResultView> {
|
||||
/// 选择接受的区块索引
|
||||
final List<int> _selectedSections = [];
|
||||
|
||||
/// 显示模式:对比或单独显示
|
||||
bool _showDiff = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 初始默认选择所有修改的区块
|
||||
for (int i = 0; i < widget.sections.length; i++) {
|
||||
if (widget.sections[i].isModified) {
|
||||
_selectedSections.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题和统计信息
|
||||
_buildHeader(context),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 内容区域
|
||||
SizedBox(
|
||||
height: 300, // 固定高度,可滚动
|
||||
child: _showDiff
|
||||
? _buildDiffView(context)
|
||||
: _buildSideBySideView(context),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 底部操作按钮
|
||||
_buildBottomActions(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标题和统计信息
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
final stats = widget.statistics;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// 标题
|
||||
Icon(
|
||||
Icons.auto_awesome,
|
||||
size: 20,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'AI优化结果',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// 显示模式切换
|
||||
SegmentedButton<bool>(
|
||||
segments: const [
|
||||
ButtonSegment<bool>(
|
||||
value: true,
|
||||
label: Text('对比视图'),
|
||||
icon: Icon(Icons.compare_arrows),
|
||||
),
|
||||
ButtonSegment<bool>(
|
||||
value: false,
|
||||
label: Text('并排视图'),
|
||||
icon: Icon(Icons.view_week),
|
||||
),
|
||||
],
|
||||
selected: {_showDiff},
|
||||
onSelectionChanged: (Set<bool> selection) {
|
||||
setState(() {
|
||||
_showDiff = selection.first;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 统计信息
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'词数变化: ${stats.originalWordCount} → ${stats.optimizedWordCount}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'优化比例: ${(stats.changeRatio * 100).toInt()}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建对比视图
|
||||
Widget _buildDiffView(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: widget.sections.length,
|
||||
itemBuilder: (context, index) {
|
||||
final section = widget.sections[index];
|
||||
final isSelected = _selectedSections.contains(index);
|
||||
|
||||
// 未修改的区块,没有选择框
|
||||
if (section.isUnchanged) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
),
|
||||
child: Text(
|
||||
section.content,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 修改的区块,有选择框
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? theme.colorScheme.secondary
|
||||
: theme.colorScheme.outline,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 原始内容
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer.withOpacity(0.2),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.remove_circle_outline,
|
||||
size: 16,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
section.original ?? '',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 优化后内容
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer.withOpacity(0.2),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(4),
|
||||
bottomRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_circle_outline,
|
||||
size: 16,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
section.content,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 选择按钮
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
if (!_selectedSections.contains(index)) {
|
||||
_selectedSections.add(index);
|
||||
}
|
||||
} else {
|
||||
_selectedSections.remove(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建并排视图
|
||||
Widget _buildSideBySideView(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// 原始内容
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'原始内容',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(widget.original),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 优化后内容
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'优化后内容',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(widget.optimized),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建底部操作按钮
|
||||
Widget _buildBottomActions(BuildContext context) {
|
||||
final int totalModified = widget.sections.where((s) => s.isModified).length;
|
||||
final int selectedCount = _selectedSections.length;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 已选择区块计数
|
||||
Text(
|
||||
'已选择 $selectedCount / $totalModified 处修改',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
// 操作按钮
|
||||
Row(
|
||||
children: [
|
||||
// 拒绝按钮
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
label: const Text('拒绝'),
|
||||
onPressed: widget.onReject,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 接受所选按钮
|
||||
FilledButton.tonal(
|
||||
onPressed: selectedCount > 0
|
||||
? () => widget.onPartialAccept(_selectedSections)
|
||||
: null,
|
||||
child: const Text('接受所选'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 接受全部按钮
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.check, size: 16),
|
||||
label: const Text('接受全部'),
|
||||
onPressed: widget.onAccept,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
113
AINoval/lib/screens/settings/widgets/processing_indicator.dart
Normal file
113
AINoval/lib/screens/settings/widgets/processing_indicator.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 处理状态指示器组件
|
||||
class ProcessingIndicator extends StatelessWidget {
|
||||
/// 进度值(0.0-1.0)
|
||||
final double progress;
|
||||
|
||||
/// 取消操作回调
|
||||
final VoidCallback? onCancel;
|
||||
|
||||
const ProcessingIndicator({
|
||||
Key? key,
|
||||
this.progress = 0.0,
|
||||
this.onCancel,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final showProgress = progress > 0 && progress < 1.0;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 标题和进度指示
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'正在优化提示词模板...',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getStatusMessage(progress),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onCancel != null)
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.cancel, size: 16),
|
||||
label: const Text('取消'),
|
||||
onPressed: onCancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 进度条
|
||||
if (showProgress) ...[
|
||||
const SizedBox(height: 16),
|
||||
LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: theme.colorScheme.surfaceVariant,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(theme.colorScheme.primary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${(progress * 100).toInt()}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取状态消息
|
||||
String _getStatusMessage(double progress) {
|
||||
if (progress < 0.1) {
|
||||
return '正在分析提示词内容...';
|
||||
} else if (progress < 0.4) {
|
||||
return '生成优化建议中...';
|
||||
} else if (progress < 0.7) {
|
||||
return '应用语言模型增强中...';
|
||||
} else if (progress < 0.9) {
|
||||
return '润色和格式化内容...';
|
||||
} else {
|
||||
return '优化即将完成...';
|
||||
}
|
||||
}
|
||||
}
|
||||
171
AINoval/lib/screens/settings/widgets/provider_list.dart
Normal file
171
AINoval/lib/screens/settings/widgets/provider_list.dart
Normal file
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/widgets/common/app_search_field.dart';
|
||||
import '../../../config/provider_icons.dart';
|
||||
|
||||
/// 提供商列表组件
|
||||
/// 显示左侧的提供商列表,类似CherryStudio的UI
|
||||
class ProviderList extends StatelessWidget {
|
||||
const ProviderList({
|
||||
super.key,
|
||||
required this.providers,
|
||||
required this.selectedProvider,
|
||||
required this.onProviderSelected,
|
||||
});
|
||||
|
||||
final List<String> providers;
|
||||
final String? selectedProvider;
|
||||
final ValueChanged<String> onProviderSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withOpacity(0.1),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 搜索框
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AppSearchField(
|
||||
hintText: '搜索模型平台...',
|
||||
height: 34,
|
||||
borderRadius: 8,
|
||||
onChanged: (value) {
|
||||
// 实现搜索功能
|
||||
// 这里可以添加搜索逻辑
|
||||
},
|
||||
controller: TextEditingController(),
|
||||
),
|
||||
),
|
||||
|
||||
// 提供商列表
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
itemCount: providers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final provider = providers[index];
|
||||
final isSelected = provider == selectedProvider;
|
||||
|
||||
return _buildProviderItem(context, provider, isSelected);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 底部添加按钮
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 32,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
// 添加新提供商的逻辑
|
||||
},
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('添加', style: TextStyle(fontSize: 12)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProviderItem(BuildContext context, String provider, bool isSelected) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// 获取提供商图标
|
||||
Widget providerIcon = _getProviderIcon(provider);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? theme.colorScheme.primaryContainer.withOpacity(0.3)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
minLeadingWidth: 24,
|
||||
minVerticalPadding: 0,
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
leading: providerIcon,
|
||||
title: Text(
|
||||
_getProviderDisplayName(provider),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
onTap: () => onProviderSelected(provider),
|
||||
// 如果是OpenRouter,添加一个标签
|
||||
trailing: provider.toLowerCase() == 'openrouter'
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'启用',
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 获取提供商图标
|
||||
Widget _getProviderIcon(String provider) {
|
||||
final iconColor = ProviderIcons.getProviderColor(provider);
|
||||
|
||||
return Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: ProviderIcons.getProviderIconForContext(
|
||||
provider,
|
||||
iconSize: IconSize.small,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 获取提供商显示名称
|
||||
String _getProviderDisplayName(String provider) {
|
||||
return ProviderIcons.getProviderDisplayName(provider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 可搜索的模型下拉框
|
||||
/// 允许用户搜索和选择模型
|
||||
class SearchableModelDropdown extends StatefulWidget {
|
||||
const SearchableModelDropdown({
|
||||
super.key,
|
||||
required this.models,
|
||||
required this.onModelSelected,
|
||||
this.hintText = '搜索模型',
|
||||
});
|
||||
|
||||
final List<String> models;
|
||||
final ValueChanged<String> onModelSelected;
|
||||
final String hintText;
|
||||
|
||||
@override
|
||||
State<SearchableModelDropdown> createState() => _SearchableModelDropdownState();
|
||||
}
|
||||
|
||||
class _SearchableModelDropdownState extends State<SearchableModelDropdown> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
|
||||
OverlayEntry? _overlayEntry;
|
||||
String _searchText = '';
|
||||
bool _isDropdownOpen = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(_onSearchChanged);
|
||||
_focusNode.addListener(_onFocusChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.removeListener(_onSearchChanged);
|
||||
_searchController.dispose();
|
||||
_focusNode.removeListener(_onFocusChanged);
|
||||
_focusNode.dispose();
|
||||
_removeOverlay();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSearchChanged() {
|
||||
setState(() {
|
||||
_searchText = _searchController.text;
|
||||
if (_isDropdownOpen) {
|
||||
_updateOverlay();
|
||||
} else if (_searchText.isNotEmpty) {
|
||||
_showOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_showOverlay();
|
||||
} else {
|
||||
_removeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void _showOverlay() {
|
||||
if (_overlayEntry != null) {
|
||||
_removeOverlay();
|
||||
}
|
||||
|
||||
_isDropdownOpen = true;
|
||||
_overlayEntry = _createOverlayEntry();
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
void _updateOverlay() {
|
||||
_removeOverlay();
|
||||
_overlayEntry = _createOverlayEntry();
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
void _removeOverlay() {
|
||||
if (_overlayEntry != null) {
|
||||
_overlayEntry!.remove();
|
||||
_overlayEntry = null;
|
||||
_isDropdownOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
OverlayEntry _createOverlayEntry() {
|
||||
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
|
||||
return OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned(
|
||||
width: size.width,
|
||||
child: CompositedTransformFollower(
|
||||
link: _layerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: Offset(0, size.height + 4),
|
||||
child: Material(
|
||||
elevation: 3,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 250,
|
||||
minWidth: size.width,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: _buildDropdownList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDropdownList() {
|
||||
final filteredModels = widget.models
|
||||
.where((model) => model.toLowerCase().contains(_searchText.toLowerCase()))
|
||||
.toList();
|
||||
|
||||
if (filteredModels.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'没有找到匹配的模型',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
shrinkWrap: true,
|
||||
itemCount: filteredModels.length,
|
||||
itemBuilder: (context, index) {
|
||||
final model = filteredModels[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: Text(
|
||||
model,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
widget.onModelSelected(model);
|
||||
_searchController.clear();
|
||||
_removeOverlay();
|
||||
_focusNode.unfocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
focusNode: _focusNode,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).hintColor.withOpacity(0.7),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||||
? Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.3)
|
||||
: Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.7),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 模板权限指示器组件
|
||||
///
|
||||
/// 用于显示当前模板的类型(公共/私有)
|
||||
class TemplatePermissionIndicator extends StatelessWidget {
|
||||
/// 是否为公共模板
|
||||
final bool isPublic;
|
||||
|
||||
/// 复制到私有模板的回调(仅公共模板有效)
|
||||
final VoidCallback? onCopyToPrivate;
|
||||
|
||||
const TemplatePermissionIndicator({
|
||||
Key? key,
|
||||
required this.isPublic,
|
||||
this.onCopyToPrivate,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isPublic
|
||||
? theme.colorScheme.primary.withOpacity(0.1)
|
||||
: theme.colorScheme.secondary.withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: isPublic
|
||||
? theme.colorScheme.primary.withOpacity(0.2)
|
||||
: theme.colorScheme.secondary.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isPublic ? Icons.public : Icons.lock_outline,
|
||||
size: 16,
|
||||
color: isPublic
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isPublic ? '公共模板(只读)' : '私有模板',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isPublic
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (isPublic && onCopyToPrivate != null)
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.copy, size: 14),
|
||||
label: const Text('复制到我的模板'),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
minimumSize: const Size(120, 30),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed: onCopyToPrivate,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
537
AINoval/lib/screens/settings/widgets/user_preset_card.dart
Normal file
537
AINoval/lib/screens/settings/widgets/user_preset_card.dart
Normal file
@@ -0,0 +1,537 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/preset_models.dart';
|
||||
import '../../../utils/web_theme.dart';
|
||||
|
||||
/// 用户预设卡片组件
|
||||
class UserPresetCard extends StatelessWidget {
|
||||
final AIPromptPreset preset;
|
||||
final bool isSelected;
|
||||
final bool batchMode;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onEdit;
|
||||
final VoidCallback? onFavorite;
|
||||
final VoidCallback? onDelete;
|
||||
final VoidCallback? onUse;
|
||||
final ValueChanged<bool>? onSelectionChanged;
|
||||
|
||||
const UserPresetCard({
|
||||
Key? key,
|
||||
required this.preset,
|
||||
this.isSelected = false,
|
||||
this.batchMode = false,
|
||||
this.onTap,
|
||||
this.onEdit,
|
||||
this.onFavorite,
|
||||
this.onDelete,
|
||||
this.onUse,
|
||||
this.onSelectionChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
||||
: WebTheme.getCardColor(context),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildContent(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildFooter(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
if (batchMode) ...[
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) => onSelectionChanged?.call(value ?? false),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
preset.presetName ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildStatusIndicators(context),
|
||||
],
|
||||
),
|
||||
if (preset.presetDescription?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
preset.presetDescription!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.7),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (!batchMode) ...[
|
||||
// 使用按钮
|
||||
ElevatedButton.icon(
|
||||
onPressed: onUse,
|
||||
icon: const Icon(Icons.play_arrow, size: 16),
|
||||
label: const Text('使用'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(80, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) => _handleMenuAction(value),
|
||||
itemBuilder: (context) => _buildMenuItems(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIndicators(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (preset.isFavorite == true)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
size: 16,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
if (preset.isSystem == true)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.blue.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'系统',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (preset.isPublic == true)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.green.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'公开',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.green,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 系统提示词预览
|
||||
if ((preset.systemPrompt ?? '').isNotEmpty) ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'系统提示词:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
preset.systemPrompt ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: 'monospace',
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.8),
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// 用户提示词预览
|
||||
if ((preset.userPrompt ?? '').isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'用户提示词:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
preset.userPrompt ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: 'monospace',
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.8),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// 标签
|
||||
if ((preset.presetTags ?? const <String>[]).isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: (preset.presetTags ?? const <String>[])
|
||||
.map((tag) => _buildTag(context, tag))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(BuildContext context, String tag) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// 功能类型
|
||||
_buildFeatureTypeChip(context),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 创建时间
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 14,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatDateTime(preset.createdAt),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 使用次数
|
||||
Icon(
|
||||
Icons.play_circle_outline,
|
||||
size: 14,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'使用 ${preset.useCount ?? 0} 次',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// 最后使用时间
|
||||
if (preset.lastUsedAt != null) ...[
|
||||
Text(
|
||||
'最后使用: ${_formatDateTime(preset.lastUsedAt)}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
] else
|
||||
Text(
|
||||
'从未使用',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureTypeChip(BuildContext context) {
|
||||
final featureType = preset.aiFeatureType ?? 'UNKNOWN';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _getFeatureTypeColor(featureType).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: _getFeatureTypeColor(featureType).withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_getFeatureTypeLabel(featureType),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: _getFeatureTypeColor(featureType),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<PopupMenuEntry<String>> _buildMenuItems() {
|
||||
List<PopupMenuEntry<String>> items = [];
|
||||
|
||||
// 编辑选项(仅非系统预设可编辑)
|
||||
if (preset.isSystem != true) {
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('编辑'),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// 收藏选项
|
||||
if (preset.isFavorite != true) {
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'favorite',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.favorite_border, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('添加到收藏'),
|
||||
],
|
||||
),
|
||||
));
|
||||
} else {
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'unfavorite',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.favorite, size: 18, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('取消收藏'),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// 复制选项
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'duplicate',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.copy, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('复制预设'),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
// 导出选项
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'export',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.file_download, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('导出'),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
// 删除选项(仅非系统预设可删除)
|
||||
if (preset.isSystem != true) {
|
||||
items.add(const PopupMenuDivider());
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, size: 18, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('删除', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
Color _getFeatureTypeColor(String featureType) {
|
||||
switch (featureType) {
|
||||
case 'CHAT':
|
||||
return Colors.blue;
|
||||
case 'SCENE_GENERATION':
|
||||
return Colors.green;
|
||||
case 'CONTINUATION':
|
||||
return Colors.orange;
|
||||
case 'SUMMARY':
|
||||
return Colors.purple;
|
||||
case 'OUTLINE':
|
||||
return Colors.teal;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _getFeatureTypeLabel(String featureType) {
|
||||
switch (featureType) {
|
||||
case 'CHAT':
|
||||
return 'AI聊天';
|
||||
case 'SCENE_GENERATION':
|
||||
return '场景生成';
|
||||
case 'CONTINUATION':
|
||||
return '续写';
|
||||
case 'SUMMARY':
|
||||
return '总结';
|
||||
case 'OUTLINE':
|
||||
return '大纲';
|
||||
default:
|
||||
return featureType;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime? dateTime) {
|
||||
if (dateTime == null) return '';
|
||||
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inDays > 0) {
|
||||
return '${difference.inDays}天前';
|
||||
} else if (difference.inHours > 0) {
|
||||
return '${difference.inHours}小时前';
|
||||
} else if (difference.inMinutes > 0) {
|
||||
return '${difference.inMinutes}分钟前';
|
||||
} else {
|
||||
return '刚刚';
|
||||
}
|
||||
}
|
||||
|
||||
void _handleMenuAction(String action) {
|
||||
switch (action) {
|
||||
case 'edit':
|
||||
onEdit?.call();
|
||||
break;
|
||||
case 'favorite':
|
||||
case 'unfavorite':
|
||||
onFavorite?.call();
|
||||
break;
|
||||
case 'duplicate':
|
||||
// TODO: 实现复制预设功能
|
||||
break;
|
||||
case 'export':
|
||||
// TODO: 实现导出预设功能
|
||||
break;
|
||||
case 'delete':
|
||||
onDelete?.call();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/preset_models.dart';
|
||||
import '../../../services/ai_preset_service.dart';
|
||||
import '../../../utils/logger.dart';
|
||||
import '../../../widgets/common/loading_indicator.dart';
|
||||
import 'user_preset_card.dart';
|
||||
import 'add_user_preset_dialog.dart';
|
||||
import 'edit_user_preset_dialog.dart';
|
||||
|
||||
/// 用户预设管理面板
|
||||
class UserPresetManagementPanel extends StatefulWidget {
|
||||
const UserPresetManagementPanel({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<UserPresetManagementPanel> createState() => _UserPresetManagementPanelState();
|
||||
}
|
||||
|
||||
class _UserPresetManagementPanelState extends State<UserPresetManagementPanel>
|
||||
with TickerProviderStateMixin {
|
||||
final AIPresetService _presetService = AIPresetService();
|
||||
late TabController _tabController;
|
||||
|
||||
List<AIPromptPreset> _presets = [];
|
||||
List<AIPromptPreset> _selectedPresets = [];
|
||||
bool _isLoading = true;
|
||||
bool _batchMode = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
String _currentTab = 'ALL';
|
||||
|
||||
static const List<String> _tabs = ['ALL', 'CHAT', 'SCENE_GENERATION', 'CONTINUATION', 'SUMMARY', 'OUTLINE', 'FAVORITES'];
|
||||
static const Map<String, String> _tabLabels = {
|
||||
'ALL': '全部预设',
|
||||
'CHAT': 'AI聊天',
|
||||
'SCENE_GENERATION': '场景生成',
|
||||
'CONTINUATION': '续写',
|
||||
'SUMMARY': '总结',
|
||||
'OUTLINE': '大纲',
|
||||
'FAVORITES': '收藏夹',
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
_tabController.addListener(_onTabChanged);
|
||||
_loadPresets();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
setState(() {
|
||||
_currentTab = _tabs[_tabController.index];
|
||||
_selectedPresets.clear();
|
||||
_batchMode = false;
|
||||
});
|
||||
_loadPresets();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildTabBar(),
|
||||
Expanded(
|
||||
child: _buildContent(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.smart_button, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'我的预设库',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showAddPresetDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('新建预设'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
// 搜索框
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value;
|
||||
});
|
||||
_loadPresets();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索我的预设...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 批量操作开关
|
||||
if (_presets.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
label: Text('批量操作${_batchMode ? ' (${_selectedPresets.length})' : ''}'),
|
||||
selected: _batchMode,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_batchMode = selected;
|
||||
if (!selected) {
|
||||
_selectedPresets.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
// 批量操作按钮
|
||||
if (_batchMode && _selectedPresets.isNotEmpty) ...[
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) => _handleBatchAction(value),
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'favorite',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.favorite, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('添加到收藏'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'export',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.file_download, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('导出预设'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, size: 18, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('批量删除', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: null,
|
||||
icon: const Icon(Icons.more_vert),
|
||||
label: const Text('批量操作'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
// 导入按钮
|
||||
TextButton.icon(
|
||||
onPressed: _showImportDialog,
|
||||
icon: const Icon(Icons.file_upload),
|
||||
label: const Text('导入'),
|
||||
),
|
||||
|
||||
// 刷新按钮
|
||||
IconButton(
|
||||
onPressed: _loadPresets,
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: '刷新',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
tabs: _tabs.map((tab) => Tab(
|
||||
text: _tabLabels[tab],
|
||||
)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
if (_isLoading) {
|
||||
return const Center(child: LoadingIndicator());
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'加载失败',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_error!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _loadPresets,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_presets.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.smart_button,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无预设',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'创建您的第一个AI提示预设',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showAddPresetDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('新建预设'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TabBarView(
|
||||
controller: _tabController,
|
||||
children: _tabs.map((tab) => _buildPresetList()).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPresetList() {
|
||||
final filteredPresets = _getFilteredPresets();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView.builder(
|
||||
itemCount: filteredPresets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final preset = filteredPresets[index];
|
||||
return UserPresetCard(
|
||||
preset: preset,
|
||||
isSelected: _selectedPresets.contains(preset),
|
||||
batchMode: _batchMode,
|
||||
onTap: () => _onPresetCardTap(preset),
|
||||
onEdit: () => _showEditPresetDialog(preset),
|
||||
onFavorite: () => _togglePresetFavorite(preset),
|
||||
onDelete: () => _deletePreset(preset),
|
||||
onUse: () => _usePreset(preset),
|
||||
onSelectionChanged: (selected) => _onPresetSelectionChanged(preset, selected),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<AIPromptPreset> _getFilteredPresets() {
|
||||
List<AIPromptPreset> filteredPresets = List.from(_presets);
|
||||
|
||||
// 根据标签页筛选
|
||||
if (_currentTab != 'ALL') {
|
||||
if (_currentTab == 'FAVORITES') {
|
||||
filteredPresets = filteredPresets.where((p) => p.isFavorite == true).toList();
|
||||
} else {
|
||||
filteredPresets = filteredPresets.where((p) => p.aiFeatureType == _currentTab).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// 根据搜索条件筛选
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
filteredPresets = filteredPresets.where((preset) {
|
||||
final query = _searchQuery.toLowerCase();
|
||||
return (preset.presetName ?? '').toLowerCase().contains(query) ||
|
||||
(preset.presetDescription?.toLowerCase().contains(query) ?? false) ||
|
||||
((preset.systemPrompt ?? '').toLowerCase().contains(query)) ||
|
||||
((preset.userPrompt ?? '').toLowerCase().contains(query));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return filteredPresets;
|
||||
}
|
||||
|
||||
// 数据加载
|
||||
Future<void> _loadPresets() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final presets = await _presetService.getUserPresets(featureType: 'AI_CHAT');
|
||||
|
||||
setState(() {
|
||||
_presets = presets;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.error('加载用户预设失败', e.toString());
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
void _onPresetCardTap(AIPromptPreset preset) {
|
||||
if (_batchMode) {
|
||||
_onPresetSelectionChanged(preset, !_selectedPresets.contains(preset));
|
||||
} else {
|
||||
_showPresetDetails(preset);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPresetSelectionChanged(AIPromptPreset preset, bool selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedPresets.add(preset);
|
||||
} else {
|
||||
_selectedPresets.remove(preset);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 对话框显示
|
||||
void _showAddPresetDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddUserPresetDialog(
|
||||
onSuccess: _loadPresets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditPresetDialog(AIPromptPreset preset) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => EditUserPresetDialog(
|
||||
preset: preset,
|
||||
onSuccess: _loadPresets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPresetDetails(AIPromptPreset preset) {
|
||||
// TODO: 实现预设详情对话框
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('查看预设详情: ${preset.presetName}')),
|
||||
);
|
||||
}
|
||||
|
||||
void _showImportDialog() {
|
||||
// TODO: 实现导入预设对话框
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('导入功能开发中...')),
|
||||
);
|
||||
}
|
||||
|
||||
// 操作方法
|
||||
Future<void> _togglePresetFavorite(AIPromptPreset preset) async {
|
||||
try {
|
||||
await _presetService.toggleFavorite(preset.presetId);
|
||||
|
||||
final action = preset.isFavorite ? '取消收藏' : '添加到收藏';
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('预设 "${preset.presetName}" $action成功')),
|
||||
);
|
||||
_loadPresets();
|
||||
} catch (e) {
|
||||
AppLogger.error('收藏操作失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('收藏操作失败: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deletePreset(AIPromptPreset preset) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确定要删除预设 "${preset.presetName}" 吗?此操作不可撤销。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
await _presetService.deletePreset(preset.presetId);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('预设 "${preset.presetName}" 删除成功')),
|
||||
);
|
||||
_loadPresets();
|
||||
} catch (e) {
|
||||
AppLogger.error('删除预设失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('删除失败: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _usePreset(AIPromptPreset preset) {
|
||||
// TODO: 实现使用预设功能,跳转到对应的AI功能页面
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('使用预设: ${preset.presetName}')),
|
||||
);
|
||||
}
|
||||
|
||||
// 批量操作
|
||||
Future<void> _handleBatchAction(String action) async {
|
||||
if (_selectedPresets.isEmpty) return;
|
||||
|
||||
switch (action) {
|
||||
case 'favorite':
|
||||
await _batchFavoritePresets();
|
||||
break;
|
||||
case 'export':
|
||||
await _batchExportPresets();
|
||||
break;
|
||||
case 'delete':
|
||||
await _batchDeletePresets();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _batchFavoritePresets() async {
|
||||
try {
|
||||
for (final preset in _selectedPresets) {
|
||||
await _presetService.toggleFavorite(preset.presetId);
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('已收藏 ${_selectedPresets.length} 个预设')),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_selectedPresets.clear();
|
||||
_batchMode = false;
|
||||
});
|
||||
_loadPresets();
|
||||
} catch (e) {
|
||||
AppLogger.error('批量收藏预设失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('批量收藏失败: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _batchExportPresets() async {
|
||||
try {
|
||||
// TODO: 实现批量导出功能
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('导出 ${_selectedPresets.length} 个预设功能开发中...')),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_selectedPresets.clear();
|
||||
_batchMode = false;
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.error('批量导出预设失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('批量导出失败: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _batchDeletePresets() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认批量删除'),
|
||||
content: Text('确定要删除选中的 ${_selectedPresets.length} 个预设吗?此操作不可撤销。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
for (final preset in _selectedPresets) {
|
||||
await _presetService.deletePreset(preset.presetId);
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('已删除 ${_selectedPresets.length} 个预设')),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_selectedPresets.clear();
|
||||
_batchMode = false;
|
||||
});
|
||||
_loadPresets();
|
||||
} catch (e) {
|
||||
AppLogger.error('批量删除预设失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('批量删除失败: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
479
AINoval/lib/screens/settings/widgets/user_template_card.dart
Normal file
479
AINoval/lib/screens/settings/widgets/user_template_card.dart
Normal file
@@ -0,0 +1,479 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/prompt_models.dart';
|
||||
import '../../../utils/web_theme.dart';
|
||||
|
||||
/// 用户模板卡片组件
|
||||
class UserTemplateCard extends StatelessWidget {
|
||||
final PromptTemplate template;
|
||||
final bool isSelected;
|
||||
final bool batchMode;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onEdit;
|
||||
final VoidCallback? onShare;
|
||||
final VoidCallback? onFavorite;
|
||||
final VoidCallback? onDelete;
|
||||
final ValueChanged<bool>? onSelectionChanged;
|
||||
|
||||
UserTemplateCard({
|
||||
Key? key,
|
||||
required this.template,
|
||||
this.isSelected = false,
|
||||
this.batchMode = false,
|
||||
this.onTap,
|
||||
this.onEdit,
|
||||
this.onShare,
|
||||
this.onFavorite,
|
||||
this.onDelete,
|
||||
this.onSelectionChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
color: isSelected
|
||||
? WebTheme.getPrimaryColor(context).withOpacity(0.1)
|
||||
: WebTheme.getCardColor(context),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildContent(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildFooter(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
if (batchMode) ...[
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) => onSelectionChanged?.call(value ?? false),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
template.name,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildStatusIndicators(context),
|
||||
],
|
||||
),
|
||||
if ((template.description ?? '').isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
template.description ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.7),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (!batchMode) ...[
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) => _handleMenuAction(value),
|
||||
itemBuilder: (context) => _buildMenuItems(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIndicators(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (template.isFavorite == true)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
size: 16,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
if (template.isPublic == true)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'已分享',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (template.isPublic == false)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
child: Icon(
|
||||
Icons.lock,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 模板内容预览
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
template.content,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: 'monospace',
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.8),
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
if ((template.templateTags ?? const <String>[]).isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: (template.templateTags ?? const <String>[])
|
||||
.map((tag) => _buildTag(context, tag))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(BuildContext context, String tag) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// 功能类型
|
||||
if (template.aiFeatureType != null) ...[
|
||||
_buildFeatureTypeChip(context),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
|
||||
// 创建时间
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 14,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatDateTime(template.createdAt),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 使用次数
|
||||
Icon(
|
||||
Icons.play_circle_outline,
|
||||
size: 14,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'使用 ${template.useCount ?? 0} 次',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getTextColor(context).withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// 版本信息
|
||||
// 版本信息已移除(PromptTemplate 无版本字段)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureTypeChip(BuildContext context) {
|
||||
final featureType = template.aiFeatureType!;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _getFeatureTypeColor(featureType).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: _getFeatureTypeColor(featureType).withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_getFeatureTypeLabel(featureType),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: _getFeatureTypeColor(featureType),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<PopupMenuEntry<String>> _buildMenuItems() {
|
||||
List<PopupMenuEntry<String>> items = [];
|
||||
|
||||
// 编辑选项
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('编辑'),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
// 收藏选项
|
||||
if (template.isFavorite != true) {
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'favorite',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.favorite_border, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('添加到收藏'),
|
||||
],
|
||||
),
|
||||
));
|
||||
} else {
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'unfavorite',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.favorite, size: 18, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('取消收藏'),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// 分享选项
|
||||
if (template.isPublic != true) {
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'share',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.share, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('分享到社区'),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
items.add(const PopupMenuDivider());
|
||||
|
||||
// 删除选项
|
||||
items.add(const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, size: 18, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('删除', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
Color _getFeatureTypeColor(AIFeatureType featureType) {
|
||||
final scheme = Theme.of(_cachedContext!).colorScheme;
|
||||
switch (featureType) {
|
||||
case AIFeatureType.aiChat:
|
||||
return scheme.primary;
|
||||
case AIFeatureType.novelGeneration:
|
||||
return scheme.secondary;
|
||||
case AIFeatureType.novelCompose:
|
||||
return scheme.secondary; // 与内容生成保持一致的视觉语义
|
||||
case AIFeatureType.textExpansion:
|
||||
return scheme.tertiary;
|
||||
case AIFeatureType.textRefactor:
|
||||
return scheme.primary;
|
||||
case AIFeatureType.textSummary:
|
||||
return scheme.secondary;
|
||||
case AIFeatureType.sceneToSummary:
|
||||
return scheme.tertiary;
|
||||
case AIFeatureType.summaryToScene:
|
||||
return scheme.primary;
|
||||
case AIFeatureType.professionalFictionContinuation:
|
||||
return scheme.primary;
|
||||
case AIFeatureType.sceneBeatGeneration:
|
||||
return scheme.secondary;
|
||||
case AIFeatureType.settingTreeGeneration:
|
||||
return scheme.tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
String _getFeatureTypeLabel(AIFeatureType featureType) {
|
||||
switch (featureType) {
|
||||
case AIFeatureType.aiChat:
|
||||
return 'AI聊天';
|
||||
case AIFeatureType.novelGeneration:
|
||||
return '场景生成';
|
||||
case AIFeatureType.novelCompose:
|
||||
return '设定编排';
|
||||
case AIFeatureType.textExpansion:
|
||||
return '扩写';
|
||||
case AIFeatureType.textRefactor:
|
||||
return '重构';
|
||||
case AIFeatureType.textSummary:
|
||||
return '总结';
|
||||
case AIFeatureType.sceneToSummary:
|
||||
return '场景转摘要';
|
||||
case AIFeatureType.summaryToScene:
|
||||
return '摘要转场景';
|
||||
case AIFeatureType.professionalFictionContinuation:
|
||||
return '专业续写';
|
||||
case AIFeatureType.sceneBeatGeneration:
|
||||
return '场景节拍';
|
||||
case AIFeatureType.settingTreeGeneration:
|
||||
return '设定树生成';
|
||||
}
|
||||
}
|
||||
|
||||
// 为了在私有方法中访问 theme,缓存一次 context(仅在 build 调用期间有效)
|
||||
final BuildContext? _cachedContext = null;
|
||||
|
||||
Widget _buildCard(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
||||
: WebTheme.getCardColor(context),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildContent(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildFooter(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime? dateTime) {
|
||||
if (dateTime == null) return '';
|
||||
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inDays > 0) {
|
||||
return '${difference.inDays}天前';
|
||||
} else if (difference.inHours > 0) {
|
||||
return '${difference.inHours}小时前';
|
||||
} else if (difference.inMinutes > 0) {
|
||||
return '${difference.inMinutes}分钟前';
|
||||
} else {
|
||||
return '刚刚';
|
||||
}
|
||||
}
|
||||
|
||||
void _handleMenuAction(String action) {
|
||||
switch (action) {
|
||||
case 'edit':
|
||||
onEdit?.call();
|
||||
break;
|
||||
case 'favorite':
|
||||
case 'unfavorite':
|
||||
onFavorite?.call();
|
||||
break;
|
||||
case 'share':
|
||||
onShare?.call();
|
||||
break;
|
||||
case 'delete':
|
||||
onDelete?.call();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/prompt_models.dart';
|
||||
import '../../../services/api_service/base/api_client.dart';
|
||||
import '../../../services/api_service/repositories/impl/prompt_repository_impl.dart';
|
||||
import '../../../utils/logger.dart';
|
||||
import '../../../widgets/common/loading_indicator.dart';
|
||||
import 'user_template_card.dart';
|
||||
import 'add_user_template_dialog.dart';
|
||||
import 'edit_user_template_dialog.dart';
|
||||
|
||||
/// 用户模板管理面板
|
||||
class UserTemplateManagementPanel extends StatefulWidget {
|
||||
const UserTemplateManagementPanel({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<UserTemplateManagementPanel> createState() => _UserTemplateManagementPanelState();
|
||||
}
|
||||
|
||||
class _UserTemplateManagementPanelState extends State<UserTemplateManagementPanel>
|
||||
with TickerProviderStateMixin {
|
||||
final PromptRepositoryImpl _promptRepository = PromptRepositoryImpl(ApiClient());
|
||||
late TabController _tabController;
|
||||
|
||||
List<PromptTemplate> _templates = [];
|
||||
List<PromptTemplate> _selectedTemplates = [];
|
||||
bool _isLoading = true;
|
||||
bool _batchMode = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
String _currentTab = 'ALL';
|
||||
|
||||
static const List<String> _tabs = ['ALL', 'PRIVATE', 'SHARED', 'FAVORITES'];
|
||||
static const Map<String, String> _tabLabels = {
|
||||
'ALL': '全部模板',
|
||||
'PRIVATE': '私有模板',
|
||||
'SHARED': '已分享',
|
||||
'FAVORITES': '收藏夹',
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
_tabController.addListener(_onTabChanged);
|
||||
_loadTemplates();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
setState(() {
|
||||
_currentTab = _tabs[_tabController.index];
|
||||
_selectedTemplates.clear();
|
||||
_batchMode = false;
|
||||
});
|
||||
_loadTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildTabBar(),
|
||||
Expanded(
|
||||
child: _buildContent(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.article_outlined, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'我的模板库',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showAddTemplateDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('新建模板'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
// 搜索框
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value;
|
||||
});
|
||||
_loadTemplates();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索我的模板...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 批量操作开关
|
||||
if (_templates.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
label: Text('批量操作${_batchMode ? ' (${_selectedTemplates.length})' : ''}'),
|
||||
selected: _batchMode,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_batchMode = selected;
|
||||
if (!selected) {
|
||||
_selectedTemplates.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
// 批量操作按钮
|
||||
if (_batchMode && _selectedTemplates.isNotEmpty) ...[
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) => _handleBatchAction(value),
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'share',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.share, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('批量分享'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'favorite',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.favorite, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('添加到收藏'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, size: 18, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('批量删除', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: null,
|
||||
icon: const Icon(Icons.more_vert),
|
||||
label: const Text('批量操作'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
// 刷新按钮
|
||||
IconButton(
|
||||
onPressed: _loadTemplates,
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: '刷新',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
tabs: _tabs.map((tab) => Tab(
|
||||
text: _tabLabels[tab],
|
||||
)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
if (_isLoading) {
|
||||
return const Center(child: LoadingIndicator());
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'加载失败',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_error!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _loadTemplates,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_templates.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.article_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无模板',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'创建您的第一个提示词模板',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showAddTemplateDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('新建模板'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TabBarView(
|
||||
controller: _tabController,
|
||||
children: _tabs.map((tab) => _buildTemplateList()).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTemplateList() {
|
||||
final filteredTemplates = _getFilteredTemplates();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView.builder(
|
||||
itemCount: filteredTemplates.length,
|
||||
itemBuilder: (context, index) {
|
||||
final template = filteredTemplates[index];
|
||||
return UserTemplateCard(
|
||||
template: template,
|
||||
isSelected: _selectedTemplates.contains(template),
|
||||
batchMode: _batchMode,
|
||||
onTap: () => _onTemplateCardTap(template),
|
||||
onEdit: () => _showEditTemplateDialog(template),
|
||||
onShare: () => _shareTemplate(template),
|
||||
onFavorite: () => _toggleTemplateFavorite(template),
|
||||
onDelete: () => _deleteTemplate(template),
|
||||
onSelectionChanged: (selected) => _onTemplateSelectionChanged(template, selected),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<PromptTemplate> _getFilteredTemplates() {
|
||||
List<PromptTemplate> filteredTemplates = List.from(_templates);
|
||||
|
||||
// 根据标签页筛选
|
||||
switch (_currentTab) {
|
||||
case 'PRIVATE':
|
||||
filteredTemplates = filteredTemplates.where((t) => t.isPublic == false).toList();
|
||||
break;
|
||||
case 'SHARED':
|
||||
filteredTemplates = filteredTemplates.where((t) => t.isPublic == true).toList();
|
||||
break;
|
||||
case 'FAVORITES':
|
||||
filteredTemplates = filteredTemplates.where((t) => t.isFavorite == true).toList();
|
||||
break;
|
||||
}
|
||||
|
||||
// 根据搜索条件筛选
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
filteredTemplates = filteredTemplates.where((template) {
|
||||
final query = _searchQuery.toLowerCase();
|
||||
return template.name.toLowerCase().contains(query) ||
|
||||
((template.description ?? '').toLowerCase().contains(query)) ||
|
||||
(template.content.toLowerCase().contains(query));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return filteredTemplates;
|
||||
}
|
||||
|
||||
// 数据加载
|
||||
Future<void> _loadTemplates() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
// 仓库当前不支持按搜索服务端筛选,这里拉取全部再前端过滤
|
||||
final templates = await _promptRepository.getPromptTemplates();
|
||||
|
||||
setState(() {
|
||||
_templates = templates;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
AppLogger.error('加载用户模板失败', e.toString());
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
void _onTemplateCardTap(PromptTemplate template) {
|
||||
if (_batchMode) {
|
||||
_onTemplateSelectionChanged(template, !_selectedTemplates.contains(template));
|
||||
} else {
|
||||
_showTemplateDetails(template);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTemplateSelectionChanged(PromptTemplate template, bool selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedTemplates.add(template);
|
||||
} else {
|
||||
_selectedTemplates.remove(template);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 对话框显示
|
||||
void _showAddTemplateDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddUserTemplateDialog(
|
||||
onSuccess: _loadTemplates,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditTemplateDialog(PromptTemplate template) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => EditUserTemplateDialog(
|
||||
template: template,
|
||||
onSuccess: _loadTemplates,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTemplateDetails(PromptTemplate template) {
|
||||
// TODO: 实现模板详情对话框
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('查看模板详情: ${template.name}')),
|
||||
);
|
||||
}
|
||||
|
||||
// 操作方法
|
||||
Future<void> _shareTemplate(PromptTemplate template) async {
|
||||
// 当前仓库未提供分享接口,占位提示
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('分享功能暂未实现')),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _toggleTemplateFavorite(PromptTemplate template) async {
|
||||
try {
|
||||
final updated = await _promptRepository.toggleTemplateFavorite(template);
|
||||
final action = updated.isFavorite ? '添加到收藏' : '取消收藏';
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${updated.name} $action')),
|
||||
);
|
||||
_loadTemplates();
|
||||
} catch (e) {
|
||||
AppLogger.error('切换模板收藏状态失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('操作失败: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteTemplate(PromptTemplate template) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确定要删除模板 "${template.name}" 吗?此操作不可撤销。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
await _promptRepository.deletePromptTemplate(template.id);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('模板 "${template.name}" 删除成功')),
|
||||
);
|
||||
_loadTemplates();
|
||||
} catch (e) {
|
||||
AppLogger.error('删除模板失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('删除失败: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量操作
|
||||
Future<void> _handleBatchAction(String action) async {
|
||||
if (_selectedTemplates.isEmpty) return;
|
||||
|
||||
switch (action) {
|
||||
case 'share':
|
||||
await _batchShareTemplates();
|
||||
break;
|
||||
case 'favorite':
|
||||
await _batchFavoriteTemplates();
|
||||
break;
|
||||
case 'delete':
|
||||
await _batchDeleteTemplates();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _batchShareTemplates() async {
|
||||
try {
|
||||
// 当前仓库未提供分享接口
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('成功分享 ${_selectedTemplates.length} 个模板')),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_selectedTemplates.clear();
|
||||
_batchMode = false;
|
||||
});
|
||||
_loadTemplates();
|
||||
} catch (e) {
|
||||
AppLogger.error('批量分享模板失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('批量分享失败: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _batchFavoriteTemplates() async {
|
||||
try {
|
||||
for (final template in _selectedTemplates) {
|
||||
if (!template.isFavorite) {
|
||||
await _promptRepository.toggleTemplateFavorite(template);
|
||||
}
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('成功添加 ${_selectedTemplates.length} 个模板到收藏')),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_selectedTemplates.clear();
|
||||
_batchMode = false;
|
||||
});
|
||||
_loadTemplates();
|
||||
} catch (e) {
|
||||
AppLogger.error('批量收藏模板失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('批量收藏失败: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _batchDeleteTemplates() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认批量删除'),
|
||||
content: Text('确定要删除选中的 ${_selectedTemplates.length} 个模板吗?此操作不可撤销。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
for (final template in _selectedTemplates) {
|
||||
await _promptRepository.deletePromptTemplate(template.id);
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('成功删除 ${_selectedTemplates.length} 个模板')),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_selectedTemplates.clear();
|
||||
_batchMode = false;
|
||||
});
|
||||
_loadTemplates();
|
||||
} catch (e) {
|
||||
AppLogger.error('批量删除模板失败', e.toString());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('批量删除失败: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user