动态心电界面增加导入数据分片上传

This commit is contained in:
lxd 2025-07-16 14:54:12 +08:00
parent 2c21ec062f
commit 4f36f36c0a
6 changed files with 278 additions and 51 deletions

View File

@ -34,4 +34,7 @@ VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
VITE_APP_CAPTCHA_ENABLE=true
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
VITE_GOVIEW_URL='http://127.0.0.1:3000'
# 文件上传地址(可选)
VITE_FILE_UPLOAD_URL='http://114.55.171.231:6042/api/file/upload-chunked'

View File

@ -31,4 +31,7 @@ VITE_MALL_H5_DOMAIN='http://localhost:3000'
VITE_APP_CAPTCHA_ENABLE=false
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
VITE_GOVIEW_URL='http://127.0.0.1:3000'
# 文件上传地址(可选)
VITE_FILE_UPLOAD_URL='http://114.55.171.231:6042/api/file/upload-chunked'

View File

@ -60,6 +60,10 @@ export const ecgdataApi = {
updatewearstarttime: async (data: any) => {
return await request.put({ url: `/system/ecgdata/update-wearstarttime`, data })
},
//更新心电图的文件名称和压缩文件名称
upecgfilename: async (data: any) => {
return await request.put({ url: `/system/ecgdata/upecgfilename`, data })
},
// 删除心电图动态数据
deleteecgdata: async (id: number) => {

169
src/utils/upload.ts Normal file
View File

@ -0,0 +1,169 @@
/**
*
*/
export interface ChunkUploadOptions {
file: File
chunkSize?: number // 分片大小默认1MB
url: string
name: string
onProgress?: (progress: number, currentChunk: number, totalChunks: number) => void
onError?: (error: string, chunkIndex?: number) => void
retryTimes?: number // 重试次数默认3次
retryDelay?: number // 重试延迟默认1000ms
}
export interface ChunkUploadResult {
success: boolean
message?: string
data?: any
}
/**
*
*/
export async function uploadFileInChunks(options: ChunkUploadOptions): Promise<ChunkUploadResult> {
const {
file,
chunkSize = 1024 * 1024, // 默认1MB
url,
name,
onProgress,
onError,
retryTimes = 3,
retryDelay = 1000
} = options
const totalChunks = Math.ceil(file.size / chunkSize)
const uploadedChunks = new Set<number>() // 记录已上传的分片
try {
// 分片上传
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
let retryCount = 0
let success = false
while (retryCount < retryTimes && !success) {
try {
// 如果已经上传过,跳过
if (uploadedChunks.has(chunkIndex)) {
success = true
break
}
// 计算当前分片的起始和结束位置
const start = chunkIndex * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
// 创建FormData
const formData = new FormData()
formData.append('file', chunk, file.name)
formData.append('Name', name)
formData.append('chunkIndex', chunkIndex.toString())
formData.append('totalChunks', totalChunks.toString())
// 发送请求
const response = await fetch(url, {
method: 'POST',
body: formData
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.message || `分片 ${chunkIndex + 1} 上传失败`)
}
const result = await response.json()
if (!result.success) {
throw new Error(result.message || `分片 ${chunkIndex + 1} 上传失败`)
}
// 标记为已上传
uploadedChunks.add(chunkIndex)
success = true
// 调用进度回调
if (onProgress) {
const progress = Math.round(((chunkIndex + 1) / totalChunks) * 100)
onProgress(progress, chunkIndex + 1, totalChunks)
}
// 添加小延迟,避免请求过于频繁
await new Promise((resolve) => setTimeout(resolve, 100))
} catch (error) {
retryCount++
console.error(`分片 ${chunkIndex + 1} 上传失败,重试 ${retryCount}/${retryTimes}:`, error)
if (retryCount >= retryTimes) {
// 重试次数用完,抛出错误
const errorMessage = error instanceof Error ? error.message : '上传失败'
if (onError) {
onError(errorMessage, chunkIndex)
}
throw new Error(`分片 ${chunkIndex + 1} 上传失败: ${errorMessage}`)
}
// 等待后重试
await new Promise((resolve) => setTimeout(resolve, retryDelay))
}
}
}
return {
success: true,
message: '文件上传成功',
data: { totalChunks, uploadedChunks: Array.from(uploadedChunks) }
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '文件上传失败'
return {
success: false,
message: errorMessage
}
}
}
/**
*
*/
export function isFileSupported(file: File): boolean {
const maxSize = 100 * 1024 * 1024 // 100MB
return file.size <= maxSize
}
/**
*
*/
export function calculateChunkInfo(file: File, chunkSize: number = 1024 * 1024) {
const totalChunks = Math.ceil(file.size / chunkSize)
const lastChunkSize = file.size % chunkSize || chunkSize
return {
totalChunks,
chunkSize,
lastChunkSize,
fileSize: file.size,
estimatedTime: Math.ceil(totalChunks * 0.5) // 预估时间(秒)
}
}
/**
*
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
/**
*
*/
export function formatUploadSpeed(bytesPerSecond: number): string {
return formatFileSize(bytesPerSecond) + '/s'
}

View File

@ -338,11 +338,12 @@
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import PatientSelect from '@/patientcom/index.vue'
import { ecgdataApi, ecgdataVO } from '@/api/ecgdata'
import { Search, Refresh, Plus, Download } from '@element-plus/icons-vue'
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
import { uploadFileInChunks, formatFileSize, calculateChunkInfo } from '@/utils/upload'
defineOptions({ name: 'AnalysisHolter' })
const Profilevo = ref<ProfileVO>({} as ProfileVO) //
@ -510,53 +511,67 @@ const callLocalExeViaRegistry = async (params: any): Promise<void> => {
/** 上传 */
const handleUpload = async (row, file) => {
try {
// (10MB)
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
ElMessage.error('文件大小不能超过10MB')
return false
}
//
const allowedTypes = [
'application/hlf',
'application/zip',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/png',
'image/gif'
''
]
if (!allowedTypes.includes(file.type)) {
ElMessage.error('只支持PDF、Word文档和图片格式')
ElMessage.error('只支持.hlf、.zip、.pdf和.doc文件')
return false
}
//
const loading = ElMessage({
message: '正在上传文件...',
type: 'info',
duration: 0
//
const loading = ElLoading.service({
lock: true,
text: `准备上传文件 ${file.name} (${formatFileSize(file.size)})...`,
background: 'rgba(0, 0, 0, 0.8)',
customClass: 'upload-loading'
})
// FormData
const formData = new FormData()
formData.append('file', file)
try {
//
const response = await fetch('http://114.55.171.231:6042/upload.asp', {
method: 'POST',
body: formData
// 使
const result = await uploadFileInChunks({
file,
url: import.meta.env.VITE_FILE_UPLOAD_URL,
name: row.examid,
chunkSize: 1024 * 1024, // 1MB
retryTimes: 3,
retryDelay: 1000,
onProgress: (progress, currentChunk, totalChunks) => {
//
loading.setText(
`正在上传文件 ${file.name}\n` +
`进度: ${progress}% (${currentChunk}/${totalChunks})\n` +
`文件大小: ${formatFileSize(file.size)}`
)
},
onError: (error, chunkIndex) => {
console.error(`分片 ${chunkIndex} 上传失败:`, error)
//
}
})
if (response.ok) {
const result = await response.json()
loading.close()
ElMessage.success('文件上传成功')
getList() //
} else {
throw new Error('上传失败')
if (!result.success) {
throw new Error(result.message)
}
//
loading.setText('文件上传完成,正在处理...')
//
await ecgdataApi.upecgfilename({
id: row.id,
filename: row.examid + '.hlf'
})
loading.close()
ElMessage.success(`文件 ${file.name} 上传成功`)
getList() //
} catch (error) {
loading.close()
throw error
@ -565,7 +580,8 @@ const handleUpload = async (row, file) => {
return false //
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('文件上传失败,请重试')
const errorMessage = error instanceof Error ? error.message : '文件上传失败,请重试'
ElMessage.error(`文件上传失败: ${errorMessage}`)
return false
}
}
@ -717,21 +733,6 @@ const cancelWearTimeChange = (
row.tempWearTime = null
}
/** 处理佩戴时间变化 */
const handleWearTimeChange = async (row: ecgdataVO) => {
try {
console.log('佩戴时间已更新:', row.wearstarttime)
// await ecgdataApi.updateecgdata({
// id: row.id,
// wearstarttime: row.wearstarttime
// })
ElMessage.success('佩戴时间更新成功')
} catch (error) {
console.error('更新佩戴时间失败:', error)
ElMessage.error('更新佩戴时间失败')
}
}
/** 初始化 */
onMounted(async () => {
Profilevo.value = await getUserProfile()
@ -1220,4 +1221,50 @@ onMounted(async () => {
display: block !important;
}
}
// loading
:deep(.upload-loading) {
.el-loading-spinner {
.el-loading-text {
color: #fff;
font-size: 16px;
font-weight: 500;
margin-top: 20px;
}
.circular {
width: 60px;
height: 60px;
animation: rotate 2s linear infinite;
.path {
stroke: #409eff;
stroke-width: 4;
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}
}
}
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}
</style>

1
types/env.d.ts vendored
View File

@ -26,6 +26,7 @@ interface ImportMetaEnv {
readonly VITE_SOURCEMAP: string
readonly VITE_OUT_DIR: string
readonly VITE_GOVIEW_URL: string
readonly VITE_FILE_UPLOAD_URL: string
}
declare global {