缺失文件

master
hs 2026-08-10 14:11:12 +08:00
parent f4adb4e35b
commit f9f66882ae
3 changed files with 324 additions and 0 deletions

View File

@ -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),
),
);
}
}

View File

@ -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),
),
],
),
);
}
}

View File

@ -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位且必须包含大写字母、小写字母、数字和特殊符号!#@*&)。';
}
}