修改动态心电界面

This commit is contained in:
lxd 2025-07-15 16:32:25 +08:00
parent 38d7256439
commit 8b8890f892
2 changed files with 577 additions and 129 deletions

View File

@ -35,6 +35,13 @@ export const ecgdataApi = {
return await request.get({ url: `/system/ecgdata/get?id=` + id })
},
// 申请上级审核
applySuperiorReview: async (id: number, orgid: string) => {
return await request.get({
url: `/system/ecgdata/apply-superior-review?id=` + id + '&orgid=' + orgid
})
},
// 新增心电图动态数据
createecgdata: async (data: ecgdataVO) => {
return await request.post({ url: `/system/ecgdata/create`, data })
@ -49,6 +56,10 @@ export const ecgdataApi = {
updateecgdata: async (data: ecgdataVO) => {
return await request.put({ url: `/system/ecgdata/update`, data })
},
// 修改心电图动态数据佩戴开始时间
updatewearstarttime: async (data: any) => {
return await request.put({ url: `/system/ecgdata/update-wearstarttime`, data })
},
// 删除心电图动态数据
deleteecgdata: async (id: number) => {
@ -60,3 +71,16 @@ export const ecgdataApi = {
return await request.download({ url: `/system/ecgdata/export-excel`, params })
}
}
/** 调用本地Holter分析程序 */
export const callHolterAnalysis = (data: {
examid: string
patientName: string
examDate: string
// 其他参数
}) => {
return request.post({
url: '/ecgdata/call-holter-analysis',
data
})
}

View File

@ -110,65 +110,103 @@
class="modern-table"
row-key="id"
>
<el-table-column prop="examid" label="检查ID" width="320" align="left">
<el-table-column
prop="examid"
label="检查ID"
width="270"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-tooltip :content="row.examid" placement="top" :show-after="500">
<div class="exam-id">
<Icon icon="ep:document" />
<span>{{ row.examid }}</span>
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="120" align="left">
<el-table-column prop="name" label="姓名" width="120" align="center" />
<el-table-column prop="gender" label="性别" width="70" align="center" show-overflow-tooltip>
<template #default="{ row }">
<div class="patient-name">
<el-avatar :size="28" class="patient-avatar">
{{ row.name?.charAt(0) }}
</el-avatar>
<span>{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="gender" label="性别" width="70" align="center">
<template #default="{ row }">
<el-tag :type="row.gender === '1' ? 'primary' : 'danger'" class="gender-tag">
<Icon :icon="row.gender === '1' ? 'ep:male' : 'ep:female'" />
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="age" label="年龄" width="90" align="center">
<template #default="{ row }">
<div class="age-badge">{{ row.age }}</div>
</template>
<el-table-column prop="age" label="年龄" width="70" align="center" show-overflow-tooltip>
<template #default="{ row }"> {{ row.age }} </template>
</el-table-column>
<el-table-column prop="wearstarttime" label="佩戴开始时间" width="210" align="center">
<el-table-column
prop="wearstarttime"
label="佩戴开始时间"
width="210"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<div v-if="row.isEditingWearTime" class="wear-time-editor">
<el-date-picker
v-model="row.wearstarttime"
v-model="row.tempWearTime"
type="datetime"
placeholder="选择佩戴时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
size="small"
style="width: 190px"
@change="handleWearTimeChange(row)"
style="width: 140px"
:popper-class="'no-tooltip'"
/>
</template>
</el-table-column>
<el-table-column prop="duration" label="时长" width="140" align="center">
<template #default="{ row }">
<el-time-picker
v-model="row.duration"
placeholder="选择时长"
format="HH:mm:ss"
value-format="HH:mm:ss"
<div class="edit-actions">
<el-button
type="success"
size="small"
style="width: 120px"
@change="handleDurationChange(row)"
/>
circle
@click="confirmWearTimeChange(row)"
class="confirm-btn"
>
<Icon icon="ep:check" />
</el-button>
<el-button
type="danger"
size="small"
circle
@click="cancelWearTimeChange(row)"
class="cancel-btn"
>
<Icon icon="ep:close" />
</el-button>
</div>
</div>
<div v-else class="wear-time-display">
<div v-if="row.wearstarttime" class="time-content">
<span class="time-text">{{ formatWearTime(row.wearstarttime) }}</span>
</div>
<div class="edit-button-wrapper">
<el-button
type="primary"
size="small"
circle
@click="startEditWearTime(row)"
class="edit-btn"
>
<Icon icon="ep:edit" />
</el-button>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="分析" width="100" align="center">
<el-table-column
prop="duration"
label="时长"
width="120"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-tag type="info" class="duration-tag">
<Icon icon="ep:timer" />
{{ formatDuration(row.duration) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="分析" width="100" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-button
type="primary"
@ -181,31 +219,35 @@
</el-button>
</template>
</el-table-column>
<el-table-column prop="reportgenerated" label="报告" width="100" align="center">
<el-table-column
prop="reportgenerated"
label="报告"
width="100"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-tag :type="row.reportgenerated === 1 ? 'success' : 'warning'" class="status-tag">
<Icon :icon="row.reportgenerated === 1 ? 'ep:circle-check' : 'ep:clock'" />
{{ row.reportgenerated === 1 ? '已生成' : '未生成' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="上传" width="100" align="center">
<el-table-column label="导入数据" width="110" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-button type="primary" size="small" class="upload-btn" @click="handleUpload(row)">
<el-upload
:show-file-list="false"
:before-upload="(file) => handleUpload(row, file)"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.gif"
class="upload-component"
>
<el-button type="primary" size="small" class="upload-btn">
<Icon icon="ep:upload" />
上传
导入数据
</el-button>
</el-upload>
</template>
</el-table-column>
<el-table-column prop="superiorrequest" label="申请" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.superiorrequest === 1 ? 'success' : 'info'" class="status-tag">
<Icon :icon="row.superiorrequest === 1 ? 'ep:check' : 'ep:circle-close'" />
{{ row.superiorrequest === 1 ? '已申请' : '未申请' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="下载" width="100" align="center">
<el-table-column label="下载" width="100" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-button
type="success"
@ -218,7 +260,49 @@
</el-button>
</template>
</el-table-column>
<el-table-column prop="orgname" label="机构" width="160" align="left" show-overflow-tooltip>
<el-table-column label="上传" width="110" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-upload
:show-file-list="false"
:before-upload="(file) => handleUpload(row, file)"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.gif"
class="upload-component"
>
<el-button type="success" size="small" class="upload-btn">
<Icon icon="ep:upload" />
上传
</el-button>
</el-upload>
</template>
</el-table-column>
<el-table-column
prop="managerorg"
label="申请"
width="100"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-button
v-if="!row.managerorg"
type="warning"
size="small"
class="apply-btn"
@click="handleApply(row)"
>
<Icon icon="ep:document-add" />
未申请
</el-button>
<el-tag v-else type="success" class="status-tag"> 已申请 </el-tag>
</template>
</el-table-column>
<el-table-column
prop="orgname"
label="机构"
width="130"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<div class="org-info">
<Icon icon="ep:office-building" />
@ -258,9 +342,10 @@ import { ElMessage, ElMessageBox } 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'
defineOptions({ name: 'AnalysisHolter' })
const Profilevo = ref<ProfileVO>({} as ProfileVO) //
const loading = ref(false)
const exportLoading = ref(false)
const total = ref(0)
@ -291,6 +376,45 @@ const formatDate = (date: Date | string) => {
})
}
/** 格式化佩戴时间显示 */
const formatWearTime = (date: Date | string) => {
if (!date) return '未设置'
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hour = String(d.getHours()).padStart(2, '0')
const minute = String(d.getMinutes()).padStart(2, '0')
const second = String(d.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}
/** 格式化时长显示 */
const formatDuration = (duration: string) => {
if (!duration) return '0时0分'
// "00:30:45" "1:20:30"
const parts = duration.split(':')
if (parts.length !== 3) return duration
const hours = parseInt(parts[0]) || 0
const minutes = parseInt(parts[1]) || 0
const seconds = parseInt(parts[2]) || 0
//
if (hours === 0 && minutes === 0 && seconds > 0) {
return `${seconds}`
}
//
if (hours === 0) {
return `${minutes}${seconds > 0 ? seconds + '秒' : ''}`
}
//
return `${hours}${minutes}${seconds > 0 ? seconds + '秒' : ''}`
}
/** 获取列表数据 */
const getList = async () => {
loading.value = true
@ -302,7 +426,8 @@ const getList = async () => {
name: queryParams.name || undefined,
gender: queryParams.gender || undefined,
examid: queryParams.examid || undefined,
orgname: queryParams.orgname || undefined
orgname: queryParams.orgname || undefined,
orgid: Profilevo.value.orgid || undefined
}
//
@ -335,15 +460,137 @@ const resetQuery = () => {
}
/** 分析 */
const handleAnalysis = (row) => {
ElMessage.success('开始分析...')
// API
const handleAnalysis = async (row) => {
try {
// exe
const params = {
examid: row.examid,
patientName: row.name,
examDate: row.wearstarttime,
duration: row.duration,
orgname: row.orgname,
orgid: Profilevo.value.orgid
}
// 1: exe ()
await callLocalExeViaRegistry(params)
ElMessage.success('正在启动Holter分析程序...')
} catch (error) {
console.error('启动分析程序失败:', error)
ElMessage.error('启动分析程序失败,请检查程序是否正确安装')
}
}
/** 通过注册表调用本地exe程序 */
const callLocalExeViaRegistry = async (params: any): Promise<void> => {
return new Promise<void>((resolve, reject) => {
try {
//
const encodedParams = encodeURIComponent(JSON.stringify(params))
// 使exe ()
const protocolUrl = `holter-analysis://${encodedParams}`
//
const link = document.createElement('a')
link.href = protocolUrl
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
resolve()
} catch (error) {
reject(new Error('无法启动本地程序,请检查程序是否正确安装'))
}
})
}
/** 上传 */
const handleUpload = (row) => {
ElMessage.success('上传功能')
//
const handleUpload = async (row, file) => {
try {
// (10MB)
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
ElMessage.error('文件大小不能超过10MB')
return false
}
//
const allowedTypes = [
'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文档和图片格式')
return false
}
//
const loading = ElMessage({
message: '正在上传文件...',
type: 'info',
duration: 0
})
// 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
})
if (response.ok) {
const result = await response.json()
loading.close()
ElMessage.success('文件上传成功')
getList() //
} else {
throw new Error('上传失败')
}
} catch (error) {
loading.close()
throw error
}
return false //
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('文件上传失败,请重试')
return false
}
}
/** 申请上级审核 */
const handleApply = async (row) => {
try {
await ElMessageBox.confirm(`确定要为患者 ${row.name} 申请上级审核吗?`, '申请确认', {
confirmButtonText: '确定申请',
cancelButtonText: '取消',
type: 'warning'
})
// API
await ecgdataApi.applySuperiorReview(row.id, row.orgid)
ElMessage.success('申请已提交,等待上级审核')
//
getList()
} catch (error) {
if (error !== 'cancel') {
console.error('申请失败:', error)
ElMessage.error('申请失败,请重试')
}
}
}
/** 下载 */
@ -421,11 +668,59 @@ const handlePatientCancel = () => {
console.log('取消选择患者')
}
/** 开始编辑佩戴时间 */
const startEditWearTime = (
row: ecgdataVO & { isEditingWearTime?: boolean; tempWearTime?: any }
) => {
row.isEditingWearTime = true
row.tempWearTime = row.wearstarttime
}
/** 确认佩戴时间修改 */
const confirmWearTimeChange = async (
row: ecgdataVO & { isEditingWearTime?: boolean; tempWearTime?: any }
) => {
try {
if (!row.tempWearTime) {
ElMessage.warning('请选择佩戴时间')
return
}
// 使
let formattedTime = row.tempWearTime
if (row.tempWearTime) {
//
formattedTime = new Date(row.tempWearTime).getTime()
}
// API
await ecgdataApi.updatewearstarttime({
id: row.id,
wearstarttime: formattedTime
})
row.wearstarttime = row.tempWearTime
row.isEditingWearTime = false
ElMessage.success('佩戴时间更新成功')
getList()
} catch (error) {
console.error('更新佩戴时间失败:', error)
ElMessage.error('更新佩戴时间失败')
}
}
/** 取消佩戴时间修改 */
const cancelWearTimeChange = (
row: ecgdataVO & { isEditingWearTime?: boolean; tempWearTime?: any }
) => {
row.isEditingWearTime = false
row.tempWearTime = null
}
/** 处理佩戴时间变化 */
const handleWearTimeChange = async (row: ecgdataVO) => {
try {
console.log('佩戴时间已更新:', row.wearstarttime)
// API
// await ecgdataApi.updateecgdata({
// id: row.id,
// wearstarttime: row.wearstarttime
@ -437,24 +732,9 @@ const handleWearTimeChange = async (row: ecgdataVO) => {
}
}
/** 处理时长变化 */
const handleDurationChange = async (row: ecgdataVO) => {
try {
console.log('时长已更新:', row.duration)
// API
// await ecgdataApi.updateecgdata({
// id: row.id,
// duration: row.duration
// })
ElMessage.success('时长更新成功')
} catch (error) {
console.error('更新时长失败:', error)
ElMessage.error('更新时长失败')
}
}
/** 初始化 */
onMounted(() => {
onMounted(async () => {
Profilevo.value = await getUserProfile()
getList()
})
</script>
@ -579,10 +859,6 @@ onMounted(() => {
gap: 4px;
font-weight: 600;
color: #667eea;
.ep-document {
font-size: 12px;
}
}
.patient-name {
@ -597,27 +873,6 @@ onMounted(() => {
}
}
.gender-tag {
border-radius: 12px;
padding: 1px 6px;
font-weight: 500;
font-size: 11px;
.ep-male,
.ep-female {
margin-right: 1px;
}
}
.age-badge {
background: linear-gradient(135deg, #f093fb, #f5576c);
color: white;
padding: 1px 6px;
border-radius: 12px;
font-weight: 500;
font-size: 11px;
}
//
:deep(.el-date-picker),
:deep(.el-time-picker) {
@ -648,6 +903,23 @@ onMounted(() => {
}
}
// tooltip
:deep(.no-tooltip) {
.el-tooltip__popper {
display: none !important;
}
}
// hover
:deep(.el-date-picker),
:deep(.el-time-picker) {
.el-input__wrapper {
&:hover::after {
display: none !important;
}
}
}
.time-info {
display: flex;
align-items: center;
@ -662,18 +934,23 @@ onMounted(() => {
.duration-tag {
border-radius: 12px;
padding: 1px 6px;
padding: 2px 8px;
font-weight: 500;
font-size: 11px;
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
border: 1px solid #bae6fd;
color: #0369a1;
.ep-timer {
margin-right: 1px;
margin-right: 3px;
color: #0891b2;
}
}
.analysis-btn,
.upload-btn,
.download-btn {
.download-btn,
.apply-btn {
border-radius: 12px;
padding: 2px 8px;
font-weight: 500;
@ -699,8 +976,12 @@ onMounted(() => {
background: linear-gradient(135deg, #43e97b, #38f9d7);
}
.apply-btn {
background: linear-gradient(135deg, #ff9a56, #ff6b6b);
}
.status-tag {
border-radius: 12px;
border-radius: 4px;
padding: 1px 6px;
font-weight: 500;
font-size: 11px;
@ -725,6 +1006,111 @@ onMounted(() => {
color: #667eea;
}
}
//
.upload-component {
display: inline-block;
:deep(.el-upload) {
display: inline-block;
}
:deep(.el-upload-dragger) {
display: none;
}
}
//
.wear-time-display {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px;
border-radius: 6px;
transition: all 0.3s ease;
.time-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.time-text {
font-size: 12px;
color: #606266;
}
}
.edit-button-wrapper {
display: flex;
align-items: center;
.edit-btn {
width: 20px;
height: 20px;
padding: 0;
background: transparent;
border: none;
color: #909399;
transition: all 0.3s ease;
&:hover {
color: #409eff;
transform: scale(1.1);
}
.ep-edit {
font-size: 12px;
}
}
}
}
.wear-time-editor {
display: flex;
align-items: center;
gap: 8px;
.edit-actions {
display: flex;
gap: 4px;
.confirm-btn {
width: 24px;
height: 24px;
padding: 0;
background: linear-gradient(135deg, #67c23a, #85ce61);
border: none;
&:hover {
background: linear-gradient(135deg, #5daf34, #73c25a);
transform: scale(1.05);
}
.ep-check {
font-size: 10px;
}
}
.cancel-btn {
width: 24px;
height: 24px;
padding: 0;
background: linear-gradient(135deg, #f56c6c, #f78989);
border: none;
&:hover {
background: linear-gradient(135deg, #e45656, #f56c6c);
transform: scale(1.05);
}
.ep-close {
font-size: 10px;
}
}
}
}
}
.pagination-wrapper {
@ -736,6 +1122,7 @@ onMounted(() => {
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
border: none;
.el-table__header {
background: #f5f7fa;
@ -744,7 +1131,8 @@ onMounted(() => {
background: #f5f7fa;
color: #303133;
font-weight: 600;
padding: 12px 8px; // padding
padding: 12px 8px;
border: none;
}
}
@ -756,16 +1144,31 @@ onMounted(() => {
}
td {
padding: 12px 8px; // padding
padding: 12px 8px;
border: none;
}
}
//
//
.el-table__cell {
border-right: 1px solid #ebeef5;
border: none;
}
&:last-child {
border-right: none;
//
.el-table__border-line {
display: none;
}
// 线
.el-table__inner-wrapper {
border: none;
}
// tooltip
.el-table__cell {
.el-tooltip__trigger {
display: inline-block;
width: 100%;
}
}
}
@ -796,4 +1199,25 @@ onMounted(() => {
}
}
}
// tooltiptooltip
:deep(.el-popper) {
&[data-popper-reference-hidden] {
display: none !important;
}
}
// tooltiptooltip
:deep(.no-tooltip) {
.el-tooltip__popper {
display: none !important;
}
}
// tooltip
:deep(.el-table) {
.el-tooltip__popper {
display: block !important;
}
}
</style>