import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/media_asset.dart'; import '../../../services/media_service.dart'; import '../../../services/api_client.dart'; /// 媒资排行榜 Top10 管理页。 class MediaRankPage extends ConsumerStatefulWidget { const MediaRankPage({super.key}); @override ConsumerState createState() => _MediaRankPageState(); } class _MediaRankPageState extends ConsumerState { List _items = []; bool _loading = false; String? _error; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() { _loading = true; _error = null; }); try { final list = await MediaService().getMediaAssetRankTop10(); if (!mounted) return; setState(() { _items = list; _loading = false; }); } catch (e) { if (!mounted) return; setState(() { _loading = false; _error = e is ApiException ? e.message : '加载失败'; }); } } Future _moveUp(String code) async { try { await MediaService().moveUpMediaAssetRank(code); _loadData(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e is ApiException ? e.message : '操作失败')), ); } } Future _moveDown(String code) async { try { await MediaService().moveDownMediaAssetRank(code); _loadData(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e is ApiException ? e.message : '操作失败')), ); } } Future _remove(String code) 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 MediaService().removeMediaAssetRank(code); _loadData(); 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 : '移除失败')), ); } } Future _clearAll() 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 MediaService().clearMediaAssetRank(); _loadData(); 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 : '操作失败')), ); } } void _showAddDialog() { final searchCtrl = TextEditingController(); List allMedia = []; bool searching = false; showModalBottomSheet( useSafeArea: true, context: context, isScrollControlled: true, builder: (sheetCtx) { return StatefulBuilder( builder: (ctx, setSheet) { Future doSearch() async { setSheet(() => searching = true); try { allMedia = await MediaService().getMediaAssetList(page: 1, limit: 50); } catch (_) { allMedia = []; } finally { setSheet(() => searching = false); } } if (allMedia.isEmpty && !searching) { doSearch(); } final kw = searchCtrl.text.trim().toLowerCase(); final filtered = kw.isEmpty ? allMedia : allMedia.where((m) => m.name.toLowerCase().contains(kw)).toList(); final existingCodes = _items.map((e) => e.code).toSet(); final available = filtered.where((m) => !existingCodes.contains(m.code)).toList(); return SizedBox( height: MediaQuery.of(ctx).size.height * 0.7, child: Column( children: [ Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Text('选择媒资', style: Theme.of(ctx).textTheme.titleLarge), const Spacer(), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(ctx).pop(), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TextField( controller: searchCtrl, decoration: const InputDecoration( hintText: '搜索媒资名称', prefixIcon: Icon(Icons.search), border: OutlineInputBorder(), ), onChanged: (_) => setSheet(() {}), ), ), const SizedBox(height: 8), Expanded( child: searching ? const Center(child: CircularProgressIndicator()) : available.isEmpty ? const Center(child: Text('无可选媒资')) : ListView.builder( itemCount: available.length, itemBuilder: (_, index) { final media = available[index]; return ListTile( leading: media.coverUrl.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(4), child: Image.network( media.coverUrl, width: 40, height: 56, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const SizedBox( width: 40, height: 56, child: Icon(Icons.video_library)), ), ) : const SizedBox( width: 40, height: 56, child: Icon(Icons.video_library), ), title: Text(media.name), subtitle: Text(media.code), trailing: FilledButton.tonal( onPressed: () => _addToRank(ctx, media.code, media.name), child: const Text('添加'), ), ); }, ), ), ], ), ); }, ); }, ); } Future _addToRank(BuildContext sheetCtx, String code, String name) async { try { await MediaService().setMediaAssetRank([code]); if (!mounted) return; Navigator.of(sheetCtx).pop(); _loadData(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('已添加「$name」到排行榜')), ); } 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('媒资排行榜 Top10'), actions: [ if (_items.isNotEmpty) IconButton( icon: const Icon(Icons.delete_sweep), onPressed: _clearAll, tooltip: '清空排行榜', ), ], ), body: _loading ? const Center(child: CircularProgressIndicator()) : _error != null ? Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(_error!), const SizedBox(height: 8), FilledButton.tonal(onPressed: _loadData, child: const Text('重试')), ], )) : _items.isEmpty ? const Center(child: Text('排行榜为空,点击+添加')) : ListView.builder( padding: const EdgeInsets.all(8), itemCount: _items.length, itemBuilder: (ctx, i) { final item = _items[i]; final rank = i + 1; return Card( child: ListTile( leading: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 36, height: 48, alignment: Alignment.center, decoration: BoxDecoration( color: rank <= 3 ? [Colors.amber, Colors.grey, Colors.brown][rank - 1] : Theme.of(ctx).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(6), ), child: Text( '$rank', style: TextStyle( fontWeight: FontWeight.bold, color: rank <= 3 ? Colors.white : null, ), ), ), const SizedBox(width: 8), if (item.coverUrl.isNotEmpty) ClipRRect( borderRadius: BorderRadius.circular(4), child: Image.network( item.coverUrl, width: 36, height: 48, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const SizedBox( width: 36, height: 48, child: Icon(Icons.video_library, size: 20)), ), ) else const SizedBox( width: 36, height: 48, child: Icon(Icons.video_library, size: 20), ), ], ), title: Text(item.mediaName.isNotEmpty ? item.mediaName : item.code), subtitle: Text(item.code), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.arrow_upward), onPressed: i > 0 ? () => _moveUp(item.code) : null, ), IconButton( icon: const Icon(Icons.arrow_downward), onPressed: i < _items.length - 1 ? () => _moveDown(item.code) : null, ), IconButton( icon: const Icon(Icons.close), onPressed: () => _remove(item.code), ), ], ), ), ); }, ), floatingActionButton: FloatingActionButton( onPressed: _showAddDialog, child: const Icon(Icons.add), ), ); } }