缺失文件
parent
f4adb4e35b
commit
f9f66882ae
|
|
@ -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<WebViewMediaBridgePage> createState() => _WebViewMediaBridgePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WebViewMediaBridgePageState extends State<WebViewMediaBridgePage> {
|
||||||
|
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<void> _injectBridge() {
|
||||||
|
return _controller.runJavaScript('''
|
||||||
|
window.flutterSendToDart = function(message) {
|
||||||
|
if (window.$_jsChannelName && window.$_jsChannelName.postMessage) {
|
||||||
|
window.$_jsChannelName.postMessage(String(message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleWebMessage(String raw) async {
|
||||||
|
Map<String, dynamic> data;
|
||||||
|
try {
|
||||||
|
data = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
|
} 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<void> _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<void> _replyToWeb(String? callbackId, Map<String, dynamic> 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<bool> Function(String oldPassword, String newPassword)
|
||||||
|
onUpdatePassword;
|
||||||
|
|
||||||
|
static Future<bool?> show(
|
||||||
|
BuildContext context, {
|
||||||
|
required String oldPassword,
|
||||||
|
required Future<bool> Function(String oldPassword, String newPassword)
|
||||||
|
onUpdatePassword,
|
||||||
|
}) {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder:
|
||||||
|
(_) => ForceChangePasswordDialog(
|
||||||
|
oldPassword: oldPassword,
|
||||||
|
onUpdatePassword: onUpdatePassword,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ForceChangePasswordDialog> createState() =>
|
||||||
|
_ForceChangePasswordDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ForceChangePasswordDialogState extends State<ForceChangePasswordDialog> {
|
||||||
|
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<bool> _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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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位,且必须包含大写字母、小写字母、数字和特殊符号(!#@*&)。';
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue