splash_page.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. import 'dart:async';
  2. import 'package:flutter/gestures.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:flutter_screenutil/flutter_screenutil.dart';
  6. import 'package:go_router/go_router.dart';
  7. import 'package:news_app/widget/my_txt.dart';
  8. import 'package:flutter_swiper_view/flutter_swiper_view.dart';
  9. import '../../constant/api_const.dart';
  10. import '../../constant/color_res.dart';
  11. import '../../gen/assets.gen.dart';
  12. import '../../http/http_util.dart';
  13. import '../../util/log.util.dart';
  14. import '../../util/shared_prefs_instance_util.dart';
  15. import '../../widget/load_image.dart';
  16. /// @author: bo.zeng
  17. /// @email: cnhbwds@gmail.com
  18. /// @date: 2025 2025/4/9 16:00
  19. /// @description:
  20. class SplashPage extends StatefulWidget {
  21. const SplashPage({super.key});
  22. @override
  23. State<SplashPage> createState() => _SplashPageState();
  24. }
  25. class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateMixin {
  26. TapGestureRecognizer? aa;
  27. List<String> _splashImages = [];
  28. int _currentIndex = 0;
  29. Timer? _navigateTimer;
  30. bool _hasStartedNavigation = false;
  31. late SwiperController _swiperController;
  32. // 本地默认开屏图片(当网络不可用时使用)
  33. static const List<String> _defaultSplashImages = [
  34. 'assets/images/splash_bg.png', // 本地默认图片
  35. ];
  36. @override
  37. void initState() {
  38. super.initState();
  39. _swiperController = SwiperController();
  40. // 初始化使用本地默认图片
  41. _splashImages = _defaultSplashImages;
  42. // 从后台获取开屏页配置
  43. _fetchSplashConfig();
  44. }
  45. /// 从后台获取开屏页配置
  46. Future<void> _fetchSplashConfig() async {
  47. try {
  48. // HttpUtil().get() 已经返回 data['data'],不是完整 response
  49. final data = await HttpUtil().get(apiSplashConfig);
  50. consoleLog('Splash API data type: ${data.runtimeType}, value: $data');
  51. if (data != null) {
  52. List<String> fetchedImages = [];
  53. // 格式1: data 是数组,每个元素包含 url 字段 [{title: "", url: ""}]
  54. if (data is List) {
  55. consoleLog('Splash: data is List with ${data.length} items');
  56. for (int i = 0; i < data.length; i++) {
  57. final item = data[i];
  58. consoleLog('Splash: item[$i] type = ${item.runtimeType}, value = $item');
  59. if (item != null && item is Map) {
  60. final url = item['url']?.toString();
  61. consoleLog('Splash: item[$i][url] = $url');
  62. if (url != null && url.isNotEmpty) {
  63. fetchedImages.add(url);
  64. consoleLog('Splash: added image url: $url');
  65. }
  66. } else if (item is String && item.isNotEmpty) {
  67. fetchedImages.add(item);
  68. }
  69. }
  70. }
  71. // 格式2: data 是对象,包含 imageUrl 字段
  72. else if (data is Map) {
  73. consoleLog('Splash: data is Map');
  74. if (data['imageUrl'] != null) {
  75. final url = data['imageUrl'].toString();
  76. if (url.isNotEmpty) {
  77. fetchedImages.add(url);
  78. }
  79. }
  80. // 格式3: data 是对象,包含 imageList 数组
  81. if (data['imageList'] is List) {
  82. final images = data['imageList'] as List;
  83. for (var img in images) {
  84. if (img is String && img.isNotEmpty) {
  85. fetchedImages.add(img);
  86. }
  87. }
  88. }
  89. }
  90. consoleLog('Splash: fetchedImages = $fetchedImages');
  91. // 如果获取到了图片,更新显示
  92. if (fetchedImages.isNotEmpty && mounted) {
  93. setState(() {
  94. _splashImages = fetchedImages;
  95. _currentIndex = 0;
  96. });
  97. consoleLog('Splash: updated _splashImages with ${fetchedImages.length} images');
  98. // 启动导航计时器(每张图片2秒,总时长 = 图片数量 × 2)
  99. _startNavigationTimer(fetchedImages.length);
  100. }
  101. }
  102. } catch (e) {
  103. // 请求失败或无网络权限,继续使用本地默认图片,不做任何处理
  104. consoleLog('Splash fetch error (will use default): $e');
  105. // 使用默认图片时,也启动导航计时器
  106. _startNavigationTimer(_splashImages.length);
  107. }
  108. }
  109. /// 启动导航计时器(每张图片2秒)
  110. void _startNavigationTimer(int imageCount) {
  111. if (_hasStartedNavigation) return;
  112. _hasStartedNavigation = true;
  113. // 计算总时长:图片数量 × 2秒
  114. final totalSeconds = imageCount * 2;
  115. consoleLog('Splash: Navigation will auto-skip after $totalSeconds seconds ($imageCount images × 2s)');
  116. _navigateTimer = Timer(Duration(seconds: totalSeconds), () async {
  117. bool? first = await getIsFirst();
  118. // 确保页面未被销毁
  119. if (!mounted) return;
  120. if (first == true) {
  121. context.go('/main');
  122. } else {
  123. _showFirstDialog(context);
  124. }
  125. });
  126. }
  127. @override
  128. void dispose() {
  129. _navigateTimer?.cancel();
  130. _swiperController.dispose();
  131. aa?.dispose();
  132. super.dispose();
  133. }
  134. void _showFirstDialog(BuildContext context) {
  135. aa = TapGestureRecognizer();
  136. aa?.onTap = () {
  137. context.push("/user/privacy");
  138. };
  139. showDialog(
  140. context: context,
  141. builder: (context) {
  142. return AlertDialog(
  143. content: SizedBox(
  144. height: 200.h,
  145. width: 220.w,
  146. child: SingleChildScrollView(
  147. child: Column(
  148. children: [
  149. Text(
  150. "个人信息保护提示",
  151. style: TextStyle(fontSize: 14.sp, color: Colors.black),
  152. ),
  153. Text.rich(
  154. TextSpan(
  155. children: [
  156. TextSpan(
  157. text: "欢迎使用新消费传媒!我们将通过",
  158. style: TextStyle(
  159. fontSize: 12.sp,
  160. color: Colors.black,
  161. ),
  162. ),
  163. TextSpan(
  164. text: "《用户协议》",
  165. style: TextStyle(fontSize: 12.sp, color: Colors.blue),
  166. recognizer: aa,
  167. // recognizer: _tapRecognizer
  168. ),
  169. TextSpan(
  170. text: "和",
  171. style: TextStyle(
  172. fontSize: 12.sp,
  173. color: Colors.black,
  174. ),
  175. ),
  176. TextSpan(
  177. text: "《隐私政策》",
  178. style: TextStyle(fontSize: 12.sp, color: Colors.blue),
  179. recognizer: aa,
  180. ),
  181. TextSpan(
  182. text:
  183. "和帮助您了解我们为您提供的服务、"
  184. "我们如何处理个人信息以及您享有的权利。我们会严格按照相关法律法规要求,采取各种安全措施来保护您的个人信息。\n"
  185. "点击“同意”按钮,表示您已知情并同意以上协议和以下约定。\n"
  186. "1.为了保障软件的安全运行和账户安全我们会申请收集您的设备信息、IP地址WLAN MAC地址。\n"
  187. "2.上传或拍摄图片、视频,需要使用您的媒体影音、图片、视频、音频、相机、麦克风权限。\n"
  188. "3.我们可能会申请位置权限,用于为您推荐您可能感兴趣的内容。\n"
  189. "4.我们尊重您的选择权,您可以访问、修改、删除您的个人信息并管理您的授权,我们也为您提供注销、投诉渠道。",
  190. style: TextStyle(
  191. fontSize: 12.sp,
  192. color: Colors.black,
  193. ),
  194. ),
  195. ],
  196. ),
  197. ),
  198. ],
  199. ),
  200. ),
  201. ),
  202. actions: [
  203. GestureDetector(
  204. onTap: () {
  205. saveIsFirst();
  206. context.go('/main');
  207. },
  208. child: Container(
  209. alignment: Alignment.center,
  210. padding: EdgeInsets.symmetric(vertical: 6.h),
  211. decoration: BoxDecoration(
  212. color: colorE71717,
  213. borderRadius: BorderRadius.circular(10.r),
  214. ),
  215. child: myTxt(text: "同意", color: Colors.white, fontSize: 14.sp),
  216. ),
  217. ),
  218. GestureDetector(
  219. onTap: () {
  220. SystemNavigator.pop();
  221. },
  222. child: Container(
  223. margin: EdgeInsets.only(top: 5.h),
  224. padding: EdgeInsets.symmetric(vertical: 6.h),
  225. alignment: Alignment.center,
  226. child: myTxt(text: "不同意", color: Colors.black, fontSize: 14.sp),
  227. ),
  228. ),
  229. ],
  230. );
  231. },
  232. );
  233. }
  234. @override
  235. Widget build(BuildContext context) {
  236. double w = MediaQuery.of(context).size.width;
  237. double h = MediaQuery.of(context).size.height;
  238. return Scaffold(
  239. body: SizedBox(
  240. width: w,
  241. height: h,
  242. child: Stack(
  243. children: [
  244. // 轮播图片
  245. if (_splashImages.length == 1)
  246. // 只有一张图片时直接显示
  247. _buildSingleImage()
  248. else
  249. // 多张图片时使用轮播
  250. _buildCarousel(w, h),
  251. // 指示器(多张图片时显示)
  252. if (_splashImages.length > 1)
  253. Positioned(
  254. bottom: 50.h,
  255. left: 0,
  256. right: 0,
  257. child: Row(
  258. mainAxisAlignment: MainAxisAlignment.center,
  259. children: List.generate(
  260. _splashImages.length,
  261. (index) => AnimatedContainer(
  262. duration: const Duration(milliseconds: 300),
  263. margin: EdgeInsets.symmetric(horizontal: 4.w),
  264. width: _currentIndex == index ? 20.w : 8.w,
  265. height: 8.h,
  266. decoration: BoxDecoration(
  267. color: _currentIndex == index
  268. ? Colors.white
  269. : Colors.white.withOpacity(0.5),
  270. borderRadius: BorderRadius.circular(4.r),
  271. ),
  272. ),
  273. ),
  274. ),
  275. ),
  276. // 跳过按钮
  277. Positioned(
  278. top: 54.h,
  279. right: 20.w,
  280. child: GestureDetector(
  281. onTap: () {
  282. _navigateTimer?.cancel();
  283. _navigateToMain();
  284. },
  285. child: Container(
  286. padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
  287. decoration: BoxDecoration(
  288. color: Colors.black.withOpacity(0.3),
  289. borderRadius: BorderRadius.circular(20.r),
  290. ),
  291. child: myTxt(
  292. text: "跳过",
  293. color: Colors.white,
  294. fontSize: 14.sp,
  295. ),
  296. ),
  297. ),
  298. ),
  299. ],
  300. ),
  301. ),
  302. );
  303. }
  304. /// 构建单张图片
  305. Widget _buildSingleImage() {
  306. final imageUrl = _splashImages.first;
  307. if (imageUrl.startsWith('http') || imageUrl.startsWith('https')) {
  308. return LoadImage(imageUrl, fit: BoxFit.cover);
  309. } else {
  310. return Image.asset(imageUrl.startsWith('assets') ? imageUrl : 'assets/$imageUrl', fit: BoxFit.cover);
  311. }
  312. }
  313. /// 构建轮播
  314. Widget _buildCarousel(double w, double h) {
  315. return Swiper(
  316. controller: _swiperController,
  317. itemCount: _splashImages.length,
  318. index: _currentIndex,
  319. onIndexChanged: (index) {
  320. if (mounted) {
  321. setState(() {
  322. _currentIndex = index;
  323. });
  324. }
  325. },
  326. itemBuilder: (BuildContext context, int index) {
  327. final imageUrl = _splashImages[index];
  328. if (imageUrl.startsWith('http') || imageUrl.startsWith('https')) {
  329. return LoadImage(imageUrl, fit: BoxFit.cover);
  330. } else {
  331. return Image.asset(
  332. imageUrl.startsWith('assets') ? imageUrl : 'assets/$imageUrl',
  333. fit: BoxFit.cover,
  334. );
  335. }
  336. },
  337. autoplay: true, // 启用自动播放
  338. autoplayDelay: 2000, // 每张图片显示2秒
  339. loop: true,
  340. viewportFraction: 1.0,
  341. scale: 1.0,
  342. );
  343. }
  344. /// 导航到主页
  345. void _navigateToMain() async {
  346. bool? first = await getIsFirst();
  347. if (!mounted) return;
  348. if (first == true) {
  349. context.go('/main');
  350. } else {
  351. _showFirstDialog(context);
  352. }
  353. }
  354. }