|
|
@@ -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>
|