PDF 下载错误详情:, TypeError: undefined is not a function (near '...uni.base64ToPath...') at pages/home/components/Imagepreview.vue:800 __ERROR
<template>
<view class="evidence-card-container">
<!-- 原有卡片布局 -->
<view class="card-row">
<view
class="card-item"
v-for="(item, index) in evidenceList.slice(0, 3)"
:key="index"
@click="handleClick(item)"
>
<image v-if="item.icon" class="item-icon" :src="item.icon" mode="aspectFit" />
<text class="item-text">{{ item.text }}</text>
</view>
</view>
<view class="card-row">
<view
class="card-item"
v-for="(item, index) in evidenceList.slice(3, 6)"
:key="index + 3"
@click="handleClick(item)"
>
<image v-if="item.icon" class="item-icon" :src="item.icon" mode="aspectFit" />
<text class="item-text">{{ item.text }}</text>
</view>
</view>
<!-- 拍照预览弹窗 -->
<view v-if="showPhotoPreview" class="photo-preview-mask">
<view class="preview-content">
<!-- 图片预览区域 -->
<image
class="preview-image"
:src="currentPreviewImg"
mode="aspectFit"
@click="handleImageClick"
></image>
<!-- 底部操作按钮 -->
<view class="preview-footer">
<button class="preview-btn cancel-btn" @click="cancelPreview">取消</button>
<button class="preview-btn confirm-btn" @click="confirmPreview">固化</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const showPhotoPreview = ref(false) // 控制预览弹窗显示
const currentPreviewImg = ref('') // 当前预览的图片地址
const tempPhotoPath = ref('') // 临时存储拍照路径(用于H5)
// 定义单个卡片数据结构
interface EvidenceItem {
text: string
icon?: string // 图标地址,可选
action?: () => void // 点击回调,可选
}
// 合并所有取证选项到一个列表
const evidenceList = ref<EvidenceItem[]>([
{ text: '拍照取证', icon: '/static/home/ObtainEvidence/takepictures.svg' },
{ text: '录像取证', icon: '/static/home/ObtainEvidence/picturerecording.svg' },
{ text: '录音取证', icon: '/static/home/ObtainEvidence/soundrecording.svg' },
{ text: '网页取证', icon: '/static/home/ObtainEvidence/Webpage.svg' },
{ text: '录屏取证', icon: '/static/home/ObtainEvidence/screencap.svg' },
{ text: '委托取证', icon: '/static/home/ObtainEvidence/commission.svg' },
])
const showProgress = ref(false) // 控制进度条是否显示
const progress = ref(0) // 上传进度(0-100)
const isUploading = ref(false) // 标记是否正在上传(避免重复上传)
// 录像取证
const uploadVideo = (filePath: string, duration: number, size: number) => {
// 1. 处理文件名
const originalFileName = filePath.substring(filePath.lastIndexOf('/') + 1)
const fileExtension = originalFileName.includes('.') ? originalFileName.split('.').pop() : 'mp4'
const timestamp = new Date().getTime()
const newFileName = `video_evidence_${timestamp}.${fileExtension}`
// 2. 显示进度
showProgress.value = true
progress.value = 0
isUploading.value = true
// 3. 显示加载提示
let loadingVisible = false // 标记loading是否显示
const showLoading = (title: string) => {
if (!loadingVisible) {
uni.showLoading({ title, mask: true })
loadingVisible = true
} else {
// uni.setLoadingText(title); // 已有loading时,只更新文本
}
}
showLoading('准备上传...')
// 4. 创建上传任务
const uploadTask = uni.uploadFile({
// url: 'http://192.168.1.80:1592/api/upload',
url: 'http://192.168.0.111:1592/api/upload',
filePath: filePath,
name: 'file',
formData: {
originalFileName: originalFileName,
customFileName: newFileName,
duration: duration,
size: size,
type: 'video',
uploadTime: new Date().toISOString(),
},
header: {
// 'Authorization': 'Bearer your-token-here' // 如果需要身份验证
},
timeout: 300000, // 5分钟超时
success: (uploadRes) => {
console.log('上传成功,原始响应:', uploadRes.data)
try {
const data = JSON.parse(uploadRes.data)
if (data.code === 200) {
if (loadingVisible) {
uni.hideLoading()
loadingVisible = false
}
// 显示成功提示
uni.showModal({
title: '固化成功!',
content: '您的证据已保存至“我的证据”',
confirmText: '查看证据',
cancelText: '继续取证',
success: (res) => {
if (res.confirm) {
// 后端返回的单个证据对象(含 base64Data、webpagePdfBase64 等字段)
const singleEvidence = {
...data.data, // 合并previewImage、filePreview等基础信息
// 明确提取PDF相关字段(关键:以你的后端实际返回层级为准)
pdfData : data.data.pdfData, // 若后端在data.data下
webpagePdfBase64: data.data.webpagePdfBase64 ,
// 补充必要字段(确保详情页能正常显示)
evidenceType: data.data.evidenceType,
evidenceName: data.data.evidenceName,
acquisitionTime: data.data.acquisitionTime, // 取证时间(后端未返回则前端生成)
timestamp: new Date().getTime() // 用于生成唯一storageKey
};
const storageKey = `evidence_single_${singleEvidence.timestamp}`;
// 存储完整数据到本地
uni.setStorageSync(storageKey, singleEvidence);
console.log('存储的完整证据数据:', singleEvidence);
// 跳转时传递 storageKey
uni.navigateTo({
url: `/pages/home/components/Imagepreview?storageKey=${storageKey}`
});
// 清理临时数据
cancelPreview();
} else {
uni.navigateTo({ url: '/pages/home/home' });
}
}
});
} else {
throw new Error(data.msg || `上传失败,状态码: ${data.code}`)
}
} catch (e) {
console.error('解析响应失败', e)
if (loadingVisible) {
uni.hideLoading()
loadingVisible = false
}
// 显示错误提示
uni.showToast({
title: e.message || '处理响应失败',
icon: 'none',
duration: 3000,
})
}
},
fail: (err) => {
console.error('上传失败:', err)
if (loadingVisible) {
uni.hideLoading()
loadingVisible = false
}
// 显示失败提示
uni.showToast({
title: `上传失败: ${err.errMsg || '网络错误'}`,
icon: 'none',
duration: 3000,
})
},
complete: () => {
// 确保loading被关闭
if (loadingVisible) {
uni.hideLoading()
loadingVisible = false
}
showProgress.value = false
isUploading.value = false
},
})
// 5. 监听上传进度
uploadTask.onProgressUpdate((res) => {
console.log('上传进度:', res.progress)
progress.value = res.progress
})
}
//拍照取证
const takePhotoAndPreviewPDF = () => {
// #ifdef APP
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera'], // 仅调用相机
success: (res) => {
console.log('APP端拍照成功,临时路径:', res.tempFilePaths[0]);
const tempFilePath = res.tempFilePaths[0];
tempPhotoPath.value = tempFilePath;
currentPreviewImg.value = tempFilePath;
showPhotoPreview.value = true; // 显示预览弹窗
},
fail: (err) => {
console.error('APP端相机调用失败:', err);
// 若用户拒绝权限,引导去设置页
if (err.errMsg.includes('deny') || err.errMsg.includes('denied')) {
uni.showModal({
title: '权限不足',
content: '拍照取证需要相机权限,请在手机设置中开启',
confirmText: '去设置',
success: (res) => {
if (res.confirm) uni.openSetting(); // 打开应用设置
}
});
} else {
uni.showToast({ title: `拍照失败:${err.errMsg}`, icon: 'none' });
}
}
});
// #endif
// #ifdef H5
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['camera'],
success: async (res) => {
console.log('H5拍照结果:', res.tempFilePaths)
const tempFilePath = res.tempFilePaths[0]
tempPhotoPath.value = tempFilePath // 存储临时路径
currentPreviewImg.value = tempFilePath // 直接使用本地临时路径预览
showPhotoPreview.value = true // 显示预览弹窗
},
fail: (err) => {
console.error('H5拍照失败:', err)
uni.showToast({ title: '拍照失败', icon: 'none' })
}
})
//// #endif
}
const handleImageClick = () => {
// showPhotoPreview.value = false
}
// 取消预览
const cancelPreview = () => {
showPhotoPreview.value = false
currentPreviewImg.value = ''
tempPhotoPath.value = ''
}
// 确认预览(可扩展为提交证据等操作)
const confirmPreview = () => {
// 1. 校验:无图片路径或正在上传时,不执行操作
if (!tempPhotoPath.value || isUploading.value) return;
// 2. 处理文件名(与APP端格式一致)
const originalFileName = tempPhotoPath.value.substring(tempPhotoPath.value.lastIndexOf('/') + 1);
const fileExtension = originalFileName.includes('.') ? originalFileName.split('.').pop() : 'jpg';
const timestamp = new Date().getTime();
const newFileName = `evidence_${timestamp}.${fileExtension}`;
// 3. 初始化上传状态
showProgress.value = true;
progress.value = 0;
isUploading.value = true;
uni.showLoading({ title: '图片上传中...', mask: true });
// 4. 调用后端接口上传图片
const uploadTask = uni.uploadFile({
// url: 'http://192.168.1.80:1592/api/upload',
url: 'http://192.168.0.111:1592/api/upload',
filePath: tempPhotoPath.value,
name: 'file',
formData: {
originalFileName: originalFileName,
customFileName: newFileName,
type: 'image',
uploadTime: new Date().toISOString()
},
timeout: 15000, // 15秒超时(图片上传合理时长)
success: (uploadFileRes) => {
console.log('确认上传-后端原始响应:', uploadFileRes.data);
try {
const data = JSON.parse(uploadFileRes.data);
if (data.code === 200) {
uni.navigateTo({
url: '/pages/home/home'
});
uni.showModal({
title: '固化成功!',
content: '您的证据已保存至“我的证据”',
confirmText: '查看证据',
cancelText: '继续取证',
success: (res) => {
if (res.confirm) {
// 后端返回的单个证据对象(含 base64Data、webpagePdfBase64 等字段)
const singleEvidence = {
...data.data, // 合并previewImage、filePreview等基础信息
// 明确提取PDF相关字段(关键:以你的后端实际返回层级为准)
pdfData : data.data.pdfData, // 若后端在data.data下
webpagePdfBase64: data.data.webpagePdfBase64 ,
// 补充必要字段(确保详情页能正常显示)
evidenceType: data.data.evidenceType,
evidenceName: data.data.evidenceName,
acquisitionTime: data.data.acquisitionTime, // 取证时间(后端未返回则前端生成)
timestamp: new Date().getTime() // 用于生成唯一storageKey
};
const storageKey = `evidence_single_${singleEvidence.timestamp}`;
// 存储完整数据到本地
uni.setStorageSync(storageKey, singleEvidence);
console.log('存储的完整证据数据:', singleEvidence);
// 跳转时传递 storageKey
uni.navigateTo({
url: `/pages/home/components/Imagepreview?storageKey=${storageKey}`
});
// 清理临时数据
cancelPreview();
} else {
uni.navigateTo({ url: '/pages/home/home' });
}
}
});
uni.hideLoading()
} else {
throw new Error(data.msg || `上传失败,状态码: ${data.code}`);
}
} catch (e) {
uni.hideLoading();
uni.showToast({ title: `解析失败: ${e.message}`, icon: 'none', duration: 3000 });
}
},
fail: (err) => {
// 9. 上传失败处理(网络错误等)
console.error('H5确认上传-失败:', err);
uni.hideLoading();
uni.showToast({ title: `上传失败: ${err.errMsg}`, icon: 'none', duration: 3000 });
},
complete: () => {
// 10. 无论成功失败,重置状态
showProgress.value = false;
isUploading.value = false;
}
});
// 11. 监听上传进度(更新进度条)
uploadTask.onProgressUpdate((res) => {
progress.value = res.progress;
});
};
//视频取证
const takevideo = () => {
uni.chooseVideo({
sourceType: ['camera'],
compressed: true,
maxDuration: 180,
camera: 'back',
success: (res) => {
console.log('录像结果:', res.tempFilePath)
if (!res.tempFilePath) {
uni.showToast({ title: '未获取到视频文件', icon: 'none' })
return
}
// 检查视频大小 (限制为100MB)
if (res.size > 100 * 1024 * 1024) {
uni.showToast({ title: '视频大小不能超过100MB', icon: 'none' })
return
}
// 准备上传
uploadVideo(res.tempFilePath, res.duration, res.size)
},
fail: (err) => {
console.error('录像失败:', err)
uni.showToast({
title: '录像失败',
icon: 'none',
})
},
})
}
//
const downloadAndPreview = (pdfUrl) => {
uni.downloadFile({
url: pdfUrl,
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
success: () => console.log('预览成功'),
fail: (err) => console.error('打开失败', err),
})
}
},
})
}
// 点击事件处理
const handleClick = (item: EvidenceItem) => {
console.log(`点击了:${item.text}`)
if (item.action) {
item.action() // 如果有自定义回调,优先执行
return
}
// 默认处理逻辑
switch (item.text) {
case '拍照取证':
takePhotoAndPreviewPDF()
break
case '录像取证':
takevideo()
break
case '录音取证':
uni.showToast({
title: '录音取证功能开发中',
icon: 'none',
})
break
case '网页取证':
uni.navigateTo({ url: '/pages/home/components/Webforensics' })
break
case '录屏取证':
// uni.showToast({
// title: '录屏取证功能开发中',
// icon: 'none'
// });
uni.navigateTo({ url: '/pages/home/components/Screenrecord' })
break
case '委托取证':
uni.showToast({
title: '请联系客服',
icon: 'none',
})
break
default:
console.warn('未知的取证类型:', item.text)
}
}
</script>
<style scoped>
.evidence-card-container {
}
.card-row {
display: flex;
flex-direction: row;
justify-content: space-around;
margin-bottom: 30rpx;
}
.card-row:last-child {
margin-bottom: 0;
}
.card-item {
width: 30%;
background-color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 16rpx;
text-align: center;
margin-right: 20rpx;
padding: 20rpx 10rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.card-item:last-child {
margin-right: 0;
}
/* .card-item:hover {
transform: translateY(-3rpx);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
} */
.item-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 10rpx;
}
.item-text {
font-size: 24rpx;
color: #333;
line-height: 1.4;
}
.photo-preview-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 1);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.preview-content {
width: 90%;
max-width: 600px;
position: relative;
}
.preview-image {
width: 100%;
height: 90vh;
object-fit: contain;
margin-bottom: 20px;
border-radius: 8px;
}
.preview-footer {
width: 100%;
display: flex;
justify-content: space-between;
}
.preview-btn {
flex: 1;
height: 44px;
line-height: 44px;
border-radius: 8px;
font-size: 16px;
margin: 0 10px;
}
.cancel-btn {
background-color: #2e8bff;
color: #fff;
}
.confirm-btn {
background-color: #2e8bff;
color: #fff;
}
</style>