import 'dart:ui'; 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/cacheImage.dart'; import '../../tool/gradientText.dart'; import '../../widgets/glowBlobConfig.dart'; import '../music/adx.dart'; import '../music/score_single.dart'; import '../score/updateScorePage.dart'; import '../user/userpage.dart'; import '../scorelist.dart'; import 'package:provider/provider.dart'; import '../../providers/user_provider.dart'; import '../../model/song_model.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.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( body: Stack( children: [ // 在 Stack 的底层调用 LiquidGlowBackground( blurSigma: 60, // 模糊程度 duration: const Duration(seconds: 10), // 动画周期 blobs: [ GlowBlobConfig( color: Colors.blueAccent.withOpacity(0.5), size: 250, begin: const Alignment(-0.8, -0.4), // 从左下 end: const Alignment(-0.2, 0.2), // 移动到中央偏左 ), GlowBlobConfig( color: Colors.pinkAccent.withOpacity(0.4), size: 100, begin: const Alignment(-0.8, 0.9), end: const Alignment(0.3, 0.5), ), GlowBlobConfig( color: Colors.white.withOpacity(0.3), size: 200, begin: const Alignment(2.0, -0.4), // 从正下方溢出处 end: const Alignment(0.0, -0.5), // 向上浮动 ), ], ), // 2. 主内容层 SafeArea( bottom: false, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ====================== 顶部 Header ====================== _buildHeader(userProvider), const SizedBox(height: 30), // ====================== 横向卡片区域 ====================== _buildHorizontalCards(context), // ====================== 海报 ====================== const PosterImage(imageUrl: 'https://cdn.godserver.cn/post/post%20unionapp1.png'), const SizedBox(height: 20), // ====================== 用户数据展示卡片 ====================== const _UserInfoCard(), const SizedBox(height: 30), // ====================== 下部两栏布局 ====================== Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 7, child: const _RecommendedSongsSection(), ), const SizedBox(width: 4), Expanded( flex: 6, child: _buildQuickActionButtons(context), ), ], ), ), const SizedBox(height: 50), // 留出底部空间给发光球 ], ), ), ), ], ), ); } Widget _buildHeader(userProvider) { return 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), ), ), ) ], ), ), ], ), ); } Widget _buildHorizontalCards(BuildContext context) { return SizedBox( height: 180, child: SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 10).copyWith(bottom: 30), child: Row( children: [ _buildCardWithTitle( context: context, title: "用户中心", icon: Icons.person_outline, targetIndex: 3, gradient: const LinearGradient(colors: [Colors.black, Colors.grey]), 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)]), 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)]), 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]), 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]), shadowColor: Colors.red.withOpacity(0.6), ), ], ), ), ); } Widget _buildQuickActionButtons(BuildContext context) { 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, crossAxisSpacing: 12, mainAxisSpacing: 12, children: [ _quickActionItem( icon: Icons.download, label: "更新成绩", color: Colors.pinkAccent.shade100, gradient: LinearGradient(colors: [Colors.lightBlueAccent.withAlpha(10), Colors.pinkAccent.withOpacity(0.1)]), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const BindAccountPage())), ), _quickActionItem( icon: Icons.stars_outlined, label: "ADX", color: Colors.white, gradient: LinearGradient(colors: [Colors.blueAccent, Colors.pinkAccent.shade100]), boxShadow: [BoxShadow(color: Colors.blueAccent.withOpacity(0.3), blurRadius: 8, offset: const Offset(2, 4))], onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => AdxDownloadGridPage())), ), _quickActionItem( icon: Icons.generating_tokens, label: "ReiSasol", color: Colors.white, gradient: const LinearGradient(colors: [Colors.orangeAccent, Colors.pink]), onTap: () async { const url = "https://bot.q.qq.com/s/c2mloqdgv?id=102172520"; if (await canLaunchUrl(Uri.parse(url))) await launchUrl(Uri.parse(url)); }, ), _quickActionItem( icon: Icons.link, label: "友站", color: Colors.white, gradient: LinearGradient(colors: [Colors.grey[600]!, Colors.grey[400]!]), 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)), ], ); } 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: 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 ? CacheImage.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: 220, 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; 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; case 1: return Colors.blue; case 2: return Colors.yellow[700]!; case 3: return Colors.red; case 4: return Colors.purple; default: return Colors.grey; } } @override Widget build(BuildContext context) { double? masterLv = _getLevelValue(song, 3); double? reMasterLv = _getLevelValue(song, 4); // ⭐ 核心:用 InkWell / GestureDetector 包裹整个卡片,实现点击跳转 return InkWell( borderRadius: BorderRadius.circular(12), onTap: () { // 跳转到歌曲详情页 Navigator.push( context, MaterialPageRoute( builder: (context) => SongDetailPage( song: song, userScoreCache: {}, // 你可以根据实际页面传值 ), ), ); }, child: 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: [ ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: SizedBox( height: 140, width: double.infinity, child: CacheImage.network( _getCoverUrl(song.id), fit: BoxFit.cover, 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, )), ); }, ), ), ), 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), 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: CacheImage.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), ); }, ), ), ), ), ); } }