feat: init 改为 测试web

This commit is contained in:
Max 2026-03-20 09:56:02 +08:00
parent 498fa823a0
commit fb84e6f4fe
6 changed files with 897 additions and 45 deletions

3
.fvmrc Normal file
View File

@ -0,0 +1,3 @@
{
"flutter": "3.41.2"
}

3
.gitignore vendored
View File

@ -43,3 +43,6 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# FVM Version Cache
.fvm/

73
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.41.2"
}

View File

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

View File

@ -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">原生 &lt;a&gt; 标签跳转(非 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. 原生 &lt;input type="file"&gt;</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">通过原生 &lt;input&gt; 选择文件后显示信息</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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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>