rtc_prd/airhub_app/lib/pages/home_page.dart
seaislee1209 f9666d4aa3 feat: UI规范化 + 故事吸入动画 + 音乐页面优化
- 全局字体统一(Outfit/DM Sans), 头部/按钮/Toast规范化
- 故事详情页: Genie Suck吸入动画(标题+卡片一起缩小模糊消失)
- 书架页: bookPop弹出+粒子效果(三段式动画完整链路)
- 音乐页面: 心情卡片emoji换Material图标+彩色圆块横排布局
- 音乐页面: 进度条胶囊宽度对齐, 播放按钮位置修复, 间距均匀化
- 音乐播放: 接入just_audio, 支持播放暂停进度拖拽自动切歌
- 新增: iOS风格毛玻璃Toast, 渐变背景组件, 通知页面
- 阶段总结文档更新

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:34:53 +08:00

258 lines
7.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_colors.dart';
import '../widgets/animated_gradient_background.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with TickerProviderStateMixin {
late AnimationController _mascotController;
late Animation<double> _mascotAnimation;
late AnimationController _shineController;
@override
void initState() {
super.initState();
// Mascot floating animation
_mascotController = AnimationController(
duration: const Duration(seconds: 4),
vsync: this,
)..repeat(reverse: true);
_mascotAnimation = Tween<double>(begin: -10, end: 10).animate(
CurvedAnimation(parent: _mascotController, curve: Curves.easeInOut),
);
// Shine sweep animation for connect button
_shineController = AnimationController(
duration: const Duration(seconds: 4),
vsync: this,
)..repeat();
}
@override
void dispose() {
_mascotController.dispose();
_shineController.dispose();
super.dispose();
}
void _handleConnect() {
context.go('/bluetooth');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
// Gradient Background
_buildGradientBackground(),
SafeArea(
child: Column(
children: [
// Header (Logo)
_buildHeader(),
// Main Content (Mascot)
Expanded(child: _buildBody()),
// Footer (Button)
_buildFooter(),
],
),
),
],
),
);
}
Widget _buildGradientBackground() {
return const AnimatedGradientBackground();
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.only(top: 32),
alignment: Alignment.center,
child: Text(
'Airhub',
// Use Press Start 2P pixel font per HTML CSS
style: GoogleFonts.pressStart2p(
fontSize: 28,
color: const Color(0xFF6366F1), // 靛蓝
letterSpacing: 2,
shadows: const [
Shadow(
color: Color(0x30A78BFA),
offset: Offset(1, 1),
blurRadius: 0,
),
Shadow(
color: Color(0x1AA78BFA),
offset: Offset(2, 2),
blurRadius: 0,
),
],
),
),
);
}
Widget _buildBody() {
return Center(
child: AnimatedBuilder(
animation: _mascotAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _mascotAnimation.value),
child: child,
);
},
child: Container(
// Glow effect behind mascot
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFF8B5CF6).withOpacity(0.15),
blurRadius: 40,
spreadRadius: 10,
),
],
),
child: Transform.translate(
offset: const Offset(16, 0), // HTML: translateX(5%) ≈ 16px on 320
child: Image.asset(
'assets/www/mascot_transparent.png',
width: 320, // HTML: min(320px, 75vw)
fit: BoxFit.contain,
errorBuilder: (_, __, ___) =>
const Icon(Icons.adb, size: 200, color: Colors.grey),
),
),
),
),
);
}
Widget _buildFooter() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 56),
child: Container(
height: 58, // HTML: height: 58px
constraints: const BoxConstraints(maxWidth: 300), // HTML: width: min(300px, 82vw)
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(29), // HTML: border-radius: 29px
gradient: AppColors.btnPrimaryGradient,
// 5-layer box-shadow per HTML CSS --btn-primary-glow
boxShadow: [
// 0 0 15px rgba(34, 211, 238, 0.35) - cyan outer glow
BoxShadow(
color: const Color(0xFF22D3EE).withOpacity(0.35),
offset: Offset.zero,
blurRadius: 15,
),
// 0 0 30px rgba(99, 102, 241, 0.25) - indigo wider glow
BoxShadow(
color: const Color(0xFF6366F1).withOpacity(0.25),
offset: Offset.zero,
blurRadius: 30,
),
// 0 6px 20px rgba(99, 102, 241, 0.4) - bottom shadow
BoxShadow(
color: const Color(0xFF6366F1).withOpacity(0.4),
offset: const Offset(0, 6),
blurRadius: 20,
),
],
),
child: Stack(
children: [
// Shine overlay (top half gradient)
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(29),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withOpacity(0.15),
Colors.transparent,
],
stops: const [0.0, 0.5],
),
),
),
),
),
// Sweep shine animation (btn-shine from HTML)
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(29),
child: AnimatedBuilder(
animation: _shineController,
builder: (context, child) {
return Transform.translate(
offset: Offset(
(_shineController.value * 2 - 1) * 200,
0,
),
child: child,
);
},
child: Container(
width: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
Colors.white.withOpacity(0.10),
Colors.transparent,
],
stops: const [0.0, 0.5, 1.0],
),
),
),
),
),
),
// Button content
Material(
color: Colors.transparent,
elevation: 0,
child: InkWell(
onTap: _handleConnect,
borderRadius: BorderRadius.circular(29),
child: Center(
child: Text(
'立即连接',
style: GoogleFonts.dmSans(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
),
),
),
),
),
],
),
),
);
}
}