import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../core/navigation/broker_navigation.dart'; import '../../../data/models/home/activity_banner.dart'; /// 底部 Tab 根路由 — 点击 banner 跳转这些路径时用 go() 切换 tab const _tabRoutes = {'/','market', '/market', '/futures', '/copy-trading', '/asset'}; /// 首页 Banner 轮播 — 从接口获取图片,支持 internal/external 跳转 class ActivityCarousel extends ConsumerStatefulWidget { const ActivityCarousel({super.key, required this.banners}); final List banners; @override ConsumerState createState() => _ActivityCarouselState(); } class _ActivityCarouselState extends ConsumerState { final _controller = PageController(); int _currentPage = 0; Timer? _timer; @override void initState() { super.initState(); _startAutoPlay(); } @override void didUpdateWidget(covariant ActivityCarousel old) { super.didUpdateWidget(old); if (old.banners.length != widget.banners.length) { _timer?.cancel(); _currentPage = 0; _startAutoPlay(); } } @override void dispose() { _timer?.cancel(); _controller.dispose(); super.dispose(); } void _startAutoPlay() { if (widget.banners.length <= 1) return; _timer = Timer.periodic(const Duration(seconds: 4), (_) { if (!mounted) return; final next = (_currentPage + 1) % widget.banners.length; _controller.animateToPage( next, duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, ); }); } Future _onTap(ActivityBanner banner) async { if (banner.linkUrl.isEmpty) return; if (banner.isExternal) { launchUrl(Uri.parse(banner.linkUrl), mode: LaunchMode.externalApplication); return; } final url = banner.linkUrl; // ── 经纪商页需要先验证身份 ──────────────────────────── if (url == '/broker' || url.startsWith('/broker')) { await openBrokerEntry(context, ref); return; } // ── Tab 级路由用 go() 切换底部导航选中项 ────────────── if (!mounted) return; final isTabRoute = _tabRoutes.any((r) => url == r || url.startsWith('$r/')); if (isTabRoute) { context.go(url); } else { context.push(url); } } @override Widget build(BuildContext context) { if (widget.banners.isEmpty) return const SizedBox.shrink(); final cs = Theme.of(context).colorScheme; final count = widget.banners.length; return Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(14), child: SizedBox( height: 120, child: PageView.builder( controller: _controller, onPageChanged: (i) => setState(() => _currentPage = i), itemCount: count, itemBuilder: (context, index) { final banner = widget.banners[index]; return GestureDetector( onTap: () => _onTap(banner), child: banner.imageUrl.isNotEmpty ? CachedNetworkImage( imageUrl: banner.imageUrl, fit: BoxFit.cover, width: double.infinity, placeholder: (_, __) => Container( color: cs.surface, child: Center( child: Icon( Icons.image_outlined, color: cs.onSurface.withAlpha(60), size: 32, ), ), ), errorWidget: (_, __, ___) => Container( color: cs.surface, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.broken_image_outlined, color: cs.onSurface.withAlpha(80), size: 28), const SizedBox(height: 4), Text( banner.title, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 12, ), ), ], ), ), ), ) : Container( color: cs.surface, child: Center( child: Text( banner.title, style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600, ), ), ), ), ); }, ), ), ), if (count > 1) ...[ const SizedBox(height: 8), // 圆点指示器 Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(count, (i) { final active = i == _currentPage; return Container( width: active ? 16 : 6, height: 6, margin: const EdgeInsets.symmetric(horizontal: 2), decoration: BoxDecoration( color: active ? cs.onSurface : cs.onSurface.withAlpha(60), borderRadius: BorderRadius.circular(3), ), ); }), ), ], ], ), ); } }