diff --git a/packages/web_shell_core/lib/core_app.dart b/packages/web_shell_core/lib/core_app.dart index ad081aa..5fb3f6f 100644 --- a/packages/web_shell_core/lib/core_app.dart +++ b/packages/web_shell_core/lib/core_app.dart @@ -4,6 +4,8 @@ library; import 'dart:async'; import 'dart:convert'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/packages/web_shell_core/lib/src/bridge/bridge_actions.dart b/packages/web_shell_core/lib/src/bridge/bridge_actions.dart index c6459df..b3f0831 100644 --- a/packages/web_shell_core/lib/src/bridge/bridge_actions.dart +++ b/packages/web_shell_core/lib/src/bridge/bridge_actions.dart @@ -1,5 +1,197 @@ part of '../../core_app.dart'; -// 当前无需在 `bridge_actions.dart` 中编写独立代码 —— -// 桥接动作处理逻辑直接内嵌在 `shell_page.dart` 的 `_handleBridgeMessage` 方法中。 -// 此文件预留给后续将动作处理器拆分出去时使用。 +/// 获取设备信息,包括 Android 版本、设备型号、WebView 版本等。 +Future> _getDeviceInfoFromBridge() async { + final androidInfo = await DeviceInfoPlugin().androidInfo; + return { + 'platform': 'android', + 'brand': androidInfo.brand, + 'model': androidInfo.model, + 'manufacturer': androidInfo.manufacturer, + 'androidVersion': androidInfo.version.release, + 'sdkInt': androidInfo.version.sdkInt, + 'isPhysicalDevice': androidInfo.isPhysicalDevice, + 'display': androidInfo.display, + 'product': androidInfo.product, + 'appName': _env.appName, + 'appKey': _env.appKey, + 'shellVersion': _appShellBridgeVersion, + }; +} + +/// 获取当前网络连接状态及类型。 +Future> _getNetworkStatusFromBridge() async { + final results = await Connectivity().checkConnectivity(); + // connectivity_plus 返回 List + final primary = + results.isNotEmpty ? results.first : ConnectivityResult.none; + + String type; + switch (primary) { + case ConnectivityResult.wifi: + type = 'wifi'; + case ConnectivityResult.mobile: + type = 'cellular'; + case ConnectivityResult.ethernet: + type = 'ethernet'; + case ConnectivityResult.bluetooth: + type = 'bluetooth'; + case ConnectivityResult.vpn: + type = 'vpn'; + case ConnectivityResult.none: + type = 'none'; + case ConnectivityResult.other: + type = 'unknown'; + } + + return { + 'connected': primary != ConnectivityResult.none, + 'type': type, + }; +} + +/// 显示原生 Toast 提示。 +/// +/// 使用 Overlay 实现,不依赖额外插件。 +void _showToastFromBridge(BuildContext context, Map payload) { + final message = (payload['message'] ?? '').toString(); + if (message.isEmpty) return; + + final duration = switch ((payload['duration'] ?? 'short').toString()) { + 'long' => const Duration(milliseconds: 3500), + _ => const Duration(milliseconds: 2000), + }; + + final overlay = Overlay.of(context); + late final OverlayEntry entry; + + entry = OverlayEntry( + builder: (context) => _ToastWidget( + message: message, + duration: duration, + onDismiss: () => entry.remove(), + ), + ); + + overlay.insert(entry); +} + +/// 设置系统状态栏样式。 +void _setStatusBarFromBridge(Map payload) { + final style = (payload['style'] ?? '').toString(); + final color = payload['color']?.toString(); + + Brightness? brightness; + if (style == 'light') { + brightness = Brightness.light; + } else if (style == 'dark') { + brightness = Brightness.dark; + } + + Color? bgColor; + if (color != null && color.isNotEmpty) { + final parsed = int.tryParse( + color.replaceFirst('#', '0xFF'), + ); + if (parsed != null) bgColor = Color(parsed); + } + + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + statusBarIconBrightness: brightness, + statusBarColor: bgColor ?? Colors.transparent, + ), + ); +} + +// ── Toast 动效组件 ── + +class _ToastWidget extends StatefulWidget { + const _ToastWidget({ + required this.message, + required this.duration, + required this.onDismiss, + }); + + final String message; + final Duration duration; + final VoidCallback onDismiss; + + @override + State<_ToastWidget> createState() => _ToastWidgetState(); +} + +class _ToastWidgetState extends State<_ToastWidget> + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 250), + ); + _fadeAnimation = CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ); + + unawaited(_controller.forward()); + + unawaited( + Future.delayed(widget.duration).then((_) { + if (mounted) { + unawaited( + _controller.reverse().then((_) { + if (mounted) widget.onDismiss(); + }), + ); + } + }), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Positioned( + bottom: 100, + left: 0, + right: 0, + child: Center( + child: FadeTransition( + opacity: _fadeAnimation, + child: Material( + color: Colors.transparent, + child: Container( + constraints: const BoxConstraints(maxWidth: 300), + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + decoration: BoxDecoration( + color: const Color(0xDD333333), + borderRadius: BorderRadius.circular(24), + ), + child: Text( + widget.message, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/web_shell_core/lib/src/bridge/bridge_protocol.dart b/packages/web_shell_core/lib/src/bridge/bridge_protocol.dart index 9a098a3..9ed37ae 100644 --- a/packages/web_shell_core/lib/src/bridge/bridge_protocol.dart +++ b/packages/web_shell_core/lib/src/bridge/bridge_protocol.dart @@ -93,6 +93,10 @@ String _buildAppShellBridgeScript() { reloadPage: () => send('reloadPage'), goBack: () => send('goBack'), closeApp: () => send('closeApp'), + getDeviceInfo: () => send('getDeviceInfo'), + getNetworkStatus: () => send('getNetworkStatus'), + showToast: (message, options = {}) => send('showToast', { message, ...options }), + setStatusBar: (options = {}) => send('setStatusBar', options), }; window.dispatchEvent(new CustomEvent('app-shell-ready', { diff --git a/packages/web_shell_core/lib/src/ui/shell_page.dart b/packages/web_shell_core/lib/src/ui/shell_page.dart index 1d3625b..a4b08ca 100644 --- a/packages/web_shell_core/lib/src/ui/shell_page.dart +++ b/packages/web_shell_core/lib/src/ui/shell_page.dart @@ -644,6 +644,16 @@ class _WebShellPageState extends State ); await SystemNavigator.pop(); return; + case 'getDeviceInfo': + data = await _getDeviceInfoFromBridge(); + case 'getNetworkStatus': + data = await _getNetworkStatusFromBridge(); + case 'showToast': + _showToastFromBridge(context, payload); + data = true; + case 'setStatusBar': + _setStatusBarFromBridge(payload); + data = true; default: throw UnsupportedError('Unsupported AppShell action: $action'); } diff --git a/packages/web_shell_core/pubspec.yaml b/packages/web_shell_core/pubspec.yaml index 03417c9..bc412a0 100644 --- a/packages/web_shell_core/pubspec.yaml +++ b/packages/web_shell_core/pubspec.yaml @@ -8,6 +8,8 @@ environment: flutter: '>=3.3.0' dependencies: + connectivity_plus: ^7.0.0 + device_info_plus: ^12.3.0 file_picker: ^10.3.10 flutter: sdk: flutter diff --git a/packages/web_shell_core/test_assets/test_bridge.html b/packages/web_shell_core/test_assets/test_bridge.html index 1abb74a..cbb60f7 100644 --- a/packages/web_shell_core/test_assets/test_bridge.html +++ b/packages/web_shell_core/test_assets/test_bridge.html @@ -322,6 +322,41 @@ + +
8.5 设备信息 getDeviceInfo
+
+
+ +
+
点击按钮通过 Bridge 获取完整设备信息
+
+ +
8.6 网络状态 getNetworkStatus
+
+
+ +
+
点击按钮获取当前网络连接类型
+
+ +
8.7 原生 Toast showToast
+
+
+ + +
+
+ +
8.8 状态栏控制 setStatusBar
+
+
+ + + + +
+
+
9. 旧相机兼容层 Legacy Camera
@@ -424,7 +459,7 @@ function checkBridge() { log('err', 'Bridge', 'window.AppShell 未定义'); return; } - const methods = ['pickImage','captureImage','pickFile','openExternal','requestPermissions','reloadPage','goBack','closeApp']; + const methods = ['pickImage','captureImage','pickFile','openExternal','requestPermissions','reloadPage','goBack','closeApp','getDeviceInfo','getNetworkStatus','showToast','setStatusBar']; const available = methods.filter(m => typeof shell[m] === 'function'); const missing = methods.filter(m => typeof shell[m] !== 'function'); @@ -632,6 +667,73 @@ async function testCloseApp() { } } +// ═══════════════════════════════════════ +// 8.5–8.8 Phase 2 新增能力 +// ═══════════════════════════════════════ +async function testGetDeviceInfo() { + if (!window.AppShell) { log('err', 'DeviceInfo', 'AppShell 不可用'); return; } + log('info', 'DeviceInfo', '调用 getDeviceInfo()…'); + try { + const info = await AppShell.getDeviceInfo(); + let html = '设备信息 (Bridge):
'; + const keys = Object.keys(info || {}); + keys.forEach(k => { + html += `${k}: ${info[k]}
`; + }); + setPreview('deviceInfoBridgePreview', html); + log('ok', 'DeviceInfo', info); + } catch (e) { + setPreview('deviceInfoBridgePreview', '❌ 错误: ' + e.message); + log('err', 'DeviceInfo', e.message); + } +} + +async function testGetNetworkStatus() { + if (!window.AppShell) { log('err', 'Network', 'AppShell 不可用'); return; } + log('info', 'Network', '调用 getNetworkStatus()…'); + try { + const status = await AppShell.getNetworkStatus(); + const icon = status.connected ? '✅' : '❌'; + let html = `${icon} 连接状态: ${status.connected ? '已连接' : '未连接'}
`; + html += `网络类型: ${status.type}
`; + setPreview('networkPreview', html); + log('ok', 'Network', status); + } catch (e) { + setPreview('networkPreview', '❌ 错误: ' + e.message); + log('err', 'Network', e.message); + } +} + +async function testShowToast(duration) { + if (!window.AppShell) { log('err', 'Toast', 'AppShell 不可用'); return; } + const msg = duration === 'long' ? '这是一条长时间的原生 Toast 提示 (3.5s)' : '原生 Toast 测试 ✅'; + log('info', 'Toast', `调用 showToast('${msg}', {duration: '${duration}'})…`); + try { + await AppShell.showToast(msg, { duration }); + log('ok', 'Toast', 'showToast 完成'); + } catch (e) { + log('err', 'Toast', e.message); + } +} + +async function testSetStatusBar(preset) { + if (!window.AppShell) { log('err', 'StatusBar', 'AppShell 不可用'); return; } + let options = {}; + switch (preset) { + case 'light': options = { style: 'light' }; break; + case 'dark': options = { style: 'dark' }; break; + case 'accent': options = { style: 'light', color: '#3ED37B' }; break; + case 'reset': options = { style: 'dark', color: '#00000000' }; break; + } + log('info', 'StatusBar', `调用 setStatusBar(${JSON.stringify(options)})…`); + try { + await AppShell.setStatusBar(options); + log('ok', 'StatusBar', 'setStatusBar 完成, preset=' + preset); + } catch (e) { + log('err', 'StatusBar', e.message); + } +} + // ═══════════════════════════════════════ // 9. 旧相机兼容层 // ═══════════════════════════════════════