动态心电界面增加导入数据分片上传
This commit is contained in:
parent
2c21ec062f
commit
4f36f36c0a
5
.env.dev
5
.env.dev
@ -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'
|
||||
@ -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'
|
||||
@ -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
169
src/utils/upload.ts
Normal 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'
|
||||
}
|
||||
@ -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
1
types/env.d.ts
vendored
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user