import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/tv_app.dart'; import '../../../services/app_service.dart'; import '../../../services/api_client.dart'; /// TV端应用管理页。 class TvAppPage extends ConsumerStatefulWidget { const TvAppPage({super.key}); @override ConsumerState createState() => _TvAppPageState(); } class _TvAppPageState 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 kw = _keyword.trim().isEmpty ? null : _keyword.trim(); final list = await AppService().getTvAppList(page: 1, limit: 20, appName: kw); if (!mounted) return; setState(() { _items = list; _loading = false; _hasMore = list.length >= 20; _page = 1; }); } catch (e) { if (!mounted) return; setState(() { _loading = false; _error = e is ApiException ? e.message : '加载失败'; }); } } Future _loadMore() async { if (_loading) return; setState(() => _loading = true); try { final kw = _keyword.trim().isEmpty ? null : _keyword.trim(); final list = await AppService().getTvAppList(page: _page + 1, limit: 20, appName: kw); 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(); } void _showForm({TvApp? editing}) { final isEdit = editing != null; final nameCtrl = TextEditingController(text: editing?.appName ?? ''); final pkgCtrl = TextEditingController(text: editing?.packageName ?? ''); final catCtrl = TextEditingController(text: editing?.appCategory ?? ''); final iconCtrl = TextEditingController(text: editing?.iconUrl ?? ''); 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: nameCtrl, decoration: const InputDecoration( labelText: '应用名称', border: OutlineInputBorder())), const SizedBox(height: 12), TextField( controller: pkgCtrl, decoration: const InputDecoration( labelText: '包名', border: OutlineInputBorder())), const SizedBox(height: 12), TextField( controller: catCtrl, decoration: const InputDecoration( labelText: '分类', border: OutlineInputBorder())), const SizedBox(height: 12), TextField( controller: iconCtrl, decoration: const InputDecoration( labelText: '图标地址', 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': nameCtrl.text, 'packageName': pkgCtrl.text, 'appCategory': catCtrl.text, 'iconUrl': iconCtrl.text, 'orgId': orgIdCtrl.text, 'groupId': groupIdCtrl.text, 'status': statusValue, }; try { await AppService().saveTvApp(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().deleteTvApp([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('TV端应用管理')), 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 = ''); _refresh(); }, ), border: const OutlineInputBorder(), ), onSubmitted: (v) { setState(() => _keyword = v); _refresh(); }, ), ), 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]; return Card( child: ListTile( leading: item.iconUrl.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( item.iconUrl, width: 40, height: 40, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.apps, size: 40), ), ) : const CircleAvatar(child: Icon(Icons.apps)), title: Text(item.appName, style: const TextStyle(fontWeight: FontWeight.w600)), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Text('包名: ${item.packageName}'), if (item.appCategory.isNotEmpty) Text('分类: ${item.appCategory}'), 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), ), ); } }