马良AI写作初始化仓库

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

View File

@@ -0,0 +1,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;
}
}

View 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;
});
}
}
}
// 旧的图标/颜色映射方法已不再使用,移除以清理警告
}

View File

@@ -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;
});
}
}
}
}

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

File diff suppressed because it is too large Load Diff

View 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('确认添加'),
),
],
);
}
}

View File

@@ -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 '刚刚';
}
}
}

View File

@@ -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 '刚刚';
}
}
}

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

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

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

View File

@@ -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',
),
],
),
);
}
}

View 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, // 否则高亮
),
),
),
),
),
],
),
),
],
),
);
}
}

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

View File

@@ -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,
),
);
},
);
},
),
),
),
],
),
);
}
}

View File

@@ -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,
),
],
),
],
);
}
}

View 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 '优化即将完成...';
}
}
}

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

View File

@@ -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,
),
),
);
}
}

View File

@@ -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,
),
],
),
);
}
}

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

View File

@@ -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()}')),
);
}
}
}

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

View File

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