import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/sys_config.dart'; import '../../../services/api_client.dart'; import '../../../services/sys_service.dart'; /// 参数配置页。 class ConfigPage extends ConsumerStatefulWidget { const ConfigPage({super.key}); @override ConsumerState createState() => _ConfigPageState(); } class _ConfigPageState extends ConsumerState { final SystemService _service = SystemService(); final List _items = []; bool _loading = false; int _page = 1; bool _hasMore = true; String? _error; final TextEditingController _searchController = TextEditingController(); String _searchKeyword = ''; final ScrollController _scrollController = ScrollController(); final int _limit = 20; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); _loadData(); } @override void dispose() { _searchController.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.getConfigList(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); } void _onSearchChanged(String value) { _searchKeyword = value; // 本地过滤,因为 getConfigList 不支持搜索参数 setState(() {}); } List get _filteredItems { if (_searchKeyword.isEmpty) return _items; return _items .where((c) => c.paramKey.toLowerCase().contains( _searchKeyword.toLowerCase(), )) .toList(); } Future _showForm({SysConfig? config}) async { final result = await showModalBottomSheet>( context: context, isScrollControlled: true, builder: (context) => _ConfigForm(config: config), ); if (result == null) return; try { await _service.saveConfig(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 _deleteConfig(SysConfig config) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('确认删除'), content: Text('确定要删除参数「${config.paramKey}」吗?'), 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.deleteConfig([int.tryParse(config.id) ?? 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')), ); } } } @override Widget build(BuildContext context) { final filtered = _filteredItems; return Scaffold( appBar: AppBar(automaticallyImplyLeading: false, title: const Text('参数配置')), body: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: '搜索参数键名', prefixIcon: const Icon(Icons.search), suffixIcon: _searchKeyword.isEmpty ? null : IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); _onSearchChanged(''); }, ), border: const OutlineInputBorder(), ), onChanged: _onSearchChanged, ), ), Expanded(child: _buildBody(filtered)), ], ), floatingActionButton: FloatingActionButton( onPressed: () => _showForm(), child: const Icon(Icons.add), ), ); } Widget _buildBody(List items) { 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 && _searchKeyword.isEmpty) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: Text('没有更多数据')), ); } return const SizedBox.shrink(); } final config = items[index]; return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ListTile( title: Text(config.paramKey), subtitle: Text( '值:${config.paramValue.isEmpty ? "(空)" : config.paramValue}' '${config.remark.isNotEmpty ? "\n备注:${config.remark}" : ""}', ), trailing: PopupMenuButton( onSelected: (value) { if (value == 'edit') { _showForm(config: config); } else if (value == 'delete') { _deleteConfig(config); } }, itemBuilder: (context) => [ const PopupMenuItem(value: 'edit', child: Text('编辑')), const PopupMenuItem(value: 'delete', child: Text('删除')), ], ), ), ); }, ), ); } } /// 参数配置表单。 class _ConfigForm extends StatefulWidget { const _ConfigForm({this.config}); final SysConfig? config; @override State<_ConfigForm> createState() => _ConfigFormState(); } class _ConfigFormState extends State<_ConfigForm> { final _formKey = GlobalKey(); late TextEditingController _paramKeyCtrl; late TextEditingController _paramValueCtrl; late TextEditingController _remarkCtrl; @override void initState() { super.initState(); _paramKeyCtrl = TextEditingController(text: widget.config?.paramKey ?? ''); _paramValueCtrl = TextEditingController(text: widget.config?.paramValue ?? ''); _remarkCtrl = TextEditingController(text: widget.config?.remark ?? ''); } @override void dispose() { _paramKeyCtrl.dispose(); _paramValueCtrl.dispose(); _remarkCtrl.dispose(); super.dispose(); } void _submit() { if (!_formKey.currentState!.validate()) return; final data = { 'paramKey': _paramKeyCtrl.text.trim(), 'paramValue': _paramValueCtrl.text.trim(), 'remark': _remarkCtrl.text.trim(), }; if (widget.config != null) { data['id'] = widget.config!.id; } Navigator.pop(context, data); } @override Widget build(BuildContext context) { final isEdit = widget.config != null; return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( isEdit ? '编辑参数' : '新增参数', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), TextFormField( controller: _paramKeyCtrl, decoration: const InputDecoration( labelText: '参数键名', border: OutlineInputBorder(), ), validator: (v) => (v == null || v.trim().isEmpty) ? '请输入参数键名' : null, ), const SizedBox(height: 12), TextFormField( controller: _paramValueCtrl, maxLines: 3, decoration: const InputDecoration( labelText: '参数值', border: OutlineInputBorder(), ), validator: (v) => (v == null || v.trim().isEmpty) ? '请输入参数值' : null, ), const SizedBox(height: 12), TextFormField( controller: _remarkCtrl, maxLines: 2, decoration: const InputDecoration( labelText: '备注', border: OutlineInputBorder(), ), ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), const SizedBox(width: 8), FilledButton( onPressed: _submit, child: const Text('保存'), ), ], ), ], ), ), ), ); } }