缺失文件
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