feat: 新增我的报告模块,集成inappwebview支持H5详情页
1. 添加flutter_inappwebview依赖并配置各平台插件注册 2. 新增我的报告列表页、详情WebView页 3. 新增报告模块API和数据模型 4. 配置路由跳转,修复我的菜单跳转逻辑 5. 添加AGP9兼容补丁修复插件构建问题 6. 注释掉登录成功的SnackBar提示
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -23,3 +23,33 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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<UserReportPage> getReportPage({
|
||||||
|
int pageIndex = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
int? reportType,
|
||||||
|
}) async {
|
||||||
|
final query = <String, dynamic>{
|
||||||
|
'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<String, dynamic>);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, dynamic> 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<UserReportDto> data;
|
||||||
|
final int total;
|
||||||
|
final int pageIndex;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
/// 兼容业务响应壳 {code, message, data: {...}}:data 存在就取 data
|
||||||
|
factory UserReportPage.fromJson(Map<String, dynamic> json) {
|
||||||
|
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
|
||||||
|
return UserReportPage(
|
||||||
|
data: (payload['data'] as List<dynamic>? ?? const [])
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/core/api/class/model.dart';
|
||||||
import 'package:new_learn/features/my/my_page.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_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/settings_page.dart';
|
||||||
import 'package:new_learn/features/my/settings/about_app_page.dart';
|
import 'package:new_learn/features/my/settings/about_app_page.dart';
|
||||||
import 'package:new_learn/features/recite/recite_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),
|
MyUpdateDetailPage(initialPost: state.extra as UserPostDto),
|
||||||
),
|
),
|
||||||
GoRoute(path: '/myPoint', builder: (_, _) => const MyPointPage()),
|
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<String, dynamic>? ?? {};
|
||||||
|
return ReportDetailPage(
|
||||||
|
title: extra['title'] as String? ?? '我的报告',
|
||||||
|
url: extra['url'] as String? ?? '',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()),
|
GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()),
|
||||||
GoRoute(path: '/about-app', builder: (_, _) => const AboutAppPage()),
|
GoRoute(path: '/about-app', builder: (_, _) => const AboutAppPage()),
|
||||||
// ---------- 首次启动隐私协议页 ----------
|
// ---------- 首次启动隐私协议页 ----------
|
||||||
|
|||||||
@@ -395,14 +395,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
context.go('/');
|
context.go('/');
|
||||||
messenger.showSnackBar(
|
// messenger.showSnackBar(
|
||||||
SnackBar(
|
// SnackBar(
|
||||||
content: Text(
|
// content: Text(
|
||||||
'登录成功,欢迎 ${res.info.userName.isEmpty ? account : res.info.userName}',
|
// '登录成功,欢迎 ${res.info.userName.isEmpty ? account : res.info.userName}',
|
||||||
),
|
// ),
|
||||||
duration: const Duration(seconds: 2),
|
// duration: const Duration(seconds: 2),
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
} on ApiException catch (e) {
|
} on ApiException catch (e) {
|
||||||
if (mounted) _toast(e.message);
|
if (mounted) _toast(e.message);
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class MyPage extends ConsumerWidget {
|
|||||||
'${AppEnv.imgBaseUrl}/StaticResource/2026-06-10/exam1781070655873.png',
|
'${AppEnv.imgBaseUrl}/StaticResource/2026-06-10/exam1781070655873.png',
|
||||||
title: '我的报告',
|
title: '我的报告',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// TODO: 跳转到我的报告页
|
context.push('/myReport');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
|
|||||||
@@ -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<String, String> _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<MyReportPage> createState() => _MyReportPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyReportPageState extends ConsumerState<MyReportPage> {
|
||||||
|
static const int _pageSize = 10;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
talker.debug('打开页面:我的报告');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分页加载回调
|
||||||
|
Future<PagingResult<UserReportDto>> _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<UserReportDto>(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ReportDetailPage> createState() => _ReportDetailPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReportDetailPageState extends State<ReportDetailPage> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:new_learn/core/app.dart';
|
import 'package:new_learn/core/app.dart';
|
||||||
@@ -108,6 +109,9 @@ Future<void> main() async {
|
|||||||
// 需要在启动 App 之前读取本地存储、调用原生插件,必须写在 runApp 之前!
|
// 需要在启动 App 之前读取本地存储、调用原生插件,必须写在 runApp 之前!
|
||||||
// 作用: 初始化 Flutter 和原生的绑定桥梁
|
// 作用: 初始化 Flutter 和原生的绑定桥梁
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
// flutter_inappwebview 平台实现注册(Chrome Web / Windows desktop 必须显式触发)
|
||||||
|
// 否则会报 InAppWebViewPlatform.instance != null 断言错误
|
||||||
|
InAppWebViewPlatform.instance;
|
||||||
// go_router 17.x 大坑:命令式导航(context.push/go)默认不写回浏览器
|
// go_router 17.x 大坑:命令式导航(context.push/go)默认不写回浏览器
|
||||||
// 地址栏(optionURLReflectsImperativeAPIs 默认 false)。必须在使用
|
// 地址栏(optionURLReflectsImperativeAPIs 默认 false)。必须在使用
|
||||||
// appRouter(runApp)之前全局开启,否则 web 调试 URL 不随页面变化。
|
// appRouter(runApp)之前全局开启,否则 web 调试 URL 不随页面变化。
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -499,6 +499,70 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.5+1"
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -166,6 +166,14 @@ dependencies:
|
|||||||
# url_launcher:跳转应用商店 / 外部浏览器下载地址
|
# url_launcher:跳转应用商店 / 外部浏览器下载地址
|
||||||
url_launcher: ^6.3.1
|
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 滚动联动)
|
# 常驻底部面板(smooth_sheets:3 停靠点拖拽面板,支持内部 ListView 滚动联动)
|
||||||
# 用法:lib/core/components/persistent_snap_sheet.dart 封装组件
|
# 用法:lib/core/components/persistent_snap_sheet.dart 封装组件
|
||||||
# 版本:1.1.1(2026-07 发布,支持 persistent + modal 两种模式)
|
# 版本:1.1.1(2026-07 发布,支持 persistent + modal 两种模式)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
|
#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
|
||||||
#include <gal/gal_plugin_c_api.h>
|
#include <gal/gal_plugin_c_api.h>
|
||||||
@@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
FlutterTimezonePluginCApiRegisterWithRegistrar(
|
FlutterTimezonePluginCApiRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
flutter_inappwebview_windows
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
flutter_timezone
|
flutter_timezone
|
||||||
gal
|
gal
|
||||||
|
|||||||
Reference in New Issue
Block a user