feat: init 改为 测试web
This commit is contained in:
parent
498fa823a0
commit
fb84e6f4fe
|
|
@ -1,45 +1,48 @@
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
*.class
|
*.class
|
||||||
*.log
|
*.log
|
||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.atom/
|
.atom/
|
||||||
.build/
|
.build/
|
||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
.swiftpm/
|
.swiftpm/
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
*.iws
|
*.iws
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
# is commented out by default.
|
# is commented out by default.
|
||||||
#.vscode/
|
#.vscode/
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
# Flutter/Dart/Pub related
|
||||||
**/doc/api/
|
**/doc/api/
|
||||||
**/ios/Flutter/.last_build_id
|
**/ios/Flutter/.last_build_id
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
/coverage/
|
/coverage/
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|
||||||
# Obfuscation related
|
# Obfuscation related
|
||||||
app.*.map.json
|
app.*.map.json
|
||||||
|
|
||||||
# Android Studio will place build artifacts here
|
# Android Studio will place build artifacts here
|
||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
# FVM Version Cache
|
||||||
|
.fvm/
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "web_android_shell",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quanxue",
|
||||||
|
"cwd": "apps/quanxue",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quanxue (profile mode)",
|
||||||
|
"cwd": "apps/quanxue",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quanxue (release mode)",
|
||||||
|
"cwd": "apps/quanxue",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_android_shell",
|
||||||
|
"cwd": "packages/web_android_shell",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_android_shell (profile mode)",
|
||||||
|
"cwd": "packages/web_android_shell",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_android_shell (release mode)",
|
||||||
|
"cwd": "packages/web_android_shell",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_shell_core",
|
||||||
|
"cwd": "packages/web_shell_core",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_shell_core (profile mode)",
|
||||||
|
"cwd": "packages/web_shell_core",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "web_shell_core (release mode)",
|
||||||
|
"cwd": "packages/web_shell_core",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"dart.flutterSdkPath": ".fvm/versions/3.41.2"
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ void main() {
|
||||||
backgroundColor: const Color(0xFFFFFFFF),
|
backgroundColor: const Color(0xFFFFFFFF),
|
||||||
textColor: const Color(0xFF1F2937),
|
textColor: const Color(0xFF1F2937),
|
||||||
mutedTextColor: const Color(0xFF6B7280),
|
mutedTextColor: const Color(0xFF6B7280),
|
||||||
|
initialUrl: "http://192.168.2.57:8080/test_bridge.html",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,769 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>WebShell Core 功能验收测试</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--accent: #3ED37B;
|
||||||
|
--accent-light: #e8f9ef;
|
||||||
|
--bg: #f7f8fa;
|
||||||
|
--card-bg: #fff;
|
||||||
|
--text: #1f2937;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--border: #e5e7eb;
|
||||||
|
--success: #10b981;
|
||||||
|
--error: #ef4444;
|
||||||
|
--warn: #f59e0b;
|
||||||
|
--info: #3b82f6;
|
||||||
|
--radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-bottom: 220px; /* 给日志区留空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 顶部状态栏 ── */
|
||||||
|
.status-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: linear-gradient(135deg, #1f2937, #374151);
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.status-dot {
|
||||||
|
width: 10px; height: 10px; border-radius: 50%;
|
||||||
|
background: var(--error);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.status-dot.ready { background: var(--success); }
|
||||||
|
.status-text { flex: 1; font-size: 13px; }
|
||||||
|
.status-version { font-size: 12px; opacity: 0.7; }
|
||||||
|
|
||||||
|
/* ── 卡片区 ── */
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 16px 12px 8px;
|
||||||
|
color: var(--text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.section-title::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px; height: 18px;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin: 0 12px 12px;
|
||||||
|
padding: 14px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.card-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s, opacity 0.2s;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
button:active { transform: scale(0.95); opacity: 0.85; }
|
||||||
|
button.secondary { background: var(--info); }
|
||||||
|
button.warn { background: var(--warn); color: #fff; }
|
||||||
|
button.danger { background: var(--error); }
|
||||||
|
|
||||||
|
.preview-area {
|
||||||
|
min-height: 40px;
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.preview-area img {
|
||||||
|
max-width: 120px;
|
||||||
|
max-height: 120px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 原生 <a> 标签区域 ── */
|
||||||
|
.link-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.link-row a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--info);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 文件输入区 ── */
|
||||||
|
.file-input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.file-input-group label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.file-input-group input[type="file"] { display: none; }
|
||||||
|
|
||||||
|
/* ── 日志面板 ── */
|
||||||
|
.log-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 200px;
|
||||||
|
background: #1e1e2e;
|
||||||
|
border-top: 3px solid var(--accent);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
.log-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #2a2a3d;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.log-header button {
|
||||||
|
background: transparent;
|
||||||
|
color: #ccc;
|
||||||
|
padding: 2px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
.log-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-family: "SF Mono", "Menlo", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.log-entry { margin-bottom: 2px; }
|
||||||
|
.log-time { color: #888; }
|
||||||
|
.log-ok { color: #4ade80; }
|
||||||
|
.log-err { color: #f87171; }
|
||||||
|
.log-warn { color: #fbbf24; }
|
||||||
|
.log-info { color: #60a5fa; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ══════════ 状态栏 ══════════ -->
|
||||||
|
<div class="status-bar">
|
||||||
|
<div class="status-dot" id="statusDot"></div>
|
||||||
|
<div class="status-text" id="statusText">等待 AppShell 桥接注入…</div>
|
||||||
|
<div class="status-version" id="statusVersion"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 1. 环境检测 ══════════ -->
|
||||||
|
<div class="section-title">1. 环境检测</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Bridge 状态</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button onclick="checkBridge()">检测 Bridge</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-area" id="envPreview">点击按钮检测 AppShell Bridge 状态</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 2. 核心 WebView 行为 ══════════ -->
|
||||||
|
<div class="section-title">2. 核心 WebView 行为</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">原生 <a> 标签跳转(非 Bridge)</div>
|
||||||
|
<div class="link-row">
|
||||||
|
<a href="tel:10086">📞 tel:10086</a>
|
||||||
|
<a href="mailto:test@example.com">📧 mailto</a>
|
||||||
|
<a href="market://details?id=com.android.chrome">🛒 market://</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-label" style="margin-top:10px;">HTML5 Geolocation</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="secondary" onclick="testGeolocation()">📍 获取位置</button>
|
||||||
|
<button class="secondary" onclick="testAlert()">💬 测试 Alert</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-area" id="webviewPreview">点击按钮测试 WebView 原生行为</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 3. captureImage ══════════ -->
|
||||||
|
<div class="section-title">3. 拍照 captureImage</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">响应格式</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button onclick="testCaptureImage('dataUrl')">📸 dataUrl</button>
|
||||||
|
<button onclick="testCaptureImage('base64')">📸 base64</button>
|
||||||
|
<button onclick="testCaptureImage('uri')">📸 uri</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-area" id="capturePreview">拍照结果将在此显示</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 4. pickImage ══════════ -->
|
||||||
|
<div class="section-title">4. 图库选图 pickImage</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="btn-row">
|
||||||
|
<button onclick="testPickImage(false)">🖼 单选</button>
|
||||||
|
<button onclick="testPickImage(true)">🖼 多选</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-area" id="pickImagePreview">选图结果将在此显示</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 5. pickFile ══════════ -->
|
||||||
|
<div class="section-title">5. 文件选择 pickFile</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="btn-row">
|
||||||
|
<button onclick="testPickFile(false, 'uri')">📄 单选 uri</button>
|
||||||
|
<button onclick="testPickFile(true, 'uri')">📄 多选 uri</button>
|
||||||
|
<button class="secondary" onclick="testPickFile(false, 'dataUrl')">📄 单选 dataUrl</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-area" id="pickFilePreview">文件信息将在此显示</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 6. openExternal ══════════ -->
|
||||||
|
<div class="section-title">6. 外部链接 openExternal</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="btn-row">
|
||||||
|
<button onclick="testOpenExternal('tel:10086')">📞 tel:10086</button>
|
||||||
|
<button onclick="testOpenExternal('mailto:test@example.com')">📧 mailto</button>
|
||||||
|
<button onclick="testOpenExternal('market://details?id=com.android.chrome')">🛒 market://</button>
|
||||||
|
<button class="warn" onclick="testOpenExternal('https://www.baidu.com')">🔗 https (应返回 false)</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-area" id="externalPreview">调用结果将在此显示</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 7. requestPermissions ══════════ -->
|
||||||
|
<div class="section-title">7. 权限请求 requestPermissions</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="btn-row">
|
||||||
|
<button onclick="testPermissions(['camera', 'microphone', 'location'])">🔐 常用三权限</button>
|
||||||
|
<button onclick="testPermissions(['photos', 'videos', 'storage'])">📂 媒体/存储</button>
|
||||||
|
<button class="secondary" onclick="testPermissions(['audio', 'images'])">🏷 别名类型</button>
|
||||||
|
<button class="warn" onclick="testPermissions(['unknown_type'])">❓ 未知类型</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-area" id="permPreview">权限状态将在此显示</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 8. 页面控制 ══════════ -->
|
||||||
|
<div class="section-title">8. 页面控制</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="btn-row">
|
||||||
|
<button onclick="testReload()">🔄 reloadPage</button>
|
||||||
|
<button onclick="testGoBack()">⬅️ goBack</button>
|
||||||
|
<button class="danger" onclick="testCloseApp()">✖ closeApp</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 9. 旧相机兼容层 ══════════ -->
|
||||||
|
<div class="section-title">9. 旧相机兼容层 Legacy Camera</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">openCamera / capturePhoto 劫持测试</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button onclick="testLegacyStatus()">🔍 检查劫持状态</button>
|
||||||
|
<button onclick="testLegacyOpenCamera()">📷 legacy openCamera</button>
|
||||||
|
<button class="secondary" onclick="testLegacyHandleNativeCapture()">🎯 handleNativeCapture</button>
|
||||||
|
<button class="secondary" onclick="testLegacyAddPreview()">🖼 addPreview</button>
|
||||||
|
<button class="warn" onclick="testLegacyEventFallback()">📡 Event 兜底</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-area" id="legacyPreview">旧相机兼容测试结果将在此显示</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 10. 原生 <input type="file"> ══════════ -->
|
||||||
|
<div class="section-title">10. 原生 <input type="file"></div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="file-input-group">
|
||||||
|
<label>🖼 图片 (image/*)<input type="file" accept="image/*" onchange="handleFileInput(this, 'nativeFilePreview')"></label>
|
||||||
|
<label>📸 拍照 (capture)<input type="file" accept="image/*" capture onchange="handleFileInput(this, 'nativeFilePreview')"></label>
|
||||||
|
<label>📄 多文件 (multiple)<input type="file" multiple onchange="handleFileInput(this, 'nativeFilePreview')"></label>
|
||||||
|
<label>📁 任意文件<input type="file" onchange="handleFileInput(this, 'nativeFilePreview')"></label>
|
||||||
|
</div>
|
||||||
|
<div class="preview-area" id="nativeFilePreview">通过原生 <input> 选择文件后显示信息</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 11. UI 状态观察 ══════════ -->
|
||||||
|
<div class="section-title">11. UI 状态观察(手动操作)</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">以下由测试人员手动操作并观察壳应用行为:</div>
|
||||||
|
<ul style="padding-left:18px; font-size:13px; color:var(--muted); line-height:2;">
|
||||||
|
<li>🚀 冷启动 → 品牌启动浮层 → 进度条 → 页面显示</li>
|
||||||
|
<li>📶 断开 Wi-Fi → 点 reloadPage → 错误浮层(标题+说明+重试)</li>
|
||||||
|
<li>🔄 恢复 Wi-Fi → 点"重新加载" → 页面恢复</li>
|
||||||
|
<li>⏱ 看门狗超时 → 观察自动切换渲染模式或提示</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ 日志面板 ══════════ -->
|
||||||
|
<div class="log-panel">
|
||||||
|
<div class="log-header">
|
||||||
|
<span>📜 操作日志 (<span id="logCount">0</span>)</span>
|
||||||
|
<button onclick="clearLog()">清空</button>
|
||||||
|
</div>
|
||||||
|
<div class="log-body" id="logBody"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 日志系统
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
let logEntryCount = 0;
|
||||||
|
|
||||||
|
function log(level, tag, msg) {
|
||||||
|
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||||
|
const body = document.getElementById('logBody');
|
||||||
|
const cls = { ok: 'log-ok', err: 'log-err', warn: 'log-warn', info: 'log-info' }[level] || 'log-info';
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
entry.className = 'log-entry';
|
||||||
|
const msgStr = typeof msg === 'object' ? JSON.stringify(msg, null, 2) : String(msg);
|
||||||
|
entry.innerHTML = `<span class="log-time">${time}</span> <span class="${cls}">[${tag}]</span> ${escapeHtml(msgStr)}`;
|
||||||
|
body.appendChild(entry);
|
||||||
|
body.scrollTop = body.scrollHeight;
|
||||||
|
logEntryCount++;
|
||||||
|
document.getElementById('logCount').textContent = logEntryCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLog() {
|
||||||
|
document.getElementById('logBody').innerHTML = '';
|
||||||
|
logEntryCount = 0;
|
||||||
|
document.getElementById('logCount').textContent = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPreview(id, html) {
|
||||||
|
document.getElementById(id).innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 1. 环境检测
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
let shellReady = false;
|
||||||
|
|
||||||
|
window.addEventListener('app-shell-ready', function(e) {
|
||||||
|
shellReady = true;
|
||||||
|
const detail = e.detail || {};
|
||||||
|
document.getElementById('statusDot').classList.add('ready');
|
||||||
|
document.getElementById('statusText').textContent = 'AppShell 已就绪';
|
||||||
|
document.getElementById('statusVersion').textContent = 'v' + (detail.version || '?');
|
||||||
|
log('ok', 'Event', 'app-shell-ready 已接收, version=' + detail.version + ', isNativeShell=' + detail.isNativeShell);
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkBridge() {
|
||||||
|
const shell = window.AppShell;
|
||||||
|
if (!shell) {
|
||||||
|
setPreview('envPreview', '❌ window.AppShell 不存在');
|
||||||
|
log('err', 'Bridge', 'window.AppShell 未定义');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const methods = ['pickImage','captureImage','pickFile','openExternal','requestPermissions','reloadPage','goBack','closeApp'];
|
||||||
|
const available = methods.filter(m => typeof shell[m] === 'function');
|
||||||
|
const missing = methods.filter(m => typeof shell[m] !== 'function');
|
||||||
|
|
||||||
|
let html = `<b>isNativeShell:</b> ${shell.isNativeShell}<br>`;
|
||||||
|
html += `<b>version:</b> ${shell.version}<br>`;
|
||||||
|
html += `<b>方法 (${available.length}/${methods.length}):</b> ${available.join(', ')}<br>`;
|
||||||
|
if (missing.length > 0) {
|
||||||
|
html += `<b style="color:var(--error)">缺失:</b> ${missing.join(', ')}`;
|
||||||
|
}
|
||||||
|
html += `<br><b>app-shell-ready 事件:</b> ${shellReady ? '✅ 已接收' : '⏳ 未收到'}`;
|
||||||
|
|
||||||
|
setPreview('envPreview', html);
|
||||||
|
log(missing.length > 0 ? 'warn' : 'ok', 'Bridge', `检测完成: ${available.length}/${methods.length} 可用`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 2. 核心 WebView 行为
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
function testGeolocation() {
|
||||||
|
log('info', 'Geolocation', '请求位置…');
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
setPreview('webviewPreview', '❌ 当前环境不支持 Geolocation API');
|
||||||
|
log('err', 'Geolocation', '不支持');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
function(pos) {
|
||||||
|
const msg = `纬度=${pos.coords.latitude}, 经度=${pos.coords.longitude}`;
|
||||||
|
setPreview('webviewPreview', '✅ 位置获取成功: ' + msg);
|
||||||
|
log('ok', 'Geolocation', msg);
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
setPreview('webviewPreview', '❌ 位置获取失败: ' + err.message);
|
||||||
|
log('err', 'Geolocation', err.message);
|
||||||
|
},
|
||||||
|
{ timeout: 10000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testAlert() {
|
||||||
|
log('info', 'Alert', '正在弹出 window.alert…');
|
||||||
|
window.alert('这是一条测试弹窗消息。\n由 test_bridge.html 发出。');
|
||||||
|
log('ok', 'Alert', 'alert 已关闭');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 3. captureImage
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
async function testCaptureImage(responseType) {
|
||||||
|
if (!window.AppShell) { log('err', 'captureImage', 'AppShell 不可用'); return; }
|
||||||
|
log('info', 'captureImage', `调用 captureImage({responseType: '${responseType}'})…`);
|
||||||
|
try {
|
||||||
|
const result = await AppShell.captureImage({ responseType });
|
||||||
|
if (result == null) {
|
||||||
|
setPreview('capturePreview', '⚠️ 用户取消了拍照');
|
||||||
|
log('warn', 'captureImage', '用户取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = `<b>name:</b> ${result.name || '-'}<br>`;
|
||||||
|
html += `<b>mimeType:</b> ${result.mimeType || '-'}<br>`;
|
||||||
|
html += `<b>uri:</b> ${result.uri || '-'}<br>`;
|
||||||
|
html += `<b>size:</b> ${result.size || '-'}<br>`;
|
||||||
|
html += `<b>base64:</b> ${result.base64 ? '✅ (' + result.base64.length + ' chars)' : '—'}<br>`;
|
||||||
|
html += `<b>dataUrl:</b> ${result.dataUrl ? '✅' : '—'}<br>`;
|
||||||
|
if (result.dataUrl) {
|
||||||
|
html += `<img src="${result.dataUrl}" alt="拍照预览">`;
|
||||||
|
}
|
||||||
|
setPreview('capturePreview', html);
|
||||||
|
log('ok', 'captureImage', { name: result.name, mimeType: result.mimeType, responseType, hasDataUrl: !!result.dataUrl, hasBase64: !!result.base64 });
|
||||||
|
} catch (e) {
|
||||||
|
setPreview('capturePreview', '❌ 错误: ' + e.message);
|
||||||
|
log('err', 'captureImage', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 4. pickImage
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
async function testPickImage(multiple) {
|
||||||
|
if (!window.AppShell) { log('err', 'pickImage', 'AppShell 不可用'); return; }
|
||||||
|
const label = multiple ? '多选' : '单选';
|
||||||
|
log('info', 'pickImage', `调用 pickImage({multiple: ${multiple}})…`);
|
||||||
|
try {
|
||||||
|
const result = await AppShell.pickImage({ multiple, responseType: 'dataUrl' });
|
||||||
|
if (result == null || (Array.isArray(result) && result.length === 0)) {
|
||||||
|
setPreview('pickImagePreview', '⚠️ 用户取消(' + label + ')');
|
||||||
|
log('warn', 'pickImage', '用户取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = Array.isArray(result) ? result : [result];
|
||||||
|
let html = `<b>${label} - 共 ${items.length} 张:</b><br>`;
|
||||||
|
items.forEach((img, i) => {
|
||||||
|
html += `${i + 1}. ${img.name || '-'} (${img.mimeType || '-'})<br>`;
|
||||||
|
if (img.dataUrl) html += `<img src="${img.dataUrl}" alt="选图${i + 1}">`;
|
||||||
|
});
|
||||||
|
setPreview('pickImagePreview', html);
|
||||||
|
log('ok', 'pickImage', { mode: label, count: items.length });
|
||||||
|
} catch (e) {
|
||||||
|
setPreview('pickImagePreview', '❌ 错误: ' + e.message);
|
||||||
|
log('err', 'pickImage', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 5. pickFile
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
async function testPickFile(multiple, responseType) {
|
||||||
|
if (!window.AppShell) { log('err', 'pickFile', 'AppShell 不可用'); return; }
|
||||||
|
log('info', 'pickFile', `调用 pickFile({multiple: ${multiple}, responseType: '${responseType}'})…`);
|
||||||
|
try {
|
||||||
|
const result = await AppShell.pickFile({ multiple, responseType });
|
||||||
|
if (result == null || (Array.isArray(result) && result.length === 0)) {
|
||||||
|
setPreview('pickFilePreview', '⚠️ 用户取消');
|
||||||
|
log('warn', 'pickFile', '用户取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = Array.isArray(result) ? result : [result];
|
||||||
|
let html = `<b>共 ${items.length} 个文件:</b><br>`;
|
||||||
|
items.forEach((f, i) => {
|
||||||
|
html += `${i + 1}. <b>${f.name || '-'}</b> | ${f.mimeType || '-'} | ${f.size || '-'} bytes | uri: ${f.uri || '-'}`;
|
||||||
|
if (f.dataUrl) html += ' | dataUrl: ✅';
|
||||||
|
if (f.base64) html += ' | base64: ✅ (' + f.base64.length + ' chars)';
|
||||||
|
html += '<br>';
|
||||||
|
});
|
||||||
|
setPreview('pickFilePreview', html);
|
||||||
|
log('ok', 'pickFile', { count: items.length, responseType });
|
||||||
|
} catch (e) {
|
||||||
|
setPreview('pickFilePreview', '❌ 错误: ' + e.message);
|
||||||
|
log('err', 'pickFile', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 6. openExternal
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
async function testOpenExternal(url) {
|
||||||
|
if (!window.AppShell) { log('err', 'openExternal', 'AppShell 不可用'); return; }
|
||||||
|
log('info', 'openExternal', `调用 openExternal('${url}')…`);
|
||||||
|
try {
|
||||||
|
const result = await AppShell.openExternal(url);
|
||||||
|
const msg = `openExternal('${url}') → ${result}`;
|
||||||
|
setPreview('externalPreview', result ? '✅ 成功打开' : '⛔ 返回 false(未打开)');
|
||||||
|
log(result ? 'ok' : 'warn', 'openExternal', msg);
|
||||||
|
} catch (e) {
|
||||||
|
setPreview('externalPreview', '❌ 错误: ' + e.message);
|
||||||
|
log('err', 'openExternal', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 7. requestPermissions
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
async function testPermissions(types) {
|
||||||
|
if (!window.AppShell) { log('err', 'Permissions', 'AppShell 不可用'); return; }
|
||||||
|
log('info', 'Permissions', `调用 requestPermissions(${JSON.stringify(types)})…`);
|
||||||
|
try {
|
||||||
|
const result = await AppShell.requestPermissions(types);
|
||||||
|
let html = '<b>权限状态:</b><br>';
|
||||||
|
const keys = Object.keys(result || {});
|
||||||
|
if (keys.length === 0) {
|
||||||
|
html += '⚠️ 返回空对象 {}(可能是未知权限类型)';
|
||||||
|
} else {
|
||||||
|
keys.forEach(k => {
|
||||||
|
const status = result[k];
|
||||||
|
const icon = status === 'granted' ? '✅' : (status === 'denied' ? '❌' : '⚠️');
|
||||||
|
html += `${icon} <b>${k}:</b> ${status}<br>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setPreview('permPreview', html);
|
||||||
|
log('ok', 'Permissions', result);
|
||||||
|
} catch (e) {
|
||||||
|
setPreview('permPreview', '❌ 错误: ' + e.message);
|
||||||
|
log('err', 'Permissions', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 8. 页面控制
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
async function testReload() {
|
||||||
|
if (!window.AppShell) { log('err', 'Control', 'AppShell 不可用'); return; }
|
||||||
|
log('info', 'Control', '调用 reloadPage()…');
|
||||||
|
try {
|
||||||
|
await AppShell.reloadPage();
|
||||||
|
log('ok', 'Control', 'reloadPage 完成');
|
||||||
|
} catch (e) { log('err', 'Control', e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testGoBack() {
|
||||||
|
if (!window.AppShell) { log('err', 'Control', 'AppShell 不可用'); return; }
|
||||||
|
log('info', 'Control', '调用 goBack()…');
|
||||||
|
try {
|
||||||
|
const result = await AppShell.goBack();
|
||||||
|
log(result ? 'ok' : 'warn', 'Control', 'goBack → ' + result);
|
||||||
|
} catch (e) { log('err', 'Control', e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testCloseApp() {
|
||||||
|
if (!window.AppShell) { log('err', 'Control', 'AppShell 不可用'); return; }
|
||||||
|
log('warn', 'Control', '即将关闭应用…');
|
||||||
|
if (confirm('确认要关闭应用吗?')) {
|
||||||
|
try { await AppShell.closeApp(); } catch (e) { log('err', 'Control', e.message); }
|
||||||
|
} else {
|
||||||
|
log('info', 'Control', '用户取消 closeApp');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 9. 旧相机兼容层
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
|
||||||
|
// 预定义 legacy 全局函数,让 Bridge 的 compat 层能找到它们并替换
|
||||||
|
window.openCamera = function() { log('warn', 'Legacy', '原始 openCamera 被调用(未被劫持)'); };
|
||||||
|
window.capturePhoto = function() { log('warn', 'Legacy', '原始 capturePhoto 被调用(未被劫持)'); };
|
||||||
|
|
||||||
|
function testLegacyStatus() {
|
||||||
|
const openCameraWrapped = !!(window.openCamera && window.openCamera.__appShellCompatWrapped);
|
||||||
|
const capturePhotoWrapped = !!(window.capturePhoto && window.capturePhoto.__appShellCompatWrapped);
|
||||||
|
const compatInstalled = !!window.__appShellLegacyCameraCompatInstalled;
|
||||||
|
|
||||||
|
let html = `<b>compat 安装:</b> ${compatInstalled ? '✅' : '❌'}<br>`;
|
||||||
|
html += `<b>openCamera 劫持:</b> ${openCameraWrapped ? '✅ __appShellCompatWrapped=true' : '❌ 未劫持'}<br>`;
|
||||||
|
html += `<b>capturePhoto 劫持:</b> ${capturePhotoWrapped ? '✅ __appShellCompatWrapped=true' : '❌ 未劫持'}<br>`;
|
||||||
|
|
||||||
|
setPreview('legacyPreview', html);
|
||||||
|
log(openCameraWrapped ? 'ok' : 'err', 'Legacy', { openCameraWrapped, capturePhotoWrapped, compatInstalled });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testLegacyOpenCamera() {
|
||||||
|
log('info', 'Legacy', '调用 window.openCamera()…');
|
||||||
|
try {
|
||||||
|
const result = await window.openCamera();
|
||||||
|
let html = '✅ openCamera 返回结果:<br>';
|
||||||
|
if (result && result.dataUrl) {
|
||||||
|
html += `<img src="${result.dataUrl}" alt="legacy拍照"><br>`;
|
||||||
|
}
|
||||||
|
html += escapeHtml(JSON.stringify(result, null, 2));
|
||||||
|
setPreview('legacyPreview', html);
|
||||||
|
log('ok', 'Legacy', 'openCamera 完成');
|
||||||
|
} catch (e) {
|
||||||
|
setPreview('legacyPreview', '❌ 错误: ' + e.message);
|
||||||
|
log('err', 'Legacy', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLegacyHandleNativeCapture() {
|
||||||
|
// 注册全局回调,然后调用 openCamera
|
||||||
|
window.handleNativeCapture = function(detail) {
|
||||||
|
let html = '✅ handleNativeCapture 被调用:<br>';
|
||||||
|
html += `<b>args:</b> ${JSON.stringify(detail.args)}<br>`;
|
||||||
|
if (detail.result && detail.result.dataUrl) {
|
||||||
|
html += `<img src="${detail.result.dataUrl}" alt="handleNativeCapture"><br>`;
|
||||||
|
}
|
||||||
|
html += escapeHtml(JSON.stringify(detail.result, null, 2));
|
||||||
|
setPreview('legacyPreview', html);
|
||||||
|
log('ok', 'Legacy', 'handleNativeCapture 回调成功');
|
||||||
|
};
|
||||||
|
log('info', 'Legacy', '已注册 handleNativeCapture,正在调用 openCamera…');
|
||||||
|
window.openCamera('test-arg-1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLegacyAddPreview() {
|
||||||
|
// 设置全局 currentUploadId 和 addPreview 回调
|
||||||
|
window.currentUploadId = 'test-upload-id';
|
||||||
|
window.addPreview = function(targetId, dataUrl) {
|
||||||
|
let html = `✅ addPreview 被调用:<br>`;
|
||||||
|
html += `<b>targetId:</b> ${targetId}<br>`;
|
||||||
|
if (dataUrl) {
|
||||||
|
html += `<img src="${dataUrl}" alt="addPreview"><br>`;
|
||||||
|
}
|
||||||
|
setPreview('legacyPreview', html);
|
||||||
|
log('ok', 'Legacy', 'addPreview 回调成功, targetId=' + targetId);
|
||||||
|
};
|
||||||
|
// 清理 handleNativeCapture 以让 addPreview 生效
|
||||||
|
delete window.handleNativeCapture;
|
||||||
|
log('info', 'Legacy', '已注册 addPreview + currentUploadId, 正在调用 openCamera…');
|
||||||
|
window.openCamera('test-upload-id');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLegacyEventFallback() {
|
||||||
|
// 清理所有全局回调,只靠事件兜底
|
||||||
|
delete window.handleNativeCapture;
|
||||||
|
delete window.addPreview;
|
||||||
|
delete window.currentUploadId;
|
||||||
|
|
||||||
|
const handler = function(e) {
|
||||||
|
let html = '✅ app-shell-image-captured 事件触发:<br>';
|
||||||
|
const detail = e.detail || {};
|
||||||
|
html += `<b>args:</b> ${JSON.stringify(detail.args)}<br>`;
|
||||||
|
if (detail.result && detail.result.dataUrl) {
|
||||||
|
html += `<img src="${detail.result.dataUrl}" alt="event-fallback"><br>`;
|
||||||
|
}
|
||||||
|
html += escapeHtml(JSON.stringify(detail.result, null, 2));
|
||||||
|
setPreview('legacyPreview', html);
|
||||||
|
log('ok', 'Legacy', 'Event 兜底成功');
|
||||||
|
window.removeEventListener('app-shell-image-captured', handler);
|
||||||
|
};
|
||||||
|
window.addEventListener('app-shell-image-captured', handler);
|
||||||
|
|
||||||
|
log('info', 'Legacy', '已监听 app-shell-image-captured, 正在调用 openCamera…');
|
||||||
|
window.openCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 10. 原生 <input type="file">
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
function handleFileInput(input, previewId) {
|
||||||
|
const files = input.files;
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
setPreview(previewId, '⚠️ 未选择文件');
|
||||||
|
log('warn', 'FileInput', '用户取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = `<b>共 ${files.length} 个文件:</b><br>`;
|
||||||
|
Array.from(files).forEach((f, i) => {
|
||||||
|
html += `${i + 1}. <b>${f.name}</b> | ${f.type || 'unknown'} | ${f.size} bytes<br>`;
|
||||||
|
// 图片预览
|
||||||
|
if (f.type && f.type.startsWith('image/')) {
|
||||||
|
const url = URL.createObjectURL(f);
|
||||||
|
html += `<img src="${url}" alt="${f.name}">`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setPreview(previewId, html);
|
||||||
|
log('ok', 'FileInput', { count: files.length, names: Array.from(files).map(f => f.name) });
|
||||||
|
// 重置 input 以允许重复选择同一文件
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
// 初始化
|
||||||
|
// ═══════════════════════════════════════
|
||||||
|
log('info', 'System', '测试页已加载,等待 AppShell 桥接注入…');
|
||||||
|
|
||||||
|
// 延迟检测 Bridge 状态
|
||||||
|
setTimeout(function() {
|
||||||
|
if (window.AppShell) {
|
||||||
|
if (!shellReady) {
|
||||||
|
document.getElementById('statusDot').classList.add('ready');
|
||||||
|
document.getElementById('statusText').textContent = 'AppShell 已检测到(事件可能在页面加载前触发)';
|
||||||
|
document.getElementById('statusVersion').textContent = 'v' + (window.AppShell.version || '?');
|
||||||
|
}
|
||||||
|
log('ok', 'System', 'AppShell Bridge 已就绪');
|
||||||
|
} else {
|
||||||
|
log('warn', 'System', '1秒后仍未检测到 AppShell,请确认运行环境');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue