fix(android): 修复PDF/下载/文件选择器,常规设置增加上传日志

This commit is contained in:
JOJO 2026-06-24 01:50:03 +08:00
parent 426825d475
commit 15dea9c2e5
4 changed files with 153 additions and 11 deletions

View File

@ -10,12 +10,12 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.DocumentsContract
import android.util.Log
import android.view.ViewGroup
import android.view.View
import android.content.Context
import android.webkit.CookieManager
import android.webkit.DownloadListener
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest
import android.webkit.ValueCallback
@ -185,10 +185,6 @@ class MainActivity : AppCompatActivity() {
}
}
webView.setDownloadListener(DownloadListener { url, _, contentDisposition, _, _ ->
startSystemDownload(url, suggestFileName(contentDisposition, url))
})
webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
// 按需放行媒体权限(例如文件上传触发的媒体访问)
@ -203,13 +199,12 @@ class MainActivity : AppCompatActivity() {
this@MainActivity.filePathCallback?.onReceiveValue(null)
this@MainActivity.filePathCallback = filePathCallback
// 统一使用 ACTION_GET_CONTENT避免不同 ROM 对 Photo Picker / 相册的兼容差异
// 导致返回的 URI/文件对象在上传时失效。根据 acceptTypes 设置过滤类型,
// 图片选择仍只显示图片文件,但走文件管理器而不是系统相册。
// 使用 ACTION_OPEN_DOCUMENT 走系统文件管理器DocumentsUI
// 避免 ACTION_GET_CONTENT 在某些 ROM 上唤起相册/Photo Picker 导致返回 URI 失效。
val acceptTypes = fileChooserParams?.acceptTypes?.filter { it.isNotBlank() }?.takeIf { it.isNotEmpty() }
?: arrayListOf("*/*")
val allowMultiple = fileChooserParams?.mode == FileChooserParams.MODE_OPEN_MULTIPLE
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = acceptTypes.first()
if (allowMultiple) {

7
package-lock.json generated
View File

@ -770,6 +770,7 @@
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@floating-ui/utils": "^0.2.11"
}
@ -779,6 +780,7 @@
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
@ -788,7 +790,8 @@
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
@ -6964,7 +6967,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View File

@ -75,6 +75,14 @@
:title="displayName"
></iframe>
</template>
</div>
<div v-else-if="canAndroidPdfPreview" class="sfc-android-pdf-preview" @click="previewPdf">
<div class="sfc-android-pdf-placeholder">
<span class="sfc-file-icon"></span>
<span>PDF 文件点击预览</span>
</div>
</div>
</div>
</template>
@ -94,6 +102,9 @@ const props = defineProps<{
preview?: string;
}>();
// ---- ----
const isAndroidApp = typeof window !== 'undefined' && Boolean((window as any).AndroidPdfBridge);
// ---- ----
const loading = ref(false);
const error = ref('');
@ -108,9 +119,15 @@ const displayName = computed(() => props.name || props.path.split('/').pop() ||
const canPreview = computed(() => {
if (props.preview === 'off') return false;
// Android App PDF iframe WebView PdfPreviewActivity
if (isAndroidApp && fileType.value === 'pdf') return false;
return ['text', 'code', 'json', 'csv', 'markdown', 'image', 'pdf'].includes(fileType.value);
});
const canAndroidPdfPreview = computed(() => {
return isAndroidApp && fileType.value === 'pdf';
});
const canCopy = computed(() => ['text', 'code', 'json', 'csv', 'markdown'].includes(fileType.value));
const metaText = computed(() => {
@ -275,6 +292,13 @@ function openFullImage() {
if (contentUrl.value) window.open(contentUrl.value, '_blank');
}
function previewPdf() {
const bridge = (window as any).AndroidPdfBridge;
if (bridge && typeof bridge.previewPdf === 'function') {
bridge.previewPdf(contentUrl.value);
}
}
function onImageError() {
error.value = '图片加载失败';
}
@ -482,4 +506,31 @@ onMounted(() => {
border: none;
display: block;
}
.sfc-android-pdf-preview {
border-top: 1px solid var(--border-default);
background: var(--surface-base);
}
.sfc-android-pdf-placeholder {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 24px;
color: var(--text-secondary);
background: var(--surface-raised);
cursor: pointer;
transition: background 0.15s;
}
.sfc-android-pdf-placeholder:hover {
background: var(--surface-muted);
}
.sfc-android-pdf-placeholder .sfc-file-icon {
width: 20px;
height: 20px;
background-color: var(--text-secondary);
}
</style>

View File

@ -450,6 +450,19 @@
</div>
</div>
</div>
<div class="settings-action-row">
<span class="settings-row-copy">
<span class="settings-row-title">上传诊断日志</span>
<span class="settings-row-desc">复现问题后点击把当前环境信息上传给开发者排查</span>
</span>
<button
type="button"
class="settings-secondary-button"
@click="uploadDiagnosticLog"
>
上传日志
</button>
</div>
</section>
<section
@ -2259,6 +2272,86 @@ const downloadLatestApp = () => {
window.location.href = url;
};
const uploadDiagnosticLog = async () => {
try {
const w = window as any;
const params = new URLSearchParams(window.location.search);
const screenInfo =
typeof window !== 'undefined'
? {
width: window.innerWidth,
height: window.innerHeight,
dpr: window.devicePixelRatio
}
: null;
const bridgeProbe = (name: string) => {
const b = w?.[name];
if (!b) return { exists: false };
const methods: Record<string, any> = {};
[
'getAppVersionCode',
'getAppVersionName',
'onThemeChanged',
'previewPdf',
'isPdfPreviewSupported',
'downloadFile',
'isSupported',
'isModelReady',
'isModelPartial',
'startRecording',
'stopRecording'
].forEach((m) => {
try {
methods[m] = typeof b[m] === 'function';
} catch {
methods[m] = 'error';
}
});
let versionCode = '';
let versionName = '';
try {
versionCode =
typeof b.getAppVersionCode === 'function' ? String(b.getAppVersionCode() || '') : '';
versionName =
typeof b.getAppVersionName === 'function' ? String(b.getAppVersionName() || '') : '';
} catch {}
return { exists: true, methods, versionCode, versionName };
};
const payload = {
source: 'diagnostic_log',
url: window.location.href,
userAgent: navigator.userAgent,
screen: screenInfo,
urlParams: {
app_vc: params.get('app_vc') || '',
app_vn: params.get('app_vn') || '',
app_shell: params.get('app_shell') || ''
},
bridges: {
AndroidThemeBridge: bridgeProbe('AndroidThemeBridge'),
AndroidPdfBridge: bridgeProbe('AndroidPdfBridge'),
AndroidDownloadBridge: bridgeProbe('AndroidDownloadBridge'),
AndroidVoiceBridge: bridgeProbe('AndroidVoiceBridge')
},
appCurrentVersionCode: appCurrentVersionCode.value,
appCurrentVersionName: appCurrentVersionName.value,
appUpdateInfo: appUpdateInfo.value,
appUpdateError: appUpdateError.value,
appUpdateCheckedAt: appUpdateCheckedAt.value,
timestamp: new Date().toISOString()
};
const resp = await fetch('/api/client_debug_log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!resp.ok) throw new Error(`status ${resp.status}`);
alert('诊断日志已上传');
} catch (e: any) {
alert('上传诊断日志失败:' + (e?.message || String(e)));
}
};
onMounted(async () => {
hydrateAppVersionFromBridge();
try {