diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..2743973 --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/android/settings.gradle b/android/settings.gradle index 643d520..e565048 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -23,3 +23,33 @@ plugins { } include ":app" + +// ============================================================ +// 【Gradle 兼容补丁】flutter_inappwebview_android 1.1.3 的 AGP 9 兼容问题 +// +// 问题:AGP 9 不支持 proguard-android.txt,必须用 proguard-android-optimize.txt。 +// 插件 build.gradle 写死了 proguard-android.txt → Gradle 解析时就报错 +// (debug 构建也炸,因为 AGP 9 在解析阶段就校验了这个方法调用)。 +// +// 修复:在 Gradle 解析插件 build.gradle 之前(beforeProject 钩子), +// 把插件文件里的字符串替换掉。这个钩子比 Gradle 自带机制还早触发, +// 所以能赶在 Gradle 报错前修好。 +// +// 优势: +// - Gradle 原生,不依赖 Python/PowerShell +// - 跨平台(Windows/Mac/Linux,Gradle 必备) +// - 所有同事拉代码后自动生效 +// - Gradle daemon 每次构建都会重新解析 build.gradle,不会有缓存问题 +// ============================================================ +gradle.beforeProject { Project project -> + if (project.name == 'flutter_inappwebview_android') { + def gradleFile = project.file('build.gradle') + def text = gradleFile.text + if (text.contains("proguard-android.txt")) { + gradleFile.text = text.replace( + "proguard-android.txt", "proguard-android-optimize.txt" + ) + println "✅ Patched flutter_inappwebview_android/build.gradle (AGP 9 compat)" + } + } +} diff --git a/lib/core/api/my_report/api.dart b/lib/core/api/my_report/api.dart new file mode 100644 index 0000000..cadaf46 --- /dev/null +++ b/lib/core/api/my_report/api.dart @@ -0,0 +1,35 @@ +import 'package:new_learn/core/api/my_report/model.dart'; +import 'package:new_learn/core/network/dio_client.dart'; + +/// ============================================================ +/// 【my_report 模块接口】我的报告 —— 对标 Vue3 端 api/myReport/index.ts +/// +/// getReportPage —— 报告列表分页(页码分页) +/// GET /api/v1/UserReport/GetUserReportList +/// 参数:pageIndex / pageSize / reportType(可选) +/// ============================================================ +class UserReportApi { + UserReportApi._(); + + static final UserReportApi instance = UserReportApi._(); + + /// 报告列表分页(页码分页:pageIndex / pageSize) + /// [reportType] 可选筛选条件,Vue 端默认不传 + Future getReportPage({ + int pageIndex = 1, + int pageSize = 10, + int? reportType, + }) async { + final query = { + 'pageIndex': pageIndex, + 'pageSize': pageSize, + }; + if (reportType != null) query['reportType'] = reportType; + + final data = await DioClient.instance.get( + '/api/v1/UserReport/GetUserReportList', + query: query, + ); + return UserReportPage.fromJson(data as Map); + } +} diff --git a/lib/core/api/my_report/model.dart b/lib/core/api/my_report/model.dart new file mode 100644 index 0000000..20a9b7f --- /dev/null +++ b/lib/core/api/my_report/model.dart @@ -0,0 +1,91 @@ +/// ============================================================ +/// 【my_report 模块模型】我的报告 —— 对标 Vue3 端 api/myReport/type.ts +/// +/// UserReportDto(单条报告)/ UserReportPage(分页响应) +/// +/// 字段说明: +/// id 报告 ID(后端大整数,返回 string 类型,前端也用 String 存) +/// reportName 报告名称(如"数学分析报告") +/// reportType 报告类型 +/// createTime 创建时间字符串(如 "2026-08-28 15:24:39" 或 ISO "T" 分隔) +/// reportUrl 报告详情 H5 页面 URL(点击列表项后 WebView 加载这个) +/// +/// 序列化:手动 fromJson,做了类型兼容(后端 id 有时 string 有时 number, +/// 统一转 String;reportUrl 清理首尾空白和反引号)。 +/// 与 my_point/model.dart 保持一致,不依赖 build_runner。 +/// ============================================================ +library; + +/// 辅助:id 字段兼容 string / num 两种后端返回,统一转 String +String _asStringId(dynamic value) { + if (value == null) return ''; + if (value is String) return value.trim(); + if (value is num) return value.toInt().toString(); + return value.toString(); +} + +/// 辅助:清理 reportUrl 首尾可能的反引号 / 空白(后端可能包了一层引号) +String _cleanUrl(String raw) { + final cleaned = raw.trim(); + // 去掉首尾反引号(用户粘贴时可能加的,或后端真返回了) + if (cleaned.startsWith('`') && cleaned.endsWith('`')) { + return cleaned.substring(1, cleaned.length - 1).trim(); + } + return cleaned; +} + +/// 单条报告 +class UserReportDto { + const UserReportDto({ + this.id = '', + this.reportName = '', + this.reportType = 0, + this.createTime = '', + this.reportUrl = '', + }); + + final String id; // 后端大整数 ID,用 String 存避免精度丢失 + final String reportName; + final int reportType; + final String createTime; + final String reportUrl; + + factory UserReportDto.fromJson(Map json) { + return UserReportDto( + id: _asStringId(json['id']), + reportName: (json['reportName'] as String?) ?? '', + reportType: (json['reportType'] as num?)?.toInt() ?? 0, + createTime: (json['createTime'] as String?) ?? '', + reportUrl: _cleanUrl((json['reportUrl'] as String?) ?? ''), + ); + } +} + +/// 报告分页响应 +class UserReportPage { + const UserReportPage({ + this.data = const [], + this.total = 0, + this.pageIndex = 1, + this.pageSize = 10, + }); + + final List data; + final int total; + final int pageIndex; + final int pageSize; + + /// 兼容业务响应壳 {code, message, data: {...}}:data 存在就取 data + factory UserReportPage.fromJson(Map json) { + final payload = (json['data'] as Map?) ?? json; + return UserReportPage( + data: (payload['data'] as List? ?? const []) + .whereType>() + .map(UserReportDto.fromJson) + .toList(), + total: (payload['total'] as num?)?.toInt() ?? 0, + pageIndex: (payload['pageIndex'] as num?)?.toInt() ?? 1, + pageSize: (payload['pageSize'] as num?)?.toInt() ?? 10, + ); + } +} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 2cadd8a..326e4d2 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -14,6 +14,8 @@ import 'package:new_learn/features/class/my_update_detail_page.dart'; import 'package:new_learn/core/api/class/model.dart'; import 'package:new_learn/features/my/my_page.dart'; import 'package:new_learn/features/my/my_point/my_point_page.dart'; +import 'package:new_learn/features/my/my_report/my_report_page.dart'; +import 'package:new_learn/features/my/my_report/report_detail_page.dart'; import 'package:new_learn/features/my/settings/settings_page.dart'; import 'package:new_learn/features/my/settings/about_app_page.dart'; import 'package:new_learn/features/recite/recite_page.dart'; @@ -125,6 +127,18 @@ final GoRouter appRouter = GoRouter( MyUpdateDetailPage(initialPost: state.extra as UserPostDto), ), GoRoute(path: '/myPoint', builder: (_, _) => const MyPointPage()), + GoRoute(path: '/myReport', builder: (_, _) => const MyReportPage()), + GoRoute( + path: '/report-detail', + builder: (_, state) { + // extra 格式:{'title': '...', 'url': '...'} + final extra = state.extra as Map? ?? {}; + return ReportDetailPage( + title: extra['title'] as String? ?? '我的报告', + url: extra['url'] as String? ?? '', + ); + }, + ), GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()), GoRoute(path: '/about-app', builder: (_, _) => const AboutAppPage()), // ---------- 首次启动隐私协议页 ---------- diff --git a/lib/features/auth/login_page.dart b/lib/features/auth/login_page.dart index ee96fe6..0e1788e 100644 --- a/lib/features/auth/login_page.dart +++ b/lib/features/auth/login_page.dart @@ -395,14 +395,14 @@ class _LoginPageState extends ConsumerState { if (mounted) { final messenger = ScaffoldMessenger.of(context); context.go('/'); - messenger.showSnackBar( - SnackBar( - content: Text( - '登录成功,欢迎 ${res.info.userName.isEmpty ? account : res.info.userName}', - ), - duration: const Duration(seconds: 2), - ), - ); + // messenger.showSnackBar( + // SnackBar( + // content: Text( + // '登录成功,欢迎 ${res.info.userName.isEmpty ? account : res.info.userName}', + // ), + // duration: const Duration(seconds: 2), + // ), + // ); } } on ApiException catch (e) { if (mounted) _toast(e.message); diff --git a/lib/features/my/my_page.dart b/lib/features/my/my_page.dart index 853bb0d..193bdcc 100644 --- a/lib/features/my/my_page.dart +++ b/lib/features/my/my_page.dart @@ -101,7 +101,7 @@ class MyPage extends ConsumerWidget { '${AppEnv.imgBaseUrl}/StaticResource/2026-06-10/exam1781070655873.png', title: '我的报告', onTap: () { - // TODO: 跳转到我的报告页 + context.push('/myReport'); }, ), _MenuItem( diff --git a/lib/features/my/my_report/my_report_page.dart b/lib/features/my/my_report/my_report_page.dart new file mode 100644 index 0000000..3366a2a --- /dev/null +++ b/lib/features/my/my_report/my_report_page.dart @@ -0,0 +1,266 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:go_router/go_router.dart'; +import 'package:new_learn/core/api/my_report/api.dart'; +import 'package:new_learn/core/api/my_report/model.dart'; +import 'package:new_learn/core/components/common_paged_list.dart'; +import 'package:new_learn/core/logger/app_talker.dart'; +import 'package:new_learn/core/utils/img_url.dart'; + +/// ============================================================ +/// 【MyReportPage】我的报告列表 —— 对标 Vue pages/myPackage/myReport/myReport.vue +/// +/// 一、数据来源: +/// 报告分页 → UserReportApi.instance.getReportPage → CommonPagedList +/// +/// 二、UI 结构: +/// Scaffold 白色 AppBar + 背景 #F5F8FA 的列表区域 +/// 卡片背景图 + 左对齐 icon/标签 + 中间文字 + 右侧箭头 +/// +/// 三、点击卡片 → 跳 /report-detail(WebView 详情页) +/// reportUrl 为空 → SnackBar "报告暂未生成" +/// +/// 四、先只做 phone 布局,Pad 后续再说 +/// ============================================================ + +/// 学科 icon 映射表(对标 Vue utils/constant.js SUBJECT_ICON_MAP) +const Map _subjectIconMap = { + '语文': '/StaticResource/2026-06-11/shuxue1781163770612.png', + '化学': '/StaticResource/2026-06-11/huaxue1781163828443.png', + '英语': '/StaticResource/2026-06-11/yingyu1781163725441.png', + '物理': '/StaticResource/2026-06-11/wuli1781148438346.png', + '数学': '/StaticResource/2026-06-11/shuxue1781163770612.png', + '生物': '/StaticResource/2026-06-11/shengwu1781163791768.png', + '历史': '/StaticResource/2026-06-11/lishi1781163811822.png', + '政治': '/StaticResource/2026-06-11/zhengzhi1781163750422.png', + '地理': '/StaticResource/2026-06-11/dili1781163844311.png', +}; + +/// 取学科名(报告名前 2 个字,对标 Vue getSubjectName) +String _extractSubjectName(UserReportDto item) { + final name = item.reportName.trim(); + if (name.length < 2) return name; + return name.substring(0, 2); +} + +/// 取学科 icon URL(无匹配则用数学兜底,对标 Vue getSubjectIcon) +String _getSubjectIconUrl(String subjectName) { + final path = _subjectIconMap[subjectName] ?? _subjectIconMap['数学']!; + return ImgUrl.build(path); +} + +/// 格式化创建时间(对标 Vue getReportTime) +String _formatCreateTime(String raw) { + if (raw.isEmpty) return ''; + final normalized = raw.replaceAll('T', ' '); + return normalized.length > 16 ? normalized.substring(0, 16) : normalized; +} + +class MyReportPage extends ConsumerStatefulWidget { + const MyReportPage({super.key}); + + @override + ConsumerState createState() => _MyReportPageState(); +} + +class _MyReportPageState extends ConsumerState { + static const int _pageSize = 10; + + @override + void initState() { + super.initState(); + talker.debug('打开页面:我的报告'); + } + + /// 分页加载回调 + Future> _loadPage(int pageIndex) async { + final page = await UserReportApi.instance.getReportPage( + pageIndex: pageIndex, + pageSize: _pageSize, + ); + final hasNext = page.data.length >= _pageSize; + return PagingResult(data: page.data, hasNext: hasNext); + } + + /// 点击卡片:跳 WebView 详情页 + void _onTapCard(UserReportDto item) { + if (item.reportUrl.isEmpty) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('报告暂未生成'))); + return; + } + context.push( + '/report-detail', + extra: { + 'title': item.reportName.isEmpty ? '我的报告' : item.reportName, + 'url': item.reportUrl, + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + elevation: 0, + title: const Text('我的报告', style: TextStyle(color: Color(0xFF1D2B47))), + iconTheme: const IconThemeData(color: Color(0xFF1D2B47)), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.canPop() ? context.pop() : context.go('/my'), + ), + ), + body: Container( + color: const Color(0xFFF5F8FA), + child: CommonPagedList( + load: _loadPage, + padding: EdgeInsets.fromLTRB(12.w, 16.h, 12.w, 0), + separatorBuilder: (_, _) => SizedBox(height: 14.h), + buildItem: (context, item, index) { + return _ReportCard(item: item, onTap: () => _onTapCard(item)); + }, + emptyWidget: Transform.translate( + offset: Offset(0, -80.h), + child: const Center( + child: Text('暂无内容', style: TextStyle(color: Color(0xFF687FAD))), + ), + ), + ), + ), + ); + } +} + +/// ============================================================ +/// 【_ReportCard】报告卡片 +/// +/// 布局(Row 从左到右): +/// [icon + 学科标签] → [报告名 + 时间] → [箭头] +/// +/// 背景图叠底,InkWell 提供点击水波纹。 +/// 图标/标签自然靠左,不做偏移 hack。 +/// ============================================================ +class _ReportCard extends StatelessWidget { + const _ReportCard({required this.item, required this.onTap}); + + final UserReportDto item; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final subjectName = _extractSubjectName(item); + final subjectIconUrl = _getSubjectIconUrl(subjectName); + final timeText = _formatCreateTime(item.createTime); + + const bgPath = + '/StaticResource/2026-06-10/examListItem_bg1781074465963.png'; + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12.r), + child: Container( + height: 109.h, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12.r)), + child: Stack( + children: [ + // 底层:卡片背景图 + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(12.r), + child: CachedNetworkImage( + imageUrl: ImgUrl.build(bgPath), + fit: BoxFit.fill, + placeholder: (_, _) => Container(color: Colors.white), + errorWidget: (_, _, _) => Container(color: Colors.white), + ), + ), + ), + // 上层内容:Row 左对齐,padding 控制整体边距 + Padding( + padding: EdgeInsets.symmetric(horizontal: 22.w), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // ── 左侧:学科 icon + 标签(垂直排列,自然靠左) ── + Column( + mainAxisSize: MainAxisSize.min, + children: [ + CachedNetworkImage( + imageUrl: subjectIconUrl, + width: 56.w, + height: 56.w, + placeholder: (_, _) => const SizedBox.shrink(), + errorWidget: (_, _, _) => const SizedBox.shrink(), + ), + SizedBox(height: 4.h), + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w), + height: 25.h, + decoration: BoxDecoration( + color: const Color(0xFFEAF5FE), + borderRadius: BorderRadius.circular(7.r), + ), + alignment: Alignment.center, + child: Text( + subjectName, + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF3D7DF5), + ), + ), + ), + ], + ), + SizedBox(width: 40.w), + // ── 中间:报告名 + 时间(Expanded 占剩余) ── + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 10.h), + Text( + item.reportName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.sp, + color: const Color(0xFF1D2B47), + ), + ), + SizedBox(height: 20.h), + Text( + timeText, + style: TextStyle( + fontSize: 12.sp, + color: const Color(0xFF687FAD), + ), + ), + ], + ), + ), + // ── 右侧:箭头 ── + Padding( + padding: EdgeInsets.only(top: 10.h), + child: Icon( + Icons.chevron_right, + color: const Color(0xFF4B6B99), + size: 22.sp, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/my/my_report/report_detail_page.dart b/lib/features/my/my_report/report_detail_page.dart new file mode 100644 index 0000000..c87130b --- /dev/null +++ b/lib/features/my/my_report/report_detail_page.dart @@ -0,0 +1,278 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:go_router/go_router.dart'; +import 'package:new_learn/core/logger/app_talker.dart'; + +/// ============================================================ +/// 【ReportDetailPage】报告详情 WebView 页 —— 对标 Vue reportWebView.vue +/// +/// 一、为什么用 flutter_inappwebview 而不是官方 webview_flutter: +/// ✅ 需要 JS → Flutter 回调(addJavaScriptHandler) +/// ✅ 需要 Flutter → JS 主动调用(callAsyncJavaScript / evaluateJavascript) +/// ✅ 比 webview_flutter 功能更全,后续扩展方便 +/// +/// 二、路由参数(通过 GoRouteState.extra 传 Map): +/// context.push('/report-detail', extra: {'title': '...', 'url': '...'}) +/// - title:AppBar 标题 +/// - url:WebView 加载的 H5 报告 URL +/// +/// 三、JS Bridge 完整指南(H5 ↔ Flutter 双向通信): +/// +/// ┌─────────────── H5 调 Flutter ───────────────┐ +/// │ │ +/// │ Flutter 注册 handler(见 _registerJsHandlers)│ +/// │ controller.addJavaScriptHandler( │ +/// │ handlerName: 'closePage', │ +/// │ callback: (args) { ... } │ +/// │ ); │ +/// │ │ +/// │ H5 侧调用: │ +/// │ // 关闭当前 WebView 页 │ +/// │ window.flutter_inappwebview │ +/// │ .callHandler('closePage'); │ +/// │ │ +/// │ // 带参数调用: │ +/// │ window.flutter_inappwebview │ +/// │ .callHandler('onReportAction', { │ +/// │ action: 'download', │ +/// │ data: 'xxx' │ +/// │ }); │ +/// │ │ +/// │ H5 兼容写法(推荐,同时支持 UniApp web-view): │ +/// │ function callFlutter(handlerName, args) { │ +/// │ // Flutter inappwebview │ +/// │ if (window.flutter_inappwebview) { │ +/// │ window.flutter_inappwebview │ +/// │ .callHandler(handlerName, args); │ +/// │ } │ +/// │ // UniApp web-view(可选,按需扩展) │ +/// │ // #ifdef APP-PLUS │ +/// │ // plus.webview.currentWebview()... │ +/// │ // #endif │ +/// │ } │ +/// │ callFlutter('closePage'); │ +/// │ │ +/// └───────────────────────────────────────────────┘ +/// +/// ┌─────────────── Flutter 调 H5 ───────────────┐ +/// │ │ +/// │ // 主动调 H5 全局函数: │ +/// │ await controller.callAsyncJavaScript( │ +/// │ functionBody: │ +/// │ 'window.onFlutterMessage("hello");', │ +/// │ ); │ +/// │ │ +/// │ // 或者执行任意 JS 代码: │ +/// │ await controller.evaluateJavascript( │ +/// │ source: 'document.title', │ +/// │ ); │ +/// │ │ +/// └───────────────────────────────────────────────┘ +/// +/// 四、支持的 JS handler 列表(H5 可调用这些名字): +/// +/// handlerName 参数 作用 +/// ────────────────────────────────────────────────── +/// closePage 无 关闭当前 WebView 页 +/// onReportAction {action,data} H5 主动通知 Flutter 做某事 +/// action 可取: +/// 'share' → 触发分享(TODO: 接 share_plus) +/// 'download' → 下载/保存报告 +/// 'refresh' → Flutter 刷列表后回退 +/// +/// 五、平台配置(如需): +/// Android:AndroidManifest.xml 需 INTERNET 权限(flutter 默认已有) +/// iOS:Info.plist 需 NSAppTransportSecurity 允许 HTTP(如果报告页是 HTTPS 可忽略) +/// ============================================================ + +class ReportDetailPage extends StatefulWidget { + const ReportDetailPage({super.key, required this.title, required this.url}); + + final String title; + final String url; + + @override + State createState() => _ReportDetailPageState(); +} + +class _ReportDetailPageState extends State { + InAppWebViewController? _webController; + + /// WebView 加载进度(0~1),用于底部蓝色进度条 + double _loadingProgress = 0; + + @override + void initState() { + super.initState(); + talker.debug('打开报告详情:${widget.title} → ${widget.url}'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + elevation: 0, + title: Text( + widget.title, + style: const TextStyle(color: Color(0xFF1D2B47)), + ), + iconTheme: const IconThemeData(color: Color(0xFF1D2B47)), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + // 先尝试 WebView 内部 history 回退,不行再 pop 页面 + onPressed: () async { + final controller = _webController; + if (controller != null && await controller.canGoBack()) { + await controller.goBack(); + } else if (context.mounted) { + context.canPop() ? context.pop() : context.go('/my'); + } + }, + ), + // 底部加载进度条(可选,提升体验) + bottom: PreferredSize( + preferredSize: const Size.fromHeight(2), + child: _loadingProgress < 1.0 + ? LinearProgressIndicator( + value: _loadingProgress, + backgroundColor: Colors.transparent, + valueColor: const AlwaysStoppedAnimation(Color(0xFF3D7DF5)), + ) + : const SizedBox.shrink(), + ), + ), + body: InAppWebView( + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + initialSettings: InAppWebViewSettings( + // 允许 JS(报告页大概率需要) + javaScriptEnabled: true, + // 支持内联视频播放 + allowsInlineMediaPlayback: true, + // 允许文件访问 + allowFileAccess: true, + // 自适应内容(防止移动端 H5 在 WebView 里显示过宽) + useHybridComposition: true, + ), + onWebViewCreated: (controller) { + _webController = controller; + // ✅【关键】注册 JS Bridge:H5 通过 window.flutter_inappwebview.callHandler 调这些 + _registerJsHandlers(controller); + talker.debug('WebView created, JS handlers registered'); + }, + onLoadStart: (controller, url) { + setState(() => _loadingProgress = 0); + talker.debug('WebView loading start: $url'); + }, + onLoadStop: (controller, url) async { + setState(() => _loadingProgress = 1); + talker.debug('WebView loading stop: $url'); + // TODO: 可选:页面加载完后主动调 H5 的初始化函数 + // await controller.callAsyncJavaScript( + // functionBody: 'window.onFlutterReady?.();', + // ); + }, + onProgressChanged: (controller, progress) { + setState(() => _loadingProgress = progress / 100.0); + }, + // WebView 内部请求拦截(可选:如需给 H5 接口加 token,可在这里处理) + // shouldInterceptRequest: (controller, request) async { + // final url = request.url?.toString() ?? ''; + // if (url.contains('/api/')) { + // // 给 H5 的接口请求追加 Authorization header + // return request.copyWith( + // headers: { + // ...request.headers ?? {}, + // 'Authorization': 'Bearer ${DioClient.instance.accessToken}', + // }, + // ); + // } + // return null; + // }, + ), + ); + } + + // ============================================================ + // JS Bridge 注册 —— 所有 handler 在这里统一定义 + // + // H5 调用方式:window.flutter_inappwebview.callHandler('closePage') + // 或带参数:window.flutter_inappwebview.callHandler('onReportAction', {action:'xxx'}) + // + // 注意:handler 必须在 onWebViewCreated 里注册,页面加载前就要就绪。 + // ============================================================ + void _registerJsHandlers(InAppWebViewController controller) { + // ---- 1. closePage:关闭当前 WebView 页 ---- + // H5 调用:window.flutter_inappwebview.callHandler('closePage') + controller.addJavaScriptHandler( + handlerName: 'closePage', + callback: (args) { + talker.debug('[JS→Flutter] closePage called, args: $args'); + if (context.mounted) { + context.canPop() ? context.pop() : context.go('/my'); + } + }, + ); + + // ---- 2. onReportAction:H5 通知 Flutter 执行某动作 ---- + // H5 调用: + // window.flutter_inappwebview.callHandler('onReportAction', { + // action: 'download', + // data: 'report_xxx.pdf' + // }) + controller.addJavaScriptHandler( + handlerName: 'onReportAction', + callback: (args) { + // args 是 H5 传过来的参数数组,第一个元素通常是对象 + final payload = args.isNotEmpty ? args.first : null; + talker.debug('[JS→Flutter] onReportAction called, payload: $payload'); + + // 解析 action 字段 + String? action; + String? data; + if (payload is Map) { + action = payload['action']?.toString(); + data = payload['data']?.toString(); + } + + switch (action) { + case 'share': + // TODO: 接 share_plus 分享报告链接 + talker.debug('[JS→Flutter] → share, data: $data'); + break; + case 'download': + // TODO: 下载/保存报告文件 + talker.debug('[JS→Flutter] → download, data: $data'); + break; + case 'refresh': + // H5 处理完后要求 Flutter 刷列表并回退 + talker.debug('[JS→Flutter] → refresh'); + if (context.mounted) { + context.canPop() ? context.pop(true) : context.go('/myReport'); + } + break; + default: + talker.debug('[JS→Flutter] → unknown action: $action'); + } + }, + ); + + // ---- 3. 更多 handler 按需添加 ---- + // 模板(复制下面这几行,改 handlerName 和 callback 即可): + // + // controller.addJavaScriptHandler( + // handlerName: 'yourHandlerName', + // callback: (args) { + // // TODO: 你的处理逻辑 + // }, + // ); + } + + @override + void dispose() { + _webController?.dispose(); + super.dispose(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 9bdb065..d36cab9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:new_learn/core/app.dart'; @@ -108,6 +109,9 @@ Future main() async { // 需要在启动 App 之前读取本地存储、调用原生插件,必须写在 runApp 之前! // 作用: 初始化 Flutter 和原生的绑定桥梁 WidgetsFlutterBinding.ensureInitialized(); + // flutter_inappwebview 平台实现注册(Chrome Web / Windows desktop 必须显式触发) + // 否则会报 InAppWebViewPlatform.instance != null 断言错误 + InAppWebViewPlatform.instance; // go_router 17.x 大坑:命令式导航(context.push/go)默认不写回浏览器 // 地址栏(optionURLReflectsImperativeAPIs 默认 false)。必须在使用 // appRouter(runApp)之前全局开启,否则 web 调试 URL 不随页面变化。 diff --git a/patches/flutter_inappwebview_android-1.1.3.patch b/patches/flutter_inappwebview_android-1.1.3.patch new file mode 100644 index 0000000..f26e04e --- /dev/null +++ b/patches/flutter_inappwebview_android-1.1.3.patch @@ -0,0 +1,15 @@ +--- a/android/build.gradle ++++ b/android/build.gradle +@@ -41,14 +41,14 @@ + buildTypes { + debug { + minifyEnabled false +- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' ++ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled true +- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' ++ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } diff --git a/pubspec.lock b/pubspec.lock index 8524d51..6ce96a4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -499,6 +499,70 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.1.5+1" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: e30fba942e3debea7b7e6cdd4f0f59ce89dd403a9865193e3221293b6d1544c6 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.3.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.6.0" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index a39e3d6..0180a91 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -166,6 +166,14 @@ dependencies: # url_launcher:跳转应用商店 / 外部浏览器下载地址 url_launcher: ^6.3.1 + # WebView(内嵌 H5 报告详情页,支持 JS ↔ Flutter 双向通信) + # 为什么用 flutter_inappwebview 而不是 webview_flutter: + # - 需要 H5 内部按钮回调到 Flutter(JS bridge:addJavaScriptHandler) + # - 比官方 webview_flutter 功能强:支持 callAsyncJavaScript、cookie 管理等 + # - 6.1.5 stable 最低 Flutter 3.24 / Dart 3.5,项目 Flutter 3.44.8 完全兼容 + # ⚠ Android 端要求 minSdkVersion >= 19(flutter_inappwebview 6.x 默认) + flutter_inappwebview: ^6.1.5 + # 常驻底部面板(smooth_sheets:3 停靠点拖拽面板,支持内部 ListView 滚动联动) # 用法:lib/core/components/persistent_snap_sheet.dart 封装组件 # 版本:1.1.1(2026-07 发布,支持 persistent + modal 两种模式) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 41feb02..632804e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); FlutterTimezonePluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ebfc32b..d6b57cc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus file_selector_windows + flutter_inappwebview_windows flutter_secure_storage_windows flutter_timezone gal