liuq 3 weeks ago
parent
commit
12d34c9f12
2 changed files with 341 additions and 107 deletions
  1. 299 97
      templates/attachment_test/index.html
  2. 42 10
      templates/js/im-sdk.js

+ 299 - 97
templates/attachment_test/index.html

@@ -76,6 +76,63 @@
         max-height: 200px;
         overflow-y: auto;
     }
+    .attachment-fault-modal {
+        display: none;
+        position: fixed;
+        inset: 0;
+        z-index: 5000;
+        align-items: center;
+        justify-content: center;
+        padding: 16px;
+        box-sizing: border-box;
+    }
+    .attachment-fault-modal.is-open {
+        display: flex;
+    }
+    .attachment-fault-backdrop {
+        position: absolute;
+        inset: 0;
+        background: rgba(0,0,0,0.45);
+    }
+    .attachment-fault-dialog {
+        position: relative;
+        background: #fff;
+        border-radius: 12px;
+        max-width: 520px;
+        width: 100%;
+        max-height: 85vh;
+        display: flex;
+        flex-direction: column;
+        box-shadow: 0 8px 32px rgba(0,0,0,0.2);
+    }
+    .attachment-fault-dialog h3 {
+        margin: 0;
+        padding: 16px 18px 8px;
+        font-size: 1.1em;
+        color: #c62828;
+    }
+    .attachment-fault-body {
+        margin: 0 18px 12px;
+        padding: 12px;
+        background: #fff5f5;
+        border: 1px solid #ffcdd2;
+        border-radius: 8px;
+        font-size: 0.8em;
+        line-height: 1.45;
+        color: #333;
+        white-space: pre-wrap;
+        word-break: break-word;
+        font-family: ui-monospace, Consolas, monospace;
+        overflow-y: auto;
+        max-height: min(50vh, 320px);
+    }
+    .attachment-fault-actions {
+        padding: 0 18px 16px;
+        display: flex;
+        gap: 10px;
+        flex-wrap: wrap;
+        justify-content: flex-end;
+    }
 </style>
 {% endblock %}
 
@@ -116,6 +173,18 @@
             <pre id="userAgentText" class="attachment-ua-text"></pre>
         </div>
     </div>
+
+    <div id="faultModal" class="attachment-fault-modal" role="dialog" aria-modal="true" aria-labelledby="faultModalTitle" aria-hidden="true">
+        <div class="attachment-fault-backdrop" id="faultModalBackdrop"></div>
+        <div class="attachment-fault-dialog">
+            <h3 id="faultModalTitle">故障信息</h3>
+            <pre id="faultModalBody" class="attachment-fault-body"></pre>
+            <div class="attachment-fault-actions">
+                <button type="button" class="btn btn-info" id="faultModalCopy">复制详情</button>
+                <button type="button" class="btn btn-primary" id="faultModalClose">确定</button>
+            </div>
+        </div>
+    </div>
 {% endblock %}
 
 {% block scripts %}
@@ -134,6 +203,68 @@
         userAgentText.textContent = navigator.userAgent || '(不可用)';
     }
 
+    var faultModal = document.getElementById('faultModal');
+    var faultModalBody = document.getElementById('faultModalBody');
+    var faultModalTitle = document.getElementById('faultModalTitle');
+    var faultModalBackdrop = document.getElementById('faultModalBackdrop');
+    var faultModalClose = document.getElementById('faultModalClose');
+    var faultModalCopy = document.getElementById('faultModalCopy');
+    var lastFaultText = '';
+
+    function formatErrorDetail(err) {
+        if (err == null) return '(无详情)';
+        if (err instanceof Error) {
+            return err.name + ': ' + err.message + (err.stack ? '\n\n' + err.stack : '');
+        }
+        if (typeof err === 'string') return err;
+        try {
+            return JSON.stringify(err, null, 2);
+        } catch (e) {
+            return String(err);
+        }
+    }
+
+    function showFaultModal(title, detail) {
+        var text = formatErrorDetail(detail);
+        lastFaultText = (title ? title + '\n\n' : '') + text;
+        if (faultModalTitle) faultModalTitle.textContent = title || '故障信息';
+        if (faultModalBody) faultModalBody.textContent = lastFaultText;
+        if (faultModal) {
+            faultModal.classList.add('is-open');
+            faultModal.setAttribute('aria-hidden', 'false');
+        }
+        console.error('[attachment-test]', title || 'fault', detail);
+    }
+
+    function hideFaultModal() {
+        if (faultModal) {
+            faultModal.classList.remove('is-open');
+            faultModal.setAttribute('aria-hidden', 'true');
+        }
+    }
+
+    if (faultModalClose) faultModalClose.addEventListener('click', hideFaultModal);
+    if (faultModalBackdrop) faultModalBackdrop.addEventListener('click', hideFaultModal);
+    if (faultModalCopy) {
+        faultModalCopy.addEventListener('click', function () {
+            if (!lastFaultText) return;
+            if (navigator.clipboard && navigator.clipboard.writeText) {
+                navigator.clipboard.writeText(lastFaultText).then(function () {
+                    showMessage('故障详情已复制到剪贴板');
+                }).catch(function (e) {
+                    showFaultModal('复制失败', e);
+                });
+            } else {
+                showFaultModal('无法复制', '当前环境不支持 clipboard API');
+            }
+        });
+    }
+    document.addEventListener('keydown', function (e) {
+        if (e.key === 'Escape' && faultModal && faultModal.classList.contains('is-open')) {
+            hideFaultModal();
+        }
+    });
+
     function useUniNative() {
         return window.imSDK && typeof imSDK.isUniAppIm === 'function' && imSDK.isUniAppIm();
     }
@@ -178,37 +309,65 @@
         return '';
     }
 
-    function uploadNativePath(filePath) {
-        if (!filePath) {
-            showMessage('未获取到本地文件路径', 'error');
-            return;
+    /** 选择图片/视频成功回调:处理 im-sdk 超时或 postMessage 失败返回的 errMsg */
+    function handleNativePickResult(res) {
+        try {
+            if (res && res.errMsg) {
+                showFaultModal('原生调用失败', res.errMsg);
+                return;
+            }
+            uploadNativePath(nativeTempPath(res));
+        } catch (e) {
+            showFaultModal('处理原生选择结果时异常', e);
         }
-        var uploadUrl = window.location.origin + '/api/attachment_test/upload';
-        var timeoutId = setTimeout(function () {
-            showLoading(false);
-            showMessage('上传超时,请检查 App 是否实现 uploadFile 与 Cookie', 'error');
-        }, 120000);
-        showLoading(true);
-        imSDK.uploadFile({
-            url: uploadUrl,
-            filePath: filePath,
-            name: 'file',
-            success: function (res) {
-                clearTimeout(timeoutId);
+    }
+
+    function uploadNativePath(filePath) {
+        try {
+            if (!filePath) {
+                showFaultModal('未获取到文件路径', '壳子未返回 tempFilePath / tempFilePaths,或用户取消选择');
+                return;
+            }
+            var uploadUrl = window.location.origin + '/api/attachment_test/upload';
+            var timeoutId = setTimeout(function () {
                 showLoading(false);
-                var data = parseNativeUploadResponse(res);
-                if (data && data.success) {
-                    showMessage(data.message || '上传成功');
-                    renderPreview({
-                        has_file: true,
-                        filename: data.filename,
-                        url: data.url
-                    });
-                } else {
-                    showMessage((data && data.message) || '上传失败', 'error');
+                showFaultModal('上传超时', '120 秒内未完成。请检查 App 是否实现 uploadFile、接口是否可达、Cookie 是否携带');
+            }, 120000);
+            showLoading(true);
+            imSDK.uploadFile({
+                url: uploadUrl,
+                filePath: filePath,
+                name: 'file',
+                success: function (res) {
+                    try {
+                        clearTimeout(timeoutId);
+                        showLoading(false);
+                        if (res && res.errMsg) {
+                            showFaultModal('上传通道失败', res.errMsg);
+                            return;
+                        }
+                        var data = parseNativeUploadResponse(res);
+                        if (data && data.success) {
+                            showMessage(data.message || '上传成功');
+                            renderPreview({
+                                has_file: true,
+                                filename: data.filename,
+                                url: data.url
+                            });
+                        } else {
+                            showFaultModal('服务器返回失败', (data && data.message) ? data.message : JSON.stringify(data));
+                        }
+                    } catch (e) {
+                        showLoading(false);
+                        clearTimeout(timeoutId);
+                        showFaultModal('解析上传结果异常', e);
+                    }
                 }
-            }
-        });
+            });
+        } catch (e) {
+            showLoading(false);
+            showFaultModal('发起原生上传异常', e);
+        }
     }
 
     function renderPreview(data) {
@@ -259,7 +418,7 @@
                 renderPreview(data);
             }
         } catch (e) {
-            showMessage('加载状态失败: ' + e.message, 'error');
+            showFaultModal('加载附件状态失败', e);
         }
     }
 
@@ -279,10 +438,10 @@
                     url: data.url,
                 });
             } else {
-                showMessage(data.message || '上传失败', 'error');
+                showFaultModal('上传被拒绝', data.message || JSON.stringify(data));
             }
         } catch (e) {
-            showMessage('上传失败: ' + e.message, 'error');
+            showFaultModal('浏览器上传失败', e);
         } finally {
             showLoading(false);
             clearInputs();
@@ -294,98 +453,136 @@
         if (!el) return;
         el.addEventListener('change', function () {
             const f = this.files && this.files[0];
-            if (f) uploadFile(f);
+            if (!f) return;
+            uploadFile(f);
         });
     }
     ['inpCameraPhoto', 'inpCameraVideo', 'inpGalleryImage', 'inpGalleryVideo', 'inpAnyFile'].forEach(wireInput);
 
     function triggerFileInput(id) {
-        var el = document.getElementById(id);
-        if (el) el.click();
+        try {
+            var el = document.getElementById(id);
+            if (el) el.click();
+        } catch (e) {
+            showFaultModal('无法打开文件选择', e);
+        }
     }
 
     document.getElementById('btnCameraPhoto').addEventListener('click', function () {
-        if (useUniNative()) {
-            imSDK.chooseImage({
-                count: 1,
-                sourceType: ['camera'],
-                success: function (res) { uploadNativePath(nativeTempPath(res)); }
-            });
-        } else {
-            triggerFileInput('inpCameraPhoto');
+        try {
+            if (useUniNative()) {
+                imSDK.chooseImage({
+                    count: 1,
+                    sourceType: ['camera'],
+                    success: handleNativePickResult
+                });
+            } else {
+                triggerFileInput('inpCameraPhoto');
+            }
+        } catch (e) {
+            showFaultModal('拍照', e);
         }
     });
     document.getElementById('btnCameraVideo').addEventListener('click', function () {
-        if (useUniNative()) {
-            imSDK.chooseVideo({
-                sourceType: ['camera'],
-                maxDuration: 120,
-                camera: 'back',
-                success: function (res) { uploadNativePath(nativeTempPath(res)); }
-            });
-        } else {
-            triggerFileInput('inpCameraVideo');
+        try {
+            if (useUniNative()) {
+                imSDK.chooseVideo({
+                    sourceType: ['camera'],
+                    maxDuration: 120,
+                    camera: 'back',
+                    success: handleNativePickResult
+                });
+            } else {
+                triggerFileInput('inpCameraVideo');
+            }
+        } catch (e) {
+            showFaultModal('拍视频', e);
         }
     });
     document.getElementById('btnGalleryImage').addEventListener('click', function () {
-        if (useUniNative()) {
-            imSDK.chooseImage({
-                count: 1,
-                sourceType: ['album'],
-                success: function (res) { uploadNativePath(nativeTempPath(res)); }
-            });
-        } else {
-            triggerFileInput('inpGalleryImage');
+        try {
+            if (useUniNative()) {
+                imSDK.chooseImage({
+                    count: 1,
+                    sourceType: ['album'],
+                    success: handleNativePickResult
+                });
+            } else {
+                triggerFileInput('inpGalleryImage');
+            }
+        } catch (e) {
+            showFaultModal('相册图片', e);
         }
     });
     document.getElementById('btnGalleryVideo').addEventListener('click', function () {
-        if (useUniNative()) {
-            imSDK.chooseVideo({
-                sourceType: ['album'],
-                maxDuration: 120,
-                camera: 'back',
-                success: function (res) { uploadNativePath(nativeTempPath(res)); }
-            });
-        } else {
-            triggerFileInput('inpGalleryVideo');
+        try {
+            if (useUniNative()) {
+                imSDK.chooseVideo({
+                    sourceType: ['album'],
+                    maxDuration: 120,
+                    camera: 'back',
+                    success: handleNativePickResult
+                });
+            } else {
+                triggerFileInput('inpGalleryVideo');
+            }
+        } catch (e) {
+            showFaultModal('相册视频', e);
         }
     });
     document.getElementById('btnAnyFile').addEventListener('click', function () {
-        if (useUniNative()) {
-            imSDK.chooseFile({
-                count: 1,
-                extension: [],
-                success: function (res) {
-                    var path = nativeTempPath(res);
-                    if (!path && res.tempFiles && res.tempFiles[0]) path = res.tempFiles[0].path;
-                    uploadNativePath(path);
-                }
-            });
-        } else {
-            triggerFileInput('inpAnyFile');
+        try {
+            if (useUniNative()) {
+                imSDK.chooseFile({
+                    count: 1,
+                    extension: [],
+                    success: function (res) {
+                        try {
+                            if (res && res.errMsg) {
+                                showFaultModal('原生选择文件失败', res.errMsg);
+                                return;
+                            }
+                            var path = nativeTempPath(res);
+                            if (!path && res.tempFiles && res.tempFiles[0]) path = res.tempFiles[0].path;
+                            uploadNativePath(path);
+                        } catch (e) {
+                            showFaultModal('处理选择文件结果异常', e);
+                        }
+                    }
+                });
+            } else {
+                triggerFileInput('inpAnyFile');
+            }
+        } catch (e) {
+            showFaultModal('选择文件', e);
         }
     });
 
     document.getElementById('btnShare').addEventListener('click', async function () {
-        if (!currentUrl) {
-            showMessage('没有可分享的链接', 'error');
-            return;
-        }
-        if (navigator.share) {
-            try {
-                await navigator.share({ title: '附件链接', text: currentFilename, url: currentUrl });
+        try {
+            if (!currentUrl) {
+                showFaultModal('无法分享', '当前没有可分享的附件链接,请先上传');
                 return;
+            }
+            if (navigator.share) {
+                try {
+                    await navigator.share({ title: '附件链接', text: currentFilename, url: currentUrl });
+                    return;
+                } catch (e) {
+                    if (e.name === 'AbortError') return;
+                    console.warn('[attachment-test] share failed, fallback clipboard', e);
+                }
+            }
+            try {
+                await navigator.clipboard.writeText(currentUrl);
+                showMessage('链接已复制到剪贴板');
             } catch (e) {
-                if (e.name === 'AbortError') return;
+                urlBox.style.display = 'block';
+                urlBox.textContent = currentUrl;
+                showFaultModal('复制链接失败', e);
             }
-        }
-        try {
-            await navigator.clipboard.writeText(currentUrl);
-            showMessage('链接已复制到剪贴板');
         } catch (e) {
-            urlBox.style.display = 'block';
-            urlBox.textContent = currentUrl;
-            showMessage('请手动复制上方链接', 'error');
+            showFaultModal('分享', e);
         }
     });
 
@@ -399,15 +596,20 @@
                 showMessage(data.message || '已删除');
                 renderPreview({ has_file: false });
             } else {
-                showMessage(data.message || '删除失败', 'error');
+                showFaultModal('删除失败', data.message || JSON.stringify(data));
             }
         } catch (e) {
-            showMessage('删除失败: ' + e.message, 'error');
+            showFaultModal('删除请求异常', e);
         } finally {
             showLoading(false);
         }
     });
 
+    window.addEventListener('unhandledrejection', function (ev) {
+        console.error('[attachment-test] unhandledrejection', ev.reason);
+        showFaultModal('未处理的 Promise 异常', ev.reason instanceof Error ? ev.reason : String(ev.reason));
+    });
+
     refreshStatus();
 })();
 </script>

+ 42 - 10
templates/js/im-sdk.js

@@ -44,27 +44,59 @@
             window.imBridgeCallbacks[callbackId] = callback;
         }
 
-        // 确保 uni.webView 已经加载完毕再发送
+        // 等待 uni.webview.js 就绪;CDN 失败时超时,避免无限轮询
+        var attempts = 0;
+        var maxAttempts = 50;
         var checkAndSend = function() {
             if (window.uni && window.uni.webView) {
-                window.uni.webView.postMessage({
-                    data: Object.assign({ action: action, callbackId: callbackId }, params)
-                });
+                console.log('[imSDK] postMessage action=', action, 'callbackId=', callbackId);
+                try {
+                    window.uni.webView.postMessage({
+                        data: Object.assign({ action: action, callbackId: callbackId }, params)
+                    });
+                } catch (e) {
+                    console.error('[imSDK] postMessage 异常:', e);
+                    if (typeof callback === 'function') {
+                        delete window.imBridgeCallbacks[callbackId];
+                        callback({ errMsg: 'postMessage failed: ' + (e && e.message) });
+                    }
+                }
             } else {
-                setTimeout(checkAndSend, 100); // 轮询等待 SDK 加载
+                attempts += 1;
+                if (attempts > maxAttempts) {
+                    console.error('[imSDK] uni.webView 未就绪(请检查 CDN 或是否在 App 内),action=', action);
+                    if (typeof callback === 'function') {
+                        delete window.imBridgeCallbacks[callbackId];
+                        callback({ errMsg: 'uni.webView not ready' });
+                    }
+                    return;
+                }
+                setTimeout(checkAndSend, 100);
             }
         };
         checkAndSend();
     }
 
     /**
-     * 是否在 UniApp WebView 内(用于走 uni 原生拍照/相册/录像,避免 H5 直接调 input file)
-     * 优先级:注入标记 __IN_UNIAPP__ > UA 含 uni-app > 已存在 uni.webView
+     * 是否在 UniApp / DCloud App WebView 内(走 postMessage,由壳子调 uni API)
+     * 不可使用 uni.webView 判断:普通浏览器加载 uni.webview.js 后也会有该对象,会误判导致点击无反应。
+     * Android 常见 UA 无 "uni-app" 字样,但有 Html5Plus。
      */
     function isUniAppIm() {
-        if (window.__IN_UNIAPP__ === true) return true;
-        if (/uni-app/i.test(navigator.userAgent || '')) return true;
-        if (window.uni && window.uni.webView) return true;
+        if (window.__IN_UNIAPP__ === true) {
+            console.log('[imSDK] isUniAppIm: true (__IN_UNIAPP__)');
+            return true;
+        }
+        var ua = navigator.userAgent || '';
+        if (/uni-app/i.test(ua)) {
+            console.log('[imSDK] isUniAppIm: true (UA: uni-app)');
+            return true;
+        }
+        if (/Html5Plus/i.test(ua)) {
+            console.log('[imSDK] isUniAppIm: true (UA: Html5Plus / DCloud Android 等)');
+            return true;
+        }
+        console.log('[imSDK] isUniAppIm: false — 使用浏览器 file。UA:', ua.slice(0, 160));
         return false;
     }