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.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.provider.DocumentsContract
import android.util.Log import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.view.View import android.view.View
import android.content.Context import android.content.Context
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.DownloadListener
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest import android.webkit.PermissionRequest
import android.webkit.ValueCallback 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() { webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) { override fun onPermissionRequest(request: PermissionRequest) {
// 按需放行媒体权限(例如文件上传触发的媒体访问) // 按需放行媒体权限(例如文件上传触发的媒体访问)
@ -203,13 +199,12 @@ class MainActivity : AppCompatActivity() {
this@MainActivity.filePathCallback?.onReceiveValue(null) this@MainActivity.filePathCallback?.onReceiveValue(null)
this@MainActivity.filePathCallback = filePathCallback this@MainActivity.filePathCallback = filePathCallback
// 统一使用 ACTION_GET_CONTENT避免不同 ROM 对 Photo Picker / 相册的兼容差异 // 使用 ACTION_OPEN_DOCUMENT 走系统文件管理器DocumentsUI
// 导致返回的 URI/文件对象在上传时失效。根据 acceptTypes 设置过滤类型, // 避免 ACTION_GET_CONTENT 在某些 ROM 上唤起相册/Photo Picker 导致返回 URI 失效。
// 图片选择仍只显示图片文件,但走文件管理器而不是系统相册。
val acceptTypes = fileChooserParams?.acceptTypes?.filter { it.isNotBlank() }?.takeIf { it.isNotEmpty() } val acceptTypes = fileChooserParams?.acceptTypes?.filter { it.isNotBlank() }?.takeIf { it.isNotEmpty() }
?: arrayListOf("*/*") ?: arrayListOf("*/*")
val allowMultiple = fileChooserParams?.mode == FileChooserParams.MODE_OPEN_MULTIPLE 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) addCategory(Intent.CATEGORY_OPENABLE)
type = acceptTypes.first() type = acceptTypes.first()
if (allowMultiple) { 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", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"@floating-ui/utils": "^0.2.11" "@floating-ui/utils": "^0.2.11"
} }
@ -779,6 +780,7 @@
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"@floating-ui/core": "^1.7.5", "@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11" "@floating-ui/utils": "^0.2.11"
@ -788,7 +790,8 @@
"version": "0.2.11", "version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT" "license": "MIT",
"optional": true
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.13.0", "version": "0.13.0",
@ -6964,7 +6967,7 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",

View File

@ -75,6 +75,14 @@
:title="displayName" :title="displayName"
></iframe> ></iframe>
</template> </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>
</div> </div>
</template> </template>
@ -94,6 +102,9 @@ const props = defineProps<{
preview?: string; preview?: string;
}>(); }>();
// ---- ----
const isAndroidApp = typeof window !== 'undefined' && Boolean((window as any).AndroidPdfBridge);
// ---- ---- // ---- ----
const loading = ref(false); const loading = ref(false);
const error = ref(''); const error = ref('');
@ -108,9 +119,15 @@ const displayName = computed(() => props.name || props.path.split('/').pop() ||
const canPreview = computed(() => { const canPreview = computed(() => {
if (props.preview === 'off') return false; 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); 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 canCopy = computed(() => ['text', 'code', 'json', 'csv', 'markdown'].includes(fileType.value));
const metaText = computed(() => { const metaText = computed(() => {
@ -275,6 +292,13 @@ function openFullImage() {
if (contentUrl.value) window.open(contentUrl.value, '_blank'); 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() { function onImageError() {
error.value = '图片加载失败'; error.value = '图片加载失败';
} }
@ -482,4 +506,31 @@ onMounted(() => {
border: none; border: none;
display: block; 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> </style>

View File

@ -450,6 +450,19 @@
</div> </div>
</div> </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>
<section <section
@ -2259,6 +2272,86 @@ const downloadLatestApp = () => {
window.location.href = url; 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 () => { onMounted(async () => {
hydrateAppVersionFromBridge(); hydrateAppVersionFromBridge();
try { try {