feat(bridge): 新增 Phase 2 协议函数 — getDeviceInfo / getNetworkStatus / showToast / setStatusBar
- 新增 device_info_plus 和 connectivity_plus 依赖 - bridge_protocol.dart: AppShell API 从 8 个扩展到 12 个 - bridge_actions.dart: 实现 4 个 Dart 处理器 + 原生风格 Toast 动效组件 - shell_page.dart: 新增 4 个 action 分支 - test_bridge.html: 新增 4 个测试区块,Bridge 检测覆盖全部 12 个方法
This commit is contained in:
parent
dad37faecb
commit
d4f7899351
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,197 @@
|
|||
part of '../../core_app.dart';
|
||||
|
||||
// 当前无需在 `bridge_actions.dart` 中编写独立代码 ——
|
||||
// 桥接动作处理逻辑直接内嵌在 `shell_page.dart` 的 `_handleBridgeMessage` 方法中。
|
||||
// 此文件预留给后续将动作处理器拆分出去时使用。
|
||||
/// 获取设备信息,包括 Android 版本、设备型号、WebView 版本等。
|
||||
Future<Map<String, dynamic>> _getDeviceInfoFromBridge() async {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
return <String, dynamic>{
|
||||
'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<Map<String, dynamic>> _getNetworkStatusFromBridge() async {
|
||||
final results = await Connectivity().checkConnectivity();
|
||||
// connectivity_plus 返回 List<ConnectivityResult>
|
||||
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 <String, dynamic>{
|
||||
'connected': primary != ConnectivityResult.none,
|
||||
'type': type,
|
||||
};
|
||||
}
|
||||
|
||||
/// 显示原生 Toast 提示。
|
||||
///
|
||||
/// 使用 Overlay 实现,不依赖额外插件。
|
||||
void _showToastFromBridge(BuildContext context, Map<String, dynamic> 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<String, dynamic> 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<double> _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<void>.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -644,6 +644,16 @@ class _WebShellPageState extends State<WebShellPage>
|
|||
);
|
||||
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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -322,6 +322,41 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ 8.5 Phase 2: 新增能力 ══════════ -->
|
||||
<div class="section-title">8.5 设备信息 getDeviceInfo</div>
|
||||
<div class="card">
|
||||
<div class="btn-row">
|
||||
<button onclick="testGetDeviceInfo()">📱 获取设备信息</button>
|
||||
</div>
|
||||
<div class="preview-area" id="deviceInfoBridgePreview">点击按钮通过 Bridge 获取完整设备信息</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">8.6 网络状态 getNetworkStatus</div>
|
||||
<div class="card">
|
||||
<div class="btn-row">
|
||||
<button onclick="testGetNetworkStatus()">📡 获取网络状态</button>
|
||||
</div>
|
||||
<div class="preview-area" id="networkPreview">点击按钮获取当前网络连接类型</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">8.7 原生 Toast showToast</div>
|
||||
<div class="card">
|
||||
<div class="btn-row">
|
||||
<button onclick="testShowToast('short')">💬 短 Toast</button>
|
||||
<button class="secondary" onclick="testShowToast('long')">💬 长 Toast</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">8.8 状态栏控制 setStatusBar</div>
|
||||
<div class="card">
|
||||
<div class="btn-row">
|
||||
<button onclick="testSetStatusBar('light')">☀️ 亮色图标</button>
|
||||
<button class="secondary" onclick="testSetStatusBar('dark')">🌙 暗色图标</button>
|
||||
<button class="warn" onclick="testSetStatusBar('accent')">🎨 主题色背景</button>
|
||||
<button onclick="testSetStatusBar('reset')">🔄 重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ 9. 旧相机兼容层 ══════════ -->
|
||||
<div class="section-title">9. 旧相机兼容层 Legacy Camera</div>
|
||||
<div class="card">
|
||||
|
|
@ -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 = '<b>设备信息 (Bridge):</b><br>';
|
||||
const keys = Object.keys(info || {});
|
||||
keys.forEach(k => {
|
||||
html += `<b>${k}:</b> ${info[k]}<br>`;
|
||||
});
|
||||
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} <b>连接状态:</b> ${status.connected ? '已连接' : '未连接'}<br>`;
|
||||
html += `<b>网络类型:</b> ${status.type}<br>`;
|
||||
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. 旧相机兼容层
|
||||
// ═══════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Reference in New Issue