web_page.dart 12 KB
import 'dart:io';

import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/config/env_config.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebPage extends StatelessWidget {
  const WebPage({super.key});

  @override
  Widget build(BuildContext buildContext) {
    final Map<String, dynamic>? extraData = GoRouterState.of(buildContext).extra as Map<String, dynamic>?;

    var loginOpFlag = true;

    var visitor = extraData?['visitor'];
    var sessionCode = extraData?['sessionCode'];
    var userCode = extraData?['userCode'];
    var classCode = extraData?['classCode'];
    var userType = extraData?['userType'];
    var stuId = extraData?['stuId'];

    if (sessionCode == null || sessionCode == '') {
      loginOpFlag = false;

      var sharedPreferences = getIt.get<SharedPreferences>();
      visitor = sharedPreferences.getInt('auth_visitor');
      sessionCode = sharedPreferences.getString('auth_sessionCode');
      userCode = sharedPreferences.getString('auth_userCode');
      classCode = sharedPreferences.getString('auth_classCode');
      userType = sharedPreferences.getInt('auth_userType');
      stuId = sharedPreferences.getString('auth_stuId');
    }

    return BlocProvider(
      create: (context) {
        final webCubit = WebCubit(
          WebState(
            loginOpFlag: loginOpFlag,
            visitor: visitor,
            sessionCode: sessionCode,
            userCode: userCode,
            classCode: classCode,
            userType: userType,
            stuId: stuId,
          ),
        );
        return webCubit;
      },
      child: BlocConsumer<WebCubit, WebState>(
        builder: (ctx, state) {
          final scaffold = Scaffold(
            appBar: state.showAppBar ? _buildAppBar(ctx, state) : null,
            body: Stack(
              children: [
                state.isUpgrading
                    ? _buildLoadingView(ctx, message: '资源更新中...')
                    : (state.loaded
                        ? WebViewWidget(controller: ctx.read<WebCubit>().controller)
                        : _buildLoadingView(ctx)),
                // 连续点击退出登录的隐形热区(左上角 60x60)
                // 用于 H5 因 JS 异常无响应时的兜底退出机制
                // 使用 Listener 而非 GestureDetector:避免 Android 端 WebView 平台视图
                // 在手势竞技场中抢占 onTap,导致点击不生效。
                // Positioned(
                //   left: 0,
                //   top: 0,
                //   width: 80,
                //   height: 80,
                //   child: Listener(
                //     behavior: HitTestBehavior.opaque,
                //     onPointerDown: (_) {
                //       final triggered = ctx.read<WebCubit>().onQuickLogoutTap();
                //       if (triggered) {
                //         _showQuickLogoutDialog(ctx);
                //       }
                //     },
                //     child: const SizedBox.expand(),
                //   ),
                // ),
              ],
            ),
            bottomNavigationBar: state.showBottomNavBar
                ? BottomNavigationBar(
                    type: BottomNavigationBarType.fixed,
                    currentIndex: state.selectedIndex,
                    selectedItemColor: Color(0xFF7691fa),
                    unselectedItemColor: Color(0xFF969799),
                    onTap: (index) {
                      // 更新选中索引
                      ctx.read<WebCubit>().updateSelectedIndex(index);
                      // 根据 index 执行相应的操作
                    },
                    items: const [
                      BottomNavigationBarItem(
                        icon: Icon(Icons.home, size: 32),
                        label: '我的班级',
                      ),
                      BottomNavigationBarItem(
                        icon: Icon(Icons.contact_page, size: 32),
                        label: '通讯录',
                      ),
                      BottomNavigationBarItem(
                        icon: Icon(Icons.find_in_page, size: 32),
                        label: '发现',
                      ),
                      BottomNavigationBarItem(
                        icon: Icon(Icons.person, size: 32),
                        label: '我的',
                      ),
                    ],
                  )
                : null,
          );

          // 不论 showAppBar 是否为 true,都直接使用 scaffold。
          // 原因:Android 平台上 setSystemUIOverlayStyle 会修改 decorView 的
          // systemUiVisibility flag,污染 H5 <video> 全屏插件赖赖的 flag,
          // 导致全屏画面卡死。状态栏样式应在 main.dart 启动时一次性设置。
          final child = scaffold;

          // ios 不使用 PopScope
          if (Platform.isIOS) {
            return child;
          }

          return PopScope(
            canPop: false,
            onPopInvokedWithResult: (didPop, result) {
              if (didPop) {
                return;
              }
              ctx.read<WebCubit>().handleBack();
            },
            child: child,
          );
        },
        listener: (context, state) {
          if (state.suggestUpgrade) {
            context.read<WebCubit>().suggestUpgrade(context);
          } else if (state.orientationCmdFlag) {
            context.read<WebCubit>().getOrientation(context);
          } else if (state.windowInfoCmdFlag) {
            context.read<WebCubit>().getWindowInfo(context);
          } else if (state.chooseImageCmdFlag) {
            context.read<WebCubit>().chooseImage(context);
          } else if (state.chooseVideoCmdFlag) {
            context.read<WebCubit>().chooseVideo(context);
          } else if (state.wechatQrBindCmdFlag) {
            context.read<WebCubit>().showWechatQrBindDialog(context);
          }
        },
      ),
    );
  }

  Widget _buildLoadingView(
    BuildContext context, {
    String message = '加载中...',
    Color textColor = const Color(0xFF333333),
  }) {
    final screenWidth = MediaQuery.sizeOf(context).width;
    // 设计稿尺寸:356dp × 564dp
    const designWidth = 356.0;
    const designHeight = 564.0;
    final scale = screenWidth / designWidth;
    final heightScale = MediaQuery.sizeOf(context).height / designHeight;
    final sloganWidth = 230.0 * scale;
    final brandWidth = 170.0 * scale;
    final sloganTopPadding = 140.0 * scale;
    final loadingSpacing = 60.0 * heightScale;
    final brandBottomPadding = 40.0 * heightScale;

    return SizedBox.expand(
      child: Column(
        children: [
          Padding(
            padding: EdgeInsets.only(top: sloganTopPadding),
            child: Image.asset(
              'assets/images/login_v3/index_slogan.png',
              width: sloganWidth,
            ),
          ),
          SizedBox(height: loadingSpacing),
          Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const SizedBox(
                width: 24,
                height: 24,
                child: CircularProgressIndicator(
                  color: Color(0xFF7691fa),
                  strokeWidth: 3,
                ),
              ),
              const SizedBox(height: 16),
              Text(
                message,
                style: TextStyle(
                  fontSize: 14,
                  color: textColor,
                  height: null,
                  fontWeight: FontWeight.normal,
                ),
              ),
            ],
          ),
          const Spacer(),
          Padding(
            padding: EdgeInsets.only(bottom: brandBottomPadding),
            child: Image.asset(
              'assets/images/login_v3/index_brand.png',
              width: brandWidth,
            ),
          ),
        ],
      ),
    );
  }

  AppBar _buildAppBar(BuildContext ctx, WebState state) {
    return AppBar(
      title: EnvConfig.isDev()
          ? Text(state.title + state.testMsg, style: TextStyle(color: Color(state.titleColor), fontSize: 16))
          : Text(state.title, style: TextStyle(color: Color(state.titleColor), fontSize: 16)),
      centerTitle: true,
      automaticallyImplyLeading: false,
      backgroundColor: Color(state.bgColor),
      actionsIconTheme: IconThemeData(color: Color(state.titleColor)),
      leading: state.opIcon == 'back'
          ? IconButton(
              icon: Icon(Icons.arrow_back, color: Color(state.titleColor)),
              onPressed: () {
                ctx.read<WebCubit>().handleBack();
              },
            )
          : (state.opIcon == 'home'
              ? IconButton(
                  icon: Icon(Icons.home, color: Color(state.titleColor)),
                  onPressed: () {
                    ctx.read<WebCubit>().handleHome();
                  },
                )
              : null),
      actions: [
        state.setting
            ? IconButton(
                icon: const Icon(Icons.settings),
                onPressed: () {
                  router.push('/setting');
                },
              )
            : const SizedBox.shrink(),
      ],
      toolbarHeight: 40.0,
    );
  }

  /// 连续点击达阈后弹出的退出登录确认对话框
  /// 样式参照 login_main_page_v3 中的 _showAgreementDialog
  Future<void> _showQuickLogoutDialog(BuildContext ctx) async {
    final webCubit = ctx.read<WebCubit>();
    final result = await showDialog(
      context: ctx,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return AlertDialog(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(
              Radius.circular(5),
            ),
          ),
          title: Text(
            '退出登录',
            style: TextStyle(
              fontSize: 17,
              color: Color(0xFF000000),
            ),
            textAlign: TextAlign.center,
          ),
          content: Text(
            '检测到连续点击了屏幕,是否退出登录?',
            style: TextStyle(color: Color(0xFF666666), fontSize: 14),
          ),
          actions: [
            Table(
              children: [
                TableRow(
                  children: [
                    TableCell(
                      child: TextButton(
                        onPressed: () {
                          Navigator.of(context).pop();
                        },
                        style: TextButton.styleFrom(
                          foregroundColor: Color(0xFF666666),
                          textStyle: TextStyle(fontSize: 17),
                          padding: EdgeInsets.zero,
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.zero,
                          ),
                        ),
                        child: Text('取消'),
                      ),
                    ),
                    TableCell(
                      child: TextButton(
                        onPressed: () {
                          Navigator.of(context).pop('OK');
                        },
                        style: TextButton.styleFrom(
                          foregroundColor: Color(0xFF7691FA),
                          textStyle: TextStyle(fontSize: 17),
                          minimumSize: Size.fromHeight(40),
                          padding: EdgeInsets.zero,
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.zero,
                          ),
                        ),
                        child: Text('确认'),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ],
        );
      },
    );
    if (result != null) {
      webCubit.handleQuickLogout();
    }
  }
}