import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/sys_log.dart'; import '../../../services/api_client.dart'; import '../../../services/sys_service.dart'; /// 系统日志页(TabBar:系统日志 / 审计日志 / JST调用日志)。 class LogPage extends ConsumerStatefulWidget { const LogPage({super.key}); @override ConsumerState createState() => _LogPageState(); } class _LogPageState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, title: const Text('系统日志'), bottom: TabBar( controller: _tabController, tabs: const [ Tab(text: '系统日志'), Tab(text: '审计日志'), Tab(text: 'JST调用日志'), ], ), ), body: TabBarView( controller: _tabController, children: const [ _LogTab(title: '系统日志', logType: _LogType.system), _LogTab(title: '审计日志', logType: _LogType.audit), _LogTab(title: 'JST调用日志', logType: _LogType.jst), ], ), ); } } /// 日志类型枚举。 enum _LogType { system, audit, jst } /// 日志列表 Tab。 class _LogTab extends StatefulWidget { const _LogTab({ required this.title, required this.logType, }); final String title; final _LogType logType; @override State<_LogTab> createState() => _LogTabState(); } class _LogTabState extends State<_LogTab> with AutomaticKeepAliveClientMixin { 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 bool get wantKeepAlive => true; @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> _fetch({int page = 1, int limit = 20}) { switch (widget.logType) { case _LogType.system: return _service.getLogList(page: page, limit: limit); case _LogType.audit: return _service.getAuditLogList(page: page, limit: limit); case _LogType.jst: return _service.getApiJstCallLogList(page: page, limit: limit); } } 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 _fetch(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) { super.build(context); 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: [ Row( children: [ Expanded( child: Text( log.operation.isEmpty ? '(无操作描述)' : log.operation, style: Theme.of(context).textTheme.titleSmall, ), ), Text( '${log.time}ms', style: Theme.of(context).textTheme.bodySmall, ), ], ), 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('IP:${log.ip}'), const SizedBox(height: 2), Text( '时间:${_formatDate(log.createDate)}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ); }, ), ); } }