diff --git a/lib/pages/WebViewInteractionPage.dart b/lib/pages/WebViewInteractionPage.dart new file mode 100644 index 0000000..56699dd --- /dev/null +++ b/lib/pages/WebViewInteractionPage.dart @@ -0,0 +1,166 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dart_sm/dart_sm.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebViewMediaBridgePage extends StatefulWidget { + const WebViewMediaBridgePage({super.key, required this.userId}); + + final String userId; + + @override + State createState() => _WebViewMediaBridgePageState(); +} + +class _WebViewMediaBridgePageState extends State { + static const _jsChannelName = 'WebViewBridge'; + static const _publicKey = + '049818e94f1abefab63d7193dd71b16241b5e721ab936eb1d4abb84f5024cb5d5b503a1776163cce4dd0f0ffdc612329f3497da3ac9b643306d5bfbe844459d186'; + static const _baseUrl = + 'http://121.22.11.43:8091/emisApp/#/pages/index?user='; + + final ImagePicker _imagePicker = ImagePicker(); + late final WebViewController _controller; + + @override + void initState() { + super.initState(); + final encryptedUser = SM2.encrypt(widget.userId, _publicKey); + final targetUrl = '$_baseUrl$encryptedUser'; + + _controller = + WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Colors.white) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (_) => _injectBridge(), + onWebResourceError: (error) { + debugPrint('设备管理 WebView 加载错误: ${error.description}'); + }, + ), + ) + ..addJavaScriptChannel( + _jsChannelName, + onMessageReceived: (message) => _handleWebMessage(message.message), + ) + ..loadRequest(Uri.parse(targetUrl)); + } + + Future _injectBridge() { + return _controller.runJavaScript(''' + window.flutterSendToDart = function(message) { + if (window.$_jsChannelName && window.$_jsChannelName.postMessage) { + window.$_jsChannelName.postMessage(String(message)); + } + }; + '''); + } + + Future _handleWebMessage(String raw) async { + Map data; + try { + data = jsonDecode(raw) as Map; + } catch (_) { + data = {'action': raw}; + } + + final action = data['action']?.toString(); + final callbackId = data['callbackId']?.toString(); + switch (action) { + case 'camera': + await _pickImageAndCallback(ImageSource.camera, callbackId); + return; + case 'gallery': + await _pickImageAndCallback(ImageSource.gallery, callbackId); + return; + case 'back': + if (mounted) Navigator.of(context).pop(); + return; + case 'ping': + await _replyToWeb(callbackId, { + 'ok': true, + 'message': 'pong from flutter', + 'time': DateTime.now().toIso8601String(), + }); + return; + default: + debugPrint('设备管理 WebView 未知 action: $action'); + } + } + + Future _pickImageAndCallback( + ImageSource source, + String? callbackId, + ) async { + try { + final file = await _imagePicker.pickImage( + source: source, + imageQuality: 85, + ); + if (file == null) { + await _replyToWeb(callbackId, { + 'ok': false, + 'cancelled': true, + 'message': '用户取消选择', + }); + return; + } + + final Uint8List bytes = await file.readAsBytes(); + final base64Data = base64Encode(bytes); + final mimeType = _guessMimeType(file.path); + await _replyToWeb(callbackId, { + 'ok': true, + 'name': file.name, + 'path': file.path, + 'mimeType': mimeType, + 'base64': base64Data, + 'dataUrl': 'data:$mimeType;base64,$base64Data', + }); + } catch (error) { + debugPrint('设备管理图片选择失败: $error'); + await _replyToWeb(callbackId, {'ok': false, 'message': error.toString()}); + } + } + + String _guessMimeType(String path) { + final lower = path.toLowerCase(); + if (lower.endsWith('.png')) return 'image/png'; + if (lower.endsWith('.webp')) return 'image/webp'; + if (lower.endsWith('.gif')) return 'image/gif'; + if (lower.endsWith('.heic')) return 'image/heic'; + return 'image/jpeg'; + } + + Future _replyToWeb(String? callbackId, Map payload) { + final jsonText = jsonEncode(payload); + if (callbackId == null || callbackId.isEmpty) { + return _controller.runJavaScript(''' + if (window.onFlutterResult) window.onFlutterResult($jsonText); + '''); + } + return _controller.runJavaScript(''' + if (window.WebViewCallbacks && window.WebViewCallbacks['$callbackId']) { + window.WebViewCallbacks['$callbackId']($jsonText); + delete window.WebViewCallbacks['$callbackId']; + } else if (window.onFlutterResult) { + window.onFlutterResult($jsonText); + } + '''); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + top: false, + bottom: false, + child: WebViewWidget(controller: _controller), + ), + ); + } +} diff --git a/lib/pages/mine/force_change_password_dialog.dart b/lib/pages/mine/force_change_password_dialog.dart new file mode 100644 index 0000000..abdef46 --- /dev/null +++ b/lib/pages/mine/force_change_password_dialog.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/services/password_policy.dart'; + +class ForceChangePasswordDialog extends StatefulWidget { + const ForceChangePasswordDialog({ + super.key, + required this.oldPassword, + required this.onUpdatePassword, + }); + + final String oldPassword; + final Future Function(String oldPassword, String newPassword) + onUpdatePassword; + + static Future show( + BuildContext context, { + required String oldPassword, + required Future Function(String oldPassword, String newPassword) + onUpdatePassword, + }) { + return showDialog( + context: context, + barrierDismissible: false, + builder: + (_) => ForceChangePasswordDialog( + oldPassword: oldPassword, + onUpdatePassword: onUpdatePassword, + ), + ); + } + + @override + State createState() => + _ForceChangePasswordDialogState(); +} + +class _ForceChangePasswordDialogState extends State { + late final TextEditingController _oldPasswordController; + final _newPasswordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + _oldPasswordController = TextEditingController(text: widget.oldPassword); + } + + @override + void dispose() { + _oldPasswordController.dispose(); + _newPasswordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _submit() async { + if (_isSubmitting) return false; + + final newPassword = _newPasswordController.text; + final confirmPassword = _confirmPasswordController.text; + final passwordError = PasswordPolicy.validate(newPassword); + if (passwordError != null) { + ToastUtil.showNormal(context, passwordError); + return false; + } + if (newPassword != confirmPassword) { + ToastUtil.showNormal(context, '新密码和确认密码不一致'); + return false; + } + + setState(() => _isSubmitting = true); + try { + final updated = await widget.onUpdatePassword( + _oldPasswordController.text, + newPassword, + ); + if (!updated && mounted) { + ToastUtil.showNormal(context, '密码修改失败,请重试'); + } + return updated; + } catch (_) { + if (mounted) { + ToastUtil.showNormal(context, '密码修改失败,请重试'); + } + return false; + } finally { + if (mounted) { + setState(() => _isSubmitting = false); + } + } + } + + Widget _buildPasswordField(String hint, TextEditingController controller) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TextField( + controller: controller, + obscureText: true, + decoration: InputDecoration( + hintText: hint, + border: const UnderlineInputBorder(), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFD9D9D9)), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFF1C61FF)), + ), + isDense: true, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return CustomAlertDialog( + title: '请修改默认密码', + confirmText: _isSubmitting ? '提交中...' : '提交', + force: true, + onConfirmAsync: _submit, + customContent: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('为保障账号安全,请先修改默认密码。'), + const SizedBox(height: 12), + _buildPasswordField('当前密码', _oldPasswordController), + _buildPasswordField('新密码', _newPasswordController), + _buildPasswordField('确认新密码', _confirmPasswordController), + const Text( + '密码长度为8-16位,且必须包含大写字母、小写字母、数字和特殊符号(!#@*&)。', + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ), + ); + } +} diff --git a/lib/services/password_policy.dart b/lib/services/password_policy.dart new file mode 100644 index 0000000..e9cb2c8 --- /dev/null +++ b/lib/services/password_policy.dart @@ -0,0 +1,18 @@ +class PasswordPolicy { + static const defaultPassword = 'Cc@12345678'; + + static final RegExp _validPassword = RegExp( + r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!#@*&])[A-Za-z\d!#@*&]{8,16}$', + ); + + static bool requiresChange(String? password) { + return password == defaultPassword; + } + + static String? validate(String password) { + if (_validPassword.hasMatch(password)) { + return null; + } + return '密码长度为8-16位,且必须包含大写字母、小写字母、数字和特殊符号(!#@*&)。'; + } +}