| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- 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<ActivityBanner> banners;
- @override
- ConsumerState<ActivityCarousel> createState() => _ActivityCarouselState();
- }
- class _ActivityCarouselState extends ConsumerState<ActivityCarousel> {
- 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<void> _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),
- ),
- );
- }),
- ),
- ],
- ],
- ),
- );
- }
- }
|