feat: 添加登录页面 URL 检测功能;更新 H5ShellPage 状态管理以支持路由变化

This commit is contained in:
Booker
2026-05-27 13:16:35 +07:00
parent 6e3920972b
commit 179a2332ae
3 changed files with 119 additions and 1 deletions

View File

@@ -83,6 +83,19 @@ class AppConfig {
return url;
}
static bool isLoginPageUrl(String? url) {
if (url == null || url.isEmpty) {
return false;
}
final uri = Uri.tryParse(url);
if (uri == null) {
return false;
}
return _isLoginPath(uri.path) || _isLoginPath(uri.fragment);
}
static int? lineIndexForUrl(String url) {
final uri = Uri.tryParse(url);
if (uri == null) {
@@ -123,6 +136,21 @@ class AppConfig {
return index;
}
static bool _isLoginPath(String path) {
final normalized = path.trim();
if (normalized.isEmpty) {
return false;
}
final pathOnly = normalized.split('?').first.split('#').first;
final segments = pathOnly.split('/').where((segment) => segment.isNotEmpty);
if (segments.isEmpty) {
return false;
}
return segments.last == 'login';
}
static bool _isSameLine(Uri lineUri, Uri uri) {
if (lineUri.host.toLowerCase() != uri.host.toLowerCase()) {
return false;

View File

@@ -133,11 +133,12 @@ class _H5ShellPageState extends State<H5ShellPage> {
setState(() => _lineSlots[lineIndex].progress = progress);
}
},
onPageStarted: (_) {
onPageStarted: (url) {
final slot = _lineSlots[lineIndex];
slot.shellCoverFallbackTimer?.cancel();
if (mounted) {
setState(() {
slot.setCurrentUrl(url);
slot.loadError = null;
slot.progress = 0;
slot.showShellCover = true;
@@ -147,6 +148,9 @@ class _H5ShellPageState extends State<H5ShellPage> {
onPageFinished: (_) {
unawaited(_handlePageFinished(lineIndex));
},
onUrlChange: (change) {
_updateSlotUrl(lineIndex, change.url);
},
onWebResourceError: (error) {
if (error.isForMainFrame ?? true) {
final slot = _lineSlots[lineIndex];
@@ -183,6 +187,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
Future<void> _handlePageFinished(int lineIndex) async {
await _loadShellBrandingIfNeeded();
await _syncShellBranding(lineIndex);
await _installRouteObserver(lineIndex);
if (mounted) {
setState(() {
_lineSlots[lineIndex].progress = 100;
@@ -196,6 +201,8 @@ class _H5ShellPageState extends State<H5ShellPage> {
final decoded = jsonDecode(message.message);
if (decoded is Map && decoded['type'] == 'first-screen-ready') {
_hideShellCover(lineIndex);
} else if (decoded is Map && decoded['type'] == 'route-changed') {
_updateSlotUrl(lineIndex, decoded['url']?.toString());
}
} catch (_) {
if (message.message == 'first-screen-ready') {
@@ -260,6 +267,60 @@ class _H5ShellPageState extends State<H5ShellPage> {
return _runJavaScriptSafely(lineIndex, script);
}
Future<void> _installRouteObserver(int lineIndex) {
const script = '''
(() => {
try {
const notify = () => {
try {
window.OpenIMShell.postMessage(JSON.stringify({
type: 'route-changed',
url: window.location.href
}));
} catch (_) {}
};
if (window.__OPENIM_FLUTTER_ROUTE_OBSERVER__) {
window.__OPENIM_FLUTTER_ROUTE_OBSERVER__.notify();
return;
}
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
window.history.pushState = function() {
const result = originalPushState.apply(this, arguments);
notify();
return result;
};
window.history.replaceState = function() {
const result = originalReplaceState.apply(this, arguments);
notify();
return result;
};
window.addEventListener('popstate', notify);
window.__OPENIM_FLUTTER_ROUTE_OBSERVER__ = { notify };
notify();
} catch (_) {}
})();
''';
return _runJavaScriptSafely(lineIndex, script);
}
void _updateSlotUrl(int lineIndex, String? url) {
if (url == null || lineIndex < 0 || lineIndex >= _lineSlots.length) {
return;
}
final slot = _lineSlots[lineIndex];
final changed = slot.setCurrentUrl(url);
if (changed && mounted) {
setState(() {});
}
}
Future<void> _ensureLineLoaded(int lineIndex) async {
final slot = _lineSlots[lineIndex];
if (slot.hasLoadedInitialRequest) {
@@ -270,6 +331,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
if (mounted) {
slot.shellCoverFallbackTimer?.cancel();
setState(() {
slot.setCurrentUrl(slot.line.url);
slot.loadError = null;
slot.progress = 0;
slot.showShellCover = true;
@@ -369,6 +431,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
final showLineSwitch = !currentSlot.showShellCover &&
currentSlot.loadError == null &&
currentSlot.isLoginPage &&
bottomInset == 0;
return PopScope(
@@ -448,8 +511,21 @@ class _H5LineWebViewSlot {
String? loadError;
bool showShellCover = true;
bool hasLoadedInitialRequest = false;
bool isLoginPage = false;
String? currentUrl;
Timer? shellCoverFallbackTimer;
bool setCurrentUrl(String url) {
final nextIsLoginPage = AppConfig.isLoginPageUrl(url);
if (currentUrl == url && isLoginPage == nextIsLoginPage) {
return false;
}
currentUrl = url;
isLoginPage = nextIsLoginPage;
return true;
}
void dispose() {
shellCoverFallbackTimer?.cancel();
}

View File

@@ -50,6 +50,20 @@ void main() {
expect(AppConfig.lineIndexForUrl('${lineUrl}login'), 0);
});
test('detects only H5 login routes for shell line switch display', () {
expect(
AppConfig.isLoginPageUrl('https://h5-test.imharry.work/login'), isTrue);
expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/app/login'),
isTrue);
expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/#/login'),
isTrue);
expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/'), isFalse);
expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/contact'),
isFalse);
expect(AppConfig.isLoginPageUrl('https://h5-test.imharry.work/getCode'),
isFalse);
});
test('refreshes an H5 route URL without adding branding to the URL', () {
final uri = Uri.parse(
AppConfig.withFreshShellParams(