Files
MaliangAINovalWriter/AINoval/lib/screens/admin/public_templates_management_screen.dart
2025-09-10 00:07:52 +08:00

874 lines
27 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

import 'package:flutter/material.dart';
import 'dart:async';
import '../../models/prompt_models.dart';
import '../../services/api_service/repositories/impl/admin_repository_impl.dart';
import '../../utils/logger.dart';
import '../../services/api_service/repositories/impl/admin_repository_templates_extension.dart';
import '../../widgets/common/loading_indicator.dart';
import 'widgets/public_template_card.dart';
import 'widgets/add_official_template_dialog.dart';
import 'widgets/template_statistics_dialog.dart';
import 'widgets/template_details_dialog.dart'; // Added import for TemplateDetailsDialog
import 'widgets/edit_template_dialog.dart';
/// 公共模板管理页面
class PublicTemplatesManagementScreen extends StatefulWidget {
const PublicTemplatesManagementScreen({Key? key}) : super(key: key);
@override
State<PublicTemplatesManagementScreen> createState() => _PublicTemplatesManagementScreenState();
}
class _PublicTemplatesManagementScreenState extends State<PublicTemplatesManagementScreen> {
@override
Widget build(BuildContext context) {
return const PublicTemplatesManagementBody();
}
}
/// 公共模板管理页面主体
class PublicTemplatesManagementBody extends StatefulWidget {
const PublicTemplatesManagementBody({Key? key}) : super(key: key);
@override
State<PublicTemplatesManagementBody> createState() => _PublicTemplatesManagementBodyState();
}
class _PublicTemplatesManagementBodyState extends State<PublicTemplatesManagementBody>
with TickerProviderStateMixin {
final AdminRepositoryImpl _adminRepository = AdminRepositoryImpl();
late TabController _tabController;
List<PromptTemplate> _templates = [];
List<PromptTemplate> _selectedTemplates = [];
bool _isLoading = true;
bool _batchMode = false;
String? _error;
String _searchQuery = '';
String _currentTab = 'ALL';
AIFeatureType? _filterFeatureType;
bool? _filterVerified;
bool? _filterIsPublic;
String _sortOption = 'LATEST';
int _pageSize = 30;
int _currentPage = 1;
Timer? _searchDebounce;
static const List<String> _tabs = ['ALL', 'OFFICIAL', 'USER_SUBMITTED', 'PENDING_REVIEW'];
static const Map<String, String> _tabLabels = {
'ALL': '全部模板',
'OFFICIAL': '官方模板',
'USER_SUBMITTED': '用户提交',
'PENDING_REVIEW': '待审核',
};
@override
void initState() {
super.initState();
_tabController = TabController(length: _tabs.length, vsync: this);
_tabController.addListener(_onTabChanged);
_loadTemplates();
}
@override
void dispose() {
_searchDebounce?.cancel();
_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 Scaffold(
body: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1600),
child: 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: Row(
children: [
// 搜索框
Expanded(
flex: 3,
child: TextField(
onChanged: (value) {
_searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 400), () {
setState(() {
_searchQuery = value;
_currentPage = 1;
});
_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),
// 功能类型筛选
SizedBox(
width: 280,
child: DropdownButtonFormField<AIFeatureType?>(
value: _filterFeatureType,
decoration: InputDecoration(
labelText: '功能类型',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: [
const DropdownMenuItem<AIFeatureType?>(
value: null,
child: Text('全部类型'),
),
..._buildFeatureTypeOptions(),
],
onChanged: (value) {
setState(() {
_filterFeatureType = value;
_currentPage = 1;
});
},
),
),
const SizedBox(width: 12),
SizedBox(
width: 160,
child: DropdownButtonFormField<bool?>(
value: _filterVerified,
decoration: InputDecoration(
labelText: '认证',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: const [
DropdownMenuItem<bool?>(value: null, child: Text('全部')),
DropdownMenuItem<bool?>(value: true, child: Text('认证')),
DropdownMenuItem<bool?>(value: false, child: Text('未认证')),
],
onChanged: (v) {
setState(() {
_filterVerified = v;
_currentPage = 1;
});
},
),
),
const SizedBox(width: 12),
SizedBox(
width: 160,
child: DropdownButtonFormField<bool?>(
value: _filterIsPublic,
decoration: InputDecoration(
labelText: '可见性',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: const [
DropdownMenuItem<bool?>(value: null, child: Text('全部')),
DropdownMenuItem<bool?>(value: true, child: Text('公开')),
DropdownMenuItem<bool?>(value: false, child: Text('私有')),
],
onChanged: (v) {
setState(() {
_filterIsPublic = v;
_currentPage = 1;
});
},
),
),
const SizedBox(width: 12),
SizedBox(
width: 180,
child: DropdownButtonFormField<String>(
value: _sortOption,
decoration: InputDecoration(
labelText: '排序',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: const [
DropdownMenuItem(value: 'LATEST', child: Text('最新')),
DropdownMenuItem(value: 'MOST_USED', child: Text('使用最多')),
DropdownMenuItem(value: 'RATING', child: Text('评分最高')),
],
onChanged: (v) {
setState(() {
_sortOption = v ?? 'LATEST';
_currentPage = 1;
});
},
),
),
// 批量操作开关
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) ...[
ElevatedButton.icon(
onPressed: _batchPublish,
icon: const Icon(Icons.publish),
label: const Text('批量发布'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _batchSetVerified,
icon: const Icon(Icons.verified),
label: const Text('批量认证'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
const SizedBox(width: 8),
],
// 添加官方模板按钮
ElevatedButton.icon(
onPressed: _showAddOfficialTemplateDialog,
icon: const Icon(Icons.add),
label: const Text('添加官方模板'),
),
const SizedBox(width: 8),
// 刷新按钮
IconButton(
onPressed: _loadTemplates,
icon: const Icon(Icons.refresh),
tooltip: '刷新',
),
// 统计按钮
IconButton(
onPressed: _showStatisticsDialog,
icon: const Icon(Icons.analytics),
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: _showAddOfficialTemplateDialog,
icon: const Icon(Icons.add),
label: const Text('添加官方模板'),
),
],
),
);
}
return TabBarView(
controller: _tabController,
children: _tabs.map((tab) => _buildTemplateList()).toList(),
);
}
Widget _buildTemplateList() {
final filteredTemplates = _getFilteredTemplates();
final visibleCount = (_currentPage * _pageSize).clamp(0, filteredTemplates.length);
final items = filteredTemplates.take(visibleCount).toList();
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final template = items[index];
return PublicTemplateCard(
template: template,
isSelected: _selectedTemplates.contains(template),
batchMode: _batchMode,
onTap: () => _onTemplateCardTap(template),
onEdit: () => _showEditTemplateDialog(template),
onDuplicate: () => _duplicatePublicTemplate(template),
onReview: () => _showTemplateReviewDialog(template),
onPublish: () => _publishTemplate(template),
onSetVerified: () => _setTemplateVerified(template),
onDelete: () => _deleteTemplate(template),
onSelectionChanged: (selected) => _onTemplateSelectionChanged(template, selected),
);
},
),
),
if (visibleCount < filteredTemplates.length)
Padding(
padding: const EdgeInsets.only(top: 12),
child: OutlinedButton.icon(
onPressed: () {
setState(() {
_currentPage += 1;
});
},
icon: const Icon(Icons.expand_more),
label: Text('加载更多(${filteredTemplates.length - visibleCount}'),
),
),
],
),
);
}
Future<void> _duplicatePublicTemplate(PromptTemplate template) async {
final controller = TextEditingController(text: '${template.name} (复制)');
final newName = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('复制模板'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
labelText: '新模板名称',
border: OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(null),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
child: const Text('确定'),
),
],
),
);
if (newName == null || newName.isEmpty) return;
try {
final now = DateTime.now();
// 使用增强模板模型创建,以便包含 userId 等关键字段
final enhanced = _convertToEnhancedTemplate(template).copyWith(
id: '',
name: newName,
createdAt: now,
updatedAt: now,
usageCount: 0,
favoriteCount: 0,
isFavorite: false,
isDefault: false,
shareCode: null,
// 复制来源
authorId: template.authorId ?? (template.isPublic ? 'system' : null),
);
await _adminRepository.createOfficialEnhancedTemplate(enhanced);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已复制为新模板: ${enhanced.name}')),
);
_loadTemplates();
}
} catch (e) {
AppLogger.e('PublicTemplatesManagement', '复制模板失败', e);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('复制失败: $e')),
);
}
}
}
List<PromptTemplate> _getFilteredTemplates() {
List<PromptTemplate> filteredTemplates = List.from(_templates);
// 注意标签页筛选现在主要在API调用层面完成
// OFFICIAL -> getVerifiedTemplates() 获取已验证模板
// USER_SUBMITTED -> getPublicTemplates() 获取所有公共模板(用户提交的)
// PENDING_REVIEW -> getPendingTemplates() 获取待审核模板
// ALL -> getPublicTemplates() 获取所有公共模板
// 根据搜索条件筛选
if (_searchQuery.isNotEmpty) {
filteredTemplates = filteredTemplates.where((template) {
final query = _searchQuery.toLowerCase();
return template.name.toLowerCase().contains(query) ||
(template.description?.toLowerCase().contains(query) ?? false);
}).toList();
}
// 功能类型筛选
if (_filterFeatureType != null) {
filteredTemplates = filteredTemplates
.where((t) => t.featureType == _filterFeatureType)
.toList();
}
// 认证筛选
if (_filterVerified != null) {
filteredTemplates = filteredTemplates.where((t) => t.isVerified == _filterVerified).toList();
}
// 可见性筛选
if (_filterIsPublic != null) {
filteredTemplates = filteredTemplates.where((t) => t.isPublic == _filterIsPublic).toList();
}
// 排序
switch (_sortOption) {
case 'MOST_USED':
filteredTemplates.sort((a, b) => (b.useCount ?? 0).compareTo(a.useCount ?? 0));
break;
case 'RATING':
filteredTemplates.sort((a, b) => (b.averageRating ?? 0).compareTo(a.averageRating ?? 0));
break;
case 'LATEST':
default:
filteredTemplates.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
break;
}
return filteredTemplates;
}
List<DropdownMenuItem<AIFeatureType>> _buildFeatureTypeOptions() {
final Set<AIFeatureType> featureTypes =
_templates.map((t) => t.featureType).toSet();
final Map<AIFeatureType, String> labels = {
AIFeatureType.textExpansion: '文本扩写',
AIFeatureType.textRefactor: '文本润色',
AIFeatureType.textSummary: '文本总结',
AIFeatureType.sceneToSummary: '场景转摘要',
AIFeatureType.summaryToScene: '摘要转场景',
AIFeatureType.aiChat: 'AI对话',
AIFeatureType.novelGeneration: '小说生成',
AIFeatureType.professionalFictionContinuation: '专业续写',
AIFeatureType.sceneBeatGeneration: '场景节拍生成',
};
final List<AIFeatureType> sorted = featureTypes.toList()
..sort((a, b) => (labels[a] ?? a.name).compareTo(labels[b] ?? b.name));
return sorted
.map((ft) => DropdownMenuItem<AIFeatureType>(
value: ft,
child: Text(labels[ft] ?? ft.name),
))
.toList();
}
// 数据加载
Future<void> _loadTemplates() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
List<PromptTemplate> templates;
switch (_currentTab) {
case 'OFFICIAL':
templates = await _adminRepository.getVerifiedTemplates();
break;
case 'PENDING_REVIEW':
templates = await _adminRepository.getPendingTemplates();
break;
case 'USER_SUBMITTED':
templates = await _adminRepository.getAllUserTemplates(
page: 0,
size: 100, // 暂时设置较大值,后续可以实现真正的分页
search: _searchQuery.isEmpty ? null : _searchQuery,
);
break;
case 'ALL':
default:
templates = await _adminRepository.getPublicTemplates(
search: _searchQuery.isEmpty ? null : _searchQuery,
);
break;
}
setState(() {
_templates = templates;
_isLoading = false;
});
} catch (e) {
AppLogger.e('PublicTemplatesManagement', '加载模板失败', e);
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 _showAddOfficialTemplateDialog() {
showDialog(
context: context,
builder: (context) => AddOfficialTemplateDialog(
onSuccess: _loadTemplates,
),
);
}
void _showEditTemplateDialog(PromptTemplate template) {
showDialog(
context: context,
builder: (context) => EditTemplateDialog(
template: template,
onSuccess: _loadTemplates,
),
);
}
/// 将PromptTemplate转换为EnhancedUserPromptTemplate
EnhancedUserPromptTemplate _convertToEnhancedTemplate(PromptTemplate template) {
return EnhancedUserPromptTemplate(
id: template.id,
userId: template.authorId ?? '',
name: template.name,
description: template.description,
featureType: template.featureType,
systemPrompt: '', // PromptTemplate没有单独的systemPrompt字段
userPrompt: template.content,
tags: template.templateTags ?? [],
categories: [],
isPublic: template.isPublic,
shareCode: null,
isFavorite: template.isFavorite,
isDefault: template.isDefault,
usageCount: template.useCount?.toInt() ?? 0,
rating: template.averageRating ?? 0.0,
ratingCount: template.ratingCount ?? 0,
createdAt: template.createdAt,
updatedAt: template.updatedAt,
lastUsedAt: null,
isVerified: template.isVerified,
authorId: template.authorId,
version: 1,
language: 'zh',
favoriteCount: 0,
reviewedAt: null,
reviewedBy: null,
reviewComment: null,
);
}
void _showTemplateReviewDialog(PromptTemplate template) {
// TODO: 实现PromptTemplate的审核对话框
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('模板审核功能开发中: ${template.name}')),
);
}
void _showTemplateDetails(PromptTemplate template) {
// 将PromptTemplate转换为EnhancedUserPromptTemplate以兼容现有对话框
final enhancedTemplate = _convertToEnhancedTemplate(template);
showDialog(
context: context,
builder: (context) => TemplateDetailsDialog(
template: enhancedTemplate,
),
);
}
void _showStatisticsDialog() {
showDialog(
context: context,
builder: (context) => TemplateStatisticsDialog(),
);
}
// 操作方法
Future<void> _publishTemplate(PromptTemplate template) async {
try {
await _adminRepository.publishTemplate(template.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('模板 "${template.name}" 发布成功')),
);
_loadTemplates();
} catch (e) {
AppLogger.e('PublicTemplatesManagement', '发布模板失败', e);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('发布失败: $e')),
);
}
}
Future<void> _setTemplateVerified(PromptTemplate template) async {
try {
await _adminRepository.setTemplateVerified(template.id, true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('模板 "${template.name}" 已设为认证')),
);
_loadTemplates();
} catch (e) {
AppLogger.e('PublicTemplatesManagement', '设置模板认证失败', e);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('设置认证失败: $e')),
);
}
}
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 _adminRepository.deleteTemplate(template.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('模板 "${template.name}" 删除成功')),
);
_loadTemplates();
} catch (e) {
AppLogger.e('PublicTemplatesManagement', '删除模板失败', e);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('删除失败: $e')),
);
}
}
// 批量操作
Future<void> _batchPublish() async {
if (_selectedTemplates.isEmpty) return;
try {
for (final template in _selectedTemplates) {
await _adminRepository.publishTemplate(template.id);
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('成功发布 ${_selectedTemplates.length} 个模板')),
);
setState(() {
_selectedTemplates.clear();
_batchMode = false;
});
_loadTemplates();
} catch (e) {
AppLogger.e('PublicTemplatesManagement', '批量发布模板失败', e);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('批量发布失败: $e')),
);
}
}
Future<void> _batchSetVerified() async {
if (_selectedTemplates.isEmpty) return;
try {
for (final template in _selectedTemplates) {
await _adminRepository.setTemplateVerified(template.id, true);
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('成功设置 ${_selectedTemplates.length} 个模板为认证')),
);
setState(() {
_selectedTemplates.clear();
_batchMode = false;
});
_loadTemplates();
} catch (e) {
AppLogger.e('PublicTemplatesManagement', '批量设置模板认证失败', e);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('批量设置认证失败: $e')),
);
}
}
}