import 'package:flutter/material.dart'; import 'package:unionapp/pages/music/music_page.dart'; import '../../service/recommendation_helper.dart'; import '../../service/song_service.dart'; import '../../tool/gradientText.dart'; import '../user/userpage.dart'; import '../scorelist.dart'; import 'package:provider/provider.dart'; import '../../providers/user_provider.dart'; import '../../model/song_model.dart'; class HomePage extends StatelessWidget { final Function(int)? onSwitchTab; const HomePage({super.key, this.onSwitchTab}); @override Widget build(BuildContext context) { final userProvider = context.watch(); return Scaffold( backgroundColor: Colors.transparent, body: SafeArea( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ====================== 顶部 Header ====================== SizedBox( width: double.infinity, height: 100, child: Stack( children: [ const Padding( padding: EdgeInsets.only(left: 30, top: 60), child: Text( "Tool", style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), ), ), Positioned( right: 20, top: 25, child: Row( children: [ GradientText( data: userProvider.username, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.bold), gradientLayers: [ GradientLayer( gradient: const LinearGradient( colors: [Colors.pinkAccent, Colors.orangeAccent], ), blendMode: BlendMode.srcIn, ), ], ), const SizedBox(width: 8), ClipRRect( borderRadius: BorderRadius.circular(0), child: SizedBox( width: 54, height: 54, child: userProvider.avatarUrl.isNotEmpty ? Image.network( userProvider.avatarUrl, fit: BoxFit.cover, ) : Container( color: Colors.grey[200], child: const Icon(Icons.person, size: 30), ), ), ) ], ), ), ], ), ), const SizedBox(height: 30), // ====================== 横向卡片区域 ====================== SizedBox( height: 180, child: SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 10).copyWith(bottom: 30), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCardWithTitle( context: context, title: "用户中心", icon: Icons.person_outline, targetIndex: 3, gradient: const LinearGradient( colors: [Colors.black, Colors.grey], begin: Alignment.topLeft, end: Alignment.bottomRight, ), shadowColor: Colors.black38, ), const SizedBox(width: 20), _buildCardWithTitle( context: context, title: "歌曲列表", icon: Icons.music_note_outlined, targetPage: const MusicPage(), gradient: const LinearGradient( colors: [Color(0xFFff9a9e), Color(0xFFfecfef)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), shadowColor: const Color(0xFFff9a9e).withOpacity(0.6), ), const SizedBox(width: 20), _buildCardWithTitle( context: context, title: "成绩管理", icon: Icons.score, targetIndex: 1, gradient: const LinearGradient( colors: [Color(0xFF84fab0), Color(0xFF8fd3f4)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), shadowColor: const Color(0xFF84fab0).withOpacity(0.6), ), const SizedBox(width: 20), _buildCardWithTitle( context: context, title: "娱乐功能", icon: Icons.kebab_dining_sharp, targetPage: const ScoreListPage(), gradient: const LinearGradient( colors: [Colors.lightBlueAccent, Colors.blueAccent], begin: Alignment.topLeft, end: Alignment.bottomRight, ), shadowColor: Colors.lightBlue.withOpacity(0.6), ), const SizedBox(width: 20), _buildCardWithTitle( context: context, title: "评分列表", icon: Icons.star, targetPage: const ScoreListPage(), gradient: const LinearGradient( colors: [Colors.redAccent, Colors.pinkAccent], begin: Alignment.topLeft, end: Alignment.bottomRight, ), shadowColor: Colors.red.withOpacity(0.6), ), const SizedBox(width: 20), ], ), ), ), // ====================== 海报 ====================== const PosterImage(imageUrl: 'https://cdn.godserver.cn/post/post%20unionapp1.png'), const SizedBox(height: 20), // ====================== 用户数据展示卡片 ====================== const _UserInfoCard(), const SizedBox(height: 30), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 左侧:智能推荐乐曲(占 6 份宽度) Expanded( flex: 6, child: const _RecommendedSongsSection(), ), const SizedBox(width: 20), // 左右间距 // 右侧:快捷按钮区域(占 4 份宽度) Expanded( flex: 6, child: _buildQuickActionButtons(), // 快捷按钮组件 ), const SizedBox(width: 10), // 左右间距 ], ), const SizedBox(height: 30), ], ), ), ), ); } // 右侧快捷按钮区域(你可以自由修改图标、文字、点击事件) Widget _buildQuickActionButtons() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "快捷操作", style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 15), // 按钮网格 GridView.count( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisCount: 2, // 一行 2 个按钮 crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 1.0, children: [ _quickActionItem( icon: Icons.download, label: "更新成绩", gradient: LinearGradient( colors: [Colors.lightBlueAccent.withAlpha(10), Colors.pinkAccent.withOpacity(0.1)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), color: Colors.pinkAccent.shade100, onTap: () {}, ), _quickActionItem( icon: Icons.stars_outlined, label: "ADX", color: Colors.white, gradient: LinearGradient( colors: [Colors.blueAccent, Colors.pinkAccent.shade100], begin: Alignment.topLeft, end: Alignment.topRight, ), boxShadow: [ BoxShadow( color: Colors.blueAccent.withOpacity(0.3), blurRadius: 8, offset: const Offset(2, 4), ), ], onTap: () {}, ), // 橙色渐变 _quickActionItem( icon: Icons.generating_tokens, label: "ReiSasol", color: Colors.white, gradient: LinearGradient( colors: [Colors.orangeAccent, Colors.pink], begin: Alignment.center, end: Alignment.bottomLeft, ), boxShadow: [ BoxShadow( color: Colors.orange.withOpacity(0.3), blurRadius: 8, offset: const Offset(2, 4), ), ], onTap: () {}, ), _quickActionItem( icon: Icons.link, label: "友站", color: Colors.white, gradient: LinearGradient( colors: [Colors.grey[600]!, Colors.grey[400]!], ), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.2), blurRadius: 6, offset: const Offset(1, 2), ), ], onTap: () {}, ), ], ), ], ); } // 修改后的组件方法 Widget _quickActionItem({ required IconData icon, required String label, required Color color, required VoidCallback onTap, // 新增:渐变背景(可选) Gradient? gradient, // 新增:阴影(可选) List? boxShadow, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( decoration: BoxDecoration( // 优先级:渐变 > 纯色背景 color: gradient == null ? Colors.black.withOpacity(0.1) : null, gradient: gradient, borderRadius: BorderRadius.circular(16), boxShadow: boxShadow, ), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), // 优化内边距 child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: color, size: 28), const SizedBox(height: 8), Text( label, style: TextStyle( fontSize: 13, color: color, fontWeight: FontWeight.w500, ), ), ], ), ), ); } Widget _buildCardWithTitle({ required BuildContext context, required String title, required IconData icon, int? targetIndex, Widget? targetPage, required LinearGradient gradient, required Color shadowColor, }) { return Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onTap: () { if (targetIndex != null && onSwitchTab != null) { onSwitchTab!(targetIndex); } else if (targetPage != null) { Navigator.push( context, MaterialPageRoute(builder: (context) => targetPage), ); } }, child: _buildCustomCardBody( gradient: gradient, shadowColor: shadowColor, icon: icon, ), ), const SizedBox(height: 12), Text( title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), textAlign: TextAlign.center, ), ], ); } Widget _buildCustomCardBody({ required LinearGradient gradient, required Color shadowColor, required IconData icon, }) { return Container( width: 100, height: 100, decoration: BoxDecoration( gradient: gradient, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: shadowColor, blurRadius: 15, spreadRadius: 3, offset: const Offset(4, 6), ), ], ), child: Center( child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.white.withOpacity(0.25), shape: BoxShape.circle, ), child: Icon( icon, size: 36, color: Colors.white, ), ), ), ); } } // ====================== 用户数据卡片 ====================== class _UserInfoCard extends StatelessWidget { const _UserInfoCard(); @override Widget build(BuildContext context) { final userProvider = Provider.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 17.0), child: Container( width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Colors.white, boxShadow: [ BoxShadow( color: Colors.lightBlueAccent.shade100, blurRadius: 4, offset: const Offset(0, 2), spreadRadius: 3, ), ], ), padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ ClipRRect( borderRadius: BorderRadius.circular(0), child: SizedBox( width: 60, height: 60, child: userProvider.avatarUrl.isNotEmpty ? Image.network( userProvider.avatarUrl, fit: BoxFit.cover, ) : Container( color: Colors.grey[200], child: const Icon(Icons.person, size: 30), ), ), ), const SizedBox(width: 18), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GradientText( data:"用户名:${userProvider.username} ", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), gradientLayers: [ GradientLayer( gradient: const LinearGradient( colors: [Colors.blueAccent, Colors.green], ), blendMode: BlendMode.srcIn, ), ], ), GradientText( data:"Ra:${userProvider.user?.rating} Points:${userProvider.user?.points}", style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), gradientLayers: [ GradientLayer( gradient: const LinearGradient( colors: [Colors.deepPurple, Colors.purple], ), blendMode: BlendMode.srcIn, ), ], ), const Divider(height: 5, thickness: 1), Text( userProvider.username == "未登录" ? "状态:未登录" : "状态:已登录", style: TextStyle( fontSize: 14, color: userProvider.username == "未登录" ? Colors.red : Colors.green, ), ), ], ), ), ], ), ], ), ), ); } } // ====================== 🎵 修复完成:推荐乐曲组件 ====================== class _RecommendedSongsSection extends StatelessWidget { const _RecommendedSongsSection(); Future> _loadData(UserProvider userProvider) async { // 加载歌曲 + 用户数据 final allSongs = await SongService.getAllSongs(); List userMusicList = []; final token = userProvider.token; try { final scoreData = await SongService.getUserAllScores( token!, name: userProvider.selectedCnUserName, ); if (scoreData.containsKey('userScoreAll_')) { userMusicList = scoreData['userScoreAll_']['userMusicList'] ?? []; } else if (scoreData.containsKey('userMusicList')) { userMusicList = scoreData['userMusicList'] ?? []; } } catch (e) { } final int currentRating = userProvider.user?.rating ?? 0; final int estimatedB35Min = (currentRating / 50).toInt(); final recommended = RecommendationHelper.getSmartRecommendations( allSongs: allSongs, userMusicList: userMusicList, userRating: currentRating, // 传入非空 int b35MinRating: estimatedB35Min, // 传入非空 int count: 6, ); return { 'songs': recommended, }; } @override Widget build(BuildContext context) { final userProvider = context.read(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ SizedBox(width: 8), Text( "随几首", style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 15), FutureBuilder>( future: _loadData(userProvider), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData) { return const Center(child: Text("加载推荐失败")); } final List songs = snapshot.data!['songs'] ?? []; if (songs.isEmpty) { return const Center( child: Padding( padding: EdgeInsets.all(20.0), child: Text("暂无推荐歌曲,快去游玩更多曲目吧!"), ), ); } return SizedBox( height: 200, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: songs.length, itemBuilder: (context, index) { return _SongItemCard(song: songs[index]); }, ), ); }, ), ], ), ); } } class _SongItemCard extends StatelessWidget { final SongModel song; const _SongItemCard({required this.song}); String _getCoverUrl(int musicId) { int displayId = musicId % 10000; // 注意:这里逻辑可能需要根据你的实际资源调整,通常 DX 歌曲 ID > 10000 if (musicId >= 10000) { String idStr = displayId.toString().padLeft(6, '0'); return "https://u.mai2.link/jacket/UI_Jacket_$idStr.jpg"; } else { return "https://cdn.godserver.cn/resource/static/mai/cover/$displayId.png"; } } // 获取难度颜色 Color _getLevelColor(int levelIndex) { switch (levelIndex) { case 0: return Colors.green; // Basic case 1: return Colors.blue; // Advanced case 2: return Colors.yellow[700]!; // Expert case 3: return Colors.red; // Master case 4: return Colors.purple; // Re:Master default: return Colors.grey; } } @override Widget build(BuildContext context) { // 假设我们要显示 Master (3) 和 Re:Master (4) 的难度 // 你需要从 song.sd 或 song.dx 中解析出具体的 level_value (定数) double? masterLv = _getLevelValue(song, 3); double? reMasterLv = _getLevelValue(song, 4); return Container( width: 140, margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.15), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ✅ 1. 图片优化:增加 loadingBuilder 和 cacheWidth ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: SizedBox( height: 120, width: double.infinity, child: Image.network( _getCoverUrl(song.id), fit: BoxFit.cover, // 关键优化:指定缓存宽度,减少内存占用和解码时间 cacheWidth: 280, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( color: Colors.grey[200], child: Center(child: CircularProgressIndicator( strokeWidth: 2, value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, )), ); }, errorBuilder: (_, __, ___) { return Container( color: Colors.grey[200], child: const Icon(Icons.music_note, size: 40, color: Colors.grey), ); }, ), ), ), const SizedBox(height: 8), // 标题 Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( song.title ?? "未知歌曲", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), ), ), // 艺术家 (修复了重复显示的问题) Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( song.artist ?? "未知艺术家", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11, color: Colors.grey[600]), ), ), const SizedBox(height: 6), // ✅ 2. 底部难度标签 Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( children: [ if (masterLv != null) _buildLevelTag("MAS", masterLv, Colors.purple), if (reMasterLv != null) ...[ const SizedBox(width: 4), _buildLevelTag("ReM", reMasterLv, Colors.deepPurple), ] ], ), ), ], ), ); } Widget _buildLevelTag(String prefix, double level, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(4), border: Border.all(color: color.withOpacity(0.5), width: 0.5), ), child: Text( "$prefix ${level.toStringAsFixed(1)}", style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: color, ), ), ); } // 辅助方法:获取定数 double? _getLevelValue(SongModel song, int levelIndex) { Map ?diffMap = song.dx; if (diffMap==null|| diffMap.isEmpty) { diffMap = song.sd; } var data = diffMap?["$levelIndex"] ?? diffMap?[levelIndex]; if (data is Map && data["level_value"] != null) { return (data["level_value"] as num).toDouble(); } return null; } } class PosterImage extends StatelessWidget { final String imageUrl; const PosterImage({Key? key, required this.imageUrl}) : super(key: key); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: ConstrainedBox( constraints: const BoxConstraints( ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.purpleAccent.withOpacity(0.14), blurRadius: 12, offset: const Offset(0, 10), spreadRadius: 10, ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( imageUrl, fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ); }, errorBuilder: (context, error, stackTrace) { return const Center( child: Icon(Icons.broken_image, size: 50, color: Colors.grey), ); }, ), ), ), ), ); } }