splash_page.dart 12 KB

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