import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/schedule.dart'; import '../../../models/sys_log.dart'; import '../../../services/api_client.dart'; import '../../../services/sys_service.dart'; /// 定时任务管理页。 class SchedulePage extends ConsumerStatefulWidget { const SchedulePage({super.key}); @override ConsumerState createState() => _SchedulePageState(); } class _SchedulePageState extends ConsumerState { final SystemService _service = SystemService(); final List _items = []; bool _loading = false; int _page = 1; bool _hasMore = true; String? _error; final ScrollController _scrollController = ScrollController(); final int _limit = 20; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); _loadData(); } @override void dispose() { _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { _loadMore(); } } Future _loadData({bool reset = false}) async { if (_loading) return; if (reset) { _page = 1; _hasMore = true; _items.clear(); } setState(() { _loading = true; _error = null; }); try { final list = await _service.getScheduleList(page: _page, limit: _limit); _items.addAll(list); _hasMore = list.length >= _limit; } on ApiException catch (e) { _error = e.message; } catch (e) { _error = e.toString(); } finally { if (mounted) setState(() => _loading = false); } } Future _loadMore() async { if (_loading || !_hasMore) return; _page++; await _loadData(); } Future _refresh() async { await _loadData(reset: true); } Future _showForm({Schedule? schedule}) async { final result = await showModalBottomSheet>( context: context, isScrollControlled: true, builder: (context) => _ScheduleForm(schedule: schedule), ); if (result == null) return; try { await _service.saveSchedule(result); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('保存成功')), ); _refresh(); } } on ApiException catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message)), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('保存失败:$e')), ); } } } Future _deleteSchedule(Schedule schedule) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('确认删除'), content: Text('确定要删除任务「${schedule.beanName}.${schedule.methodName}」吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('取消'), ), FilledButton( onPressed: () => Navigator.pop(context, true), child: const Text('删除'), ), ], ), ); if (confirmed != true) return; try { await _service.deleteSchedule([int.tryParse(schedule.jobId) ?? 0]); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('删除成功')), ); _refresh(); } } on ApiException catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message)), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('删除失败:$e')), ); } } } Future _pauseSchedule(Schedule schedule) async { try { await _service.pauseSchedule(schedule.jobId); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('任务已暂停')), ); _refresh(); } } on ApiException catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message)), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('暂停失败:$e')), ); } } } Future _resumeSchedule(Schedule schedule) async { try { await _service.resumeSchedule(schedule.jobId); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('任务已恢复')), ); _refresh(); } } on ApiException catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message)), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('恢复失败:$e')), ); } } } Future _runSchedule(Schedule schedule) async { try { await _service.runSchedule(schedule.jobId); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('任务已触发执行')), ); } } on ApiException catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message)), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('执行失败:$e')), ); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(automaticallyImplyLeading: false, title: const Text('定时任务')), body: _buildBody(), floatingActionButton: FloatingActionButton( onPressed: () => _showForm(), child: const Icon(Icons.add), ), ); } Widget _buildBody() { if (_error != null && _items.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(_error!), const SizedBox(height: 8), FilledButton.tonal( onPressed: _refresh, child: const Text('重试'), ), ], ), ); } if (_items.isEmpty && !_loading) { return const Center(child: Text('暂无数据')); } return RefreshIndicator( onRefresh: _refresh, child: ListView.builder( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), itemCount: _items.length + 1, itemBuilder: (context, index) { if (index == _items.length) { if (_loading) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } if (!_hasMore) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: Text('没有更多数据')), ); } return const SizedBox.shrink(); } final schedule = _items[index]; return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ListTile( title: Text('${schedule.beanName}.${schedule.methodName}'), subtitle: Text( 'Cron:${schedule.cronExpression} | ${schedule.status == 0 ? '正常' : '暂停'}' '${schedule.remark.isNotEmpty ? "\n${schedule.remark}" : ""}', ), trailing: PopupMenuButton( onSelected: (value) { switch (value) { case 'edit': _showForm(schedule: schedule); break; case 'delete': _deleteSchedule(schedule); break; case 'pause': _pauseSchedule(schedule); break; case 'resume': _resumeSchedule(schedule); break; case 'run': _runSchedule(schedule); break; case 'log': _showScheduleLog(schedule); break; } }, itemBuilder: (context) => [ const PopupMenuItem(value: 'edit', child: Text('编辑')), const PopupMenuItem(value: 'delete', child: Text('删除')), const PopupMenuItem(value: 'pause', child: Text('暂停')), const PopupMenuItem(value: 'resume', child: Text('恢复')), const PopupMenuItem(value: 'run', child: Text('立即运行')), const PopupMenuItem(value: 'log', child: Text('日志')), ], ), ), ); }, ), ); } void _showScheduleLog(Schedule schedule) { Navigator.push( context, MaterialPageRoute( builder: (context) => _ScheduleLogPage(jobId: schedule.jobId), ), ); } } /// 定时任务日志页。 class _ScheduleLogPage extends StatefulWidget { const _ScheduleLogPage({required this.jobId}); final String jobId; @override State<_ScheduleLogPage> createState() => _ScheduleLogPageState(); } class _ScheduleLogPageState extends State<_ScheduleLogPage> { final SystemService _service = SystemService(); final List _items = []; bool _loading = false; int _page = 1; bool _hasMore = true; String? _error; final ScrollController _scrollController = ScrollController(); final int _limit = 20; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); _loadData(); } @override void dispose() { _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { _loadMore(); } } Future _loadData({bool reset = false}) async { if (_loading) return; if (reset) { _page = 1; _hasMore = true; _items.clear(); } setState(() { _loading = true; _error = null; }); try { final list = await _service.getScheduleLogList(page: _page, limit: _limit); _items.addAll(list); _hasMore = list.length >= _limit; } on ApiException catch (e) { _error = e.message; } catch (e) { _error = e.toString(); } finally { if (mounted) setState(() => _loading = false); } } Future _loadMore() async { if (_loading || !_hasMore) return; _page++; await _loadData(); } Future _refresh() async { await _loadData(reset: true); } String _formatDate(DateTime? dt) { if (dt == null) return ''; return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-' '${dt.day.toString().padLeft(2, '0')} ' '${dt.hour.toString().padLeft(2, '0')}:' '${dt.minute.toString().padLeft(2, '0')}:' '${dt.second.toString().padLeft(2, '0')}'; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(automaticallyImplyLeading: false, title: const Text('任务日志')), body: _buildBody(), ); } Widget _buildBody() { if (_error != null && _items.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(_error!), const SizedBox(height: 8), FilledButton.tonal( onPressed: _refresh, child: const Text('重试'), ), ], ), ); } if (_items.isEmpty && !_loading) { return const Center(child: Text('暂无日志')); } return RefreshIndicator( onRefresh: _refresh, child: ListView.builder( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), itemCount: _items.length + 1, itemBuilder: (context, index) { if (index == _items.length) { if (_loading) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } if (!_hasMore) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: Text('没有更多数据')), ); } return const SizedBox.shrink(); } final log = _items[index]; return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( log.operation.isEmpty ? '(无操作描述)' : log.operation, style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 4), Text('用户:${log.username}'), const SizedBox(height: 2), Text( '方法:${log.method}', style: Theme.of(context).textTheme.bodySmall, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text('耗时:${log.time}ms'), const SizedBox(height: 2), Text( '时间:${_formatDate(log.createDate)}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ); }, ), ); } } /// 定时任务表单。 class _ScheduleForm extends StatefulWidget { const _ScheduleForm({this.schedule}); final Schedule? schedule; @override State<_ScheduleForm> createState() => _ScheduleFormState(); } class _ScheduleFormState extends State<_ScheduleForm> { final _formKey = GlobalKey(); late final TextEditingController _beanName; late final TextEditingController _methodName; late final TextEditingController _params; late final TextEditingController _cronExpression; late final TextEditingController _remark; @override void initState() { super.initState(); final s = widget.schedule; _beanName = TextEditingController(text: s?.beanName ?? ''); _methodName = TextEditingController(text: s?.methodName ?? ''); _params = TextEditingController(text: s?.params ?? ''); _cronExpression = TextEditingController(text: s?.cronExpression ?? ''); _remark = TextEditingController(text: s?.remark ?? ''); } @override void dispose() { _beanName.dispose(); _methodName.dispose(); _params.dispose(); _cronExpression.dispose(); _remark.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final isEdit = widget.schedule != null; return SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Padding( padding: const EdgeInsets.all(16), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(isEdit ? '编辑任务' : '新增任务', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 16), TextFormField( controller: _beanName, decoration: const InputDecoration(labelText: 'Bean名称'), validator: (v) => (v == null || v.trim().isEmpty) ? '请输入Bean名称' : null, ), TextFormField( controller: _methodName, decoration: const InputDecoration(labelText: '方法名称'), validator: (v) => (v == null || v.trim().isEmpty) ? '请输入方法名称' : null, ), TextFormField( controller: _params, decoration: const InputDecoration(labelText: '参数'), ), TextFormField( controller: _cronExpression, decoration: const InputDecoration(labelText: 'Cron表达式'), validator: (v) => (v == null || v.trim().isEmpty) ? '请输入Cron表达式' : null, ), TextFormField( controller: _remark, decoration: const InputDecoration(labelText: '备注'), ), const SizedBox(height: 16), FilledButton( onPressed: () { if (_formKey.currentState?.validate() != true) return; final data = { if (isEdit) 'jobId': widget.schedule!.jobId, 'beanName': _beanName.text.trim(), 'methodName': _methodName.text.trim(), 'params': _params.text.trim(), 'cronExpression': _cronExpression.text.trim(), 'remark': _remark.text.trim(), }; Navigator.pop(context, data); }, child: const Text('保存'), ), ], ), ), ), ); } }