import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/app_version.dart'; import '../../../services/app_service.dart'; import '../../../services/api_client.dart'; import '../../../utils/formatters.dart'; /// 应用版本管理页。 class AppVersionPage extends ConsumerStatefulWidget { const AppVersionPage({super.key}); @override ConsumerState createState() => _AppVersionPageState(); } class _AppVersionPageState extends ConsumerState { final _searchController = TextEditingController(); final _scrollController = ScrollController(); List _items = []; bool _loading = false; bool _hasMore = true; int _page = 1; String? _error; String _keyword = ''; @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 && !_loading && _hasMore) { _loadMore(); } } Future _loadData() async { setState(() { _loading = true; _error = null; }); try { final list = await AppService().getAppVersionList(page: 1, limit: 20); if (!mounted) return; setState(() { _items = list; _loading = false; _hasMore = list.length >= 20; _page = 1; }); } catch (e) { if (!mounted) return; setState(() { _loading = false; _error = _friendlyError(e); }); } } Future _loadMore() async { if (_loading) return; setState(() => _loading = true); try { final list = await AppService().getAppVersionList(page: _page + 1, limit: 20); if (!mounted) return; setState(() { _items.addAll(list); _loading = false; _hasMore = list.length >= 20; _page++; }); } catch (_) { if (!mounted) return; setState(() => _loading = false); } } Future _refresh() async { _page = 1; _hasMore = true; await _loadData(); } String _friendlyError(Object e) { if (e is ApiException) return e.message; final msg = e.toString(); if (msg.contains('404') || msg.contains('DioException') || msg.contains('SocketException')) { return '应用版本接口暂不可用,请联系管理员确认权限或接口地址'; } return '加载失败:$msg'; } void _showForm({AppVersion? editing}) { final isEdit = editing != null; final appNameCtrl = TextEditingController(text: editing?.appName ?? ''); final packageCtrl = TextEditingController(text: editing?.packageName ?? ''); final versionNameCtrl = TextEditingController(text: editing?.versionName ?? ''); final versionCodeCtrl = TextEditingController(text: editing?.versionCode.toString() ?? ''); final apkUrlCtrl = TextEditingController(text: editing?.apkUrl ?? ''); final orgIdCtrl = TextEditingController(text: editing?.orgId ?? ''); final groupIdCtrl = TextEditingController(text: editing?.groupId ?? ''); int statusValue = editing?.status ?? 0; showModalBottomSheet( useSafeArea: true, context: context, isScrollControlled: true, builder: (ctx) => StatefulBuilder( builder: (ctx, setSheet) => Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(ctx).viewInsets.bottom, ), child: Padding( padding: const EdgeInsets.all(16), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(isEdit ? '编辑版本' : '新增版本', style: Theme.of(ctx).textTheme.titleMedium), const SizedBox(height: 16), TextField( controller: appNameCtrl, decoration: const InputDecoration( labelText: '应用名称', border: OutlineInputBorder())), const SizedBox(height: 12), TextField( controller: packageCtrl, decoration: const InputDecoration( labelText: '包名', border: OutlineInputBorder())), const SizedBox(height: 12), TextField( controller: versionNameCtrl, decoration: const InputDecoration( labelText: '版本名', border: OutlineInputBorder())), const SizedBox(height: 12), TextField( controller: versionCodeCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '版本号', border: OutlineInputBorder())), const SizedBox(height: 12), TextField( controller: apkUrlCtrl, decoration: const InputDecoration( labelText: 'APK下载地址', border: OutlineInputBorder())), const SizedBox(height: 12), TextField( controller: orgIdCtrl, decoration: const InputDecoration( labelText: '组织ID', border: OutlineInputBorder())), const SizedBox(height: 12), TextField( controller: groupIdCtrl, decoration: const InputDecoration( labelText: '分组ID', border: OutlineInputBorder())), const SizedBox(height: 12), DropdownButtonFormField( value: statusValue, decoration: const InputDecoration( labelText: '状态', border: OutlineInputBorder()), items: const [ DropdownMenuItem(value: 0, child: Text('正常')), DropdownMenuItem(value: 1, child: Text('禁用')), ], onChanged: (v) { if (v != null) setSheet(() => statusValue = v); }, ), const SizedBox(height: 16), FilledButton( onPressed: () async { final data = { if (isEdit) 'id': editing!.id, 'appName': appNameCtrl.text, 'packageName': packageCtrl.text, 'versionName': versionNameCtrl.text, 'versionCode': int.tryParse(versionCodeCtrl.text) ?? 0, 'apkUrl': apkUrlCtrl.text, 'orgId': orgIdCtrl.text, 'groupId': groupIdCtrl.text, 'status': statusValue, }; try { if (isEdit) { await AppService().updateAppVersion(data); } else { await AppService().saveAppVersion(data); } if (!ctx.mounted) return; Navigator.pop(ctx); _refresh(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('保存成功')), ); } catch (e) { if (!ctx.mounted) return; ScaffoldMessenger.of(ctx).showSnackBar( SnackBar(content: Text(e is ApiException ? e.message : '保存失败')), ); } }, child: const Text('保存'), ), ], ), ), ), ), ), ); } Future _delete(String id) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('确认删除'), content: const Text('确定要删除此版本吗?'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('删除')), ], ), ); if (confirmed != true) return; try { await AppService().deleteAppVersion([int.tryParse(id) ?? 0]); _refresh(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('删除成功'))); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e is ApiException ? e.message : '删除失败')), ); } } @override Widget build(BuildContext context) { 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: _keyword.isEmpty ? null : IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); setState(() => _keyword = ''); }, ), ), onSubmitted: (v) => setState(() => _keyword = v), ), ), Expanded( child: _error != null ? Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(_error!), const SizedBox(height: 8), FilledButton.tonal(onPressed: _refresh, child: const Text('重试')), ], )) : RefreshIndicator( onRefresh: _refresh, child: _items.isEmpty && !_loading ? const Center(child: Text('暂无数据')) : ListView.builder( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), itemCount: _items.length + (_loading ? 1 : 0), itemBuilder: (ctx, i) { if (i >= _items.length) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } final item = _items[i]; if (_keyword.isNotEmpty && !item.appName.toLowerCase().contains(_keyword.toLowerCase())) { return const SizedBox.shrink(); } return Card( child: ListTile( title: Text(item.appName, style: const TextStyle(fontWeight: FontWeight.w600)), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Text('包名: ${item.packageName}'), Text('版本: ${item.versionName} (${item.versionCode})'), if (item.createTime != null) Text('创建: ${Formatters.formatDateTime(item.createTime!)}'), const SizedBox(height: 4), Chip( label: Text(item.status == 0 ? '正常' : '禁用'), visualDensity: VisualDensity.compact, ), ], ), trailing: PopupMenuButton( onSelected: (v) { if (v == 'edit') _showForm(editing: item); if (v == 'delete') _delete(item.id); }, itemBuilder: (_) => [ const PopupMenuItem(value: 'edit', child: Text('编辑')), const PopupMenuItem(value: 'delete', child: Text('删除')), ], ), ), ); }, ), ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () => _showForm(), child: const Icon(Icons.add), ), ); } }