feat(emr): 病历修改留痕 — AppService/Controller/前端组件
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package com.healthlink.his.web.emr.appservice;
|
||||
|
||||
import com.healthlink.his.emr.domain.EmrRevision;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IEmrRevisionAppService {
|
||||
|
||||
EmrRevision recordRevision(EmrRevision revision);
|
||||
|
||||
List<EmrRevision> getRevisions(Long emrId);
|
||||
|
||||
EmrRevision getRevisionDetail(Long id);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.healthlink.his.web.emr.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.healthlink.his.emr.domain.EmrRevision;
|
||||
import com.healthlink.his.emr.service.IEmrRevisionService;
|
||||
import com.healthlink.his.web.emr.appservice.IEmrRevisionAppService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class EmrRevisionAppServiceImpl implements IEmrRevisionAppService {
|
||||
|
||||
@Resource
|
||||
private IEmrRevisionService emrRevisionService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public EmrRevision recordRevision(EmrRevision revision) {
|
||||
EmrRevision latest = emrRevisionService.selectLatest(revision.getEmrId());
|
||||
int nextNumber = (latest != null) ? latest.getRevisionNumber() + 1 : 1;
|
||||
revision.setRevisionNumber(nextNumber);
|
||||
if (revision.getEncounterId() == null && latest != null) {
|
||||
revision.setEncounterId(latest.getEncounterId());
|
||||
}
|
||||
revision.setCreateTime(new Date());
|
||||
emrRevisionService.save(revision);
|
||||
return revision;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EmrRevision> getRevisions(Long emrId) {
|
||||
return emrRevisionService.selectByEmrId(emrId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmrRevision getRevisionDetail(Long id) {
|
||||
return emrRevisionService.getById(id);
|
||||
}
|
||||
}
|
||||
@@ -5,26 +5,47 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.emr.domain.EmrRevision;
|
||||
import com.healthlink.his.emr.service.IEmrRevisionService;
|
||||
import com.healthlink.his.web.emr.appservice.IEmrRevisionAppService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 病历修改留痕 Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/emr-revision")
|
||||
@RequestMapping("/emr/revision")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@Tag(name = "病历修改留痕")
|
||||
public class EmrRevisionController {
|
||||
|
||||
private final IEmrRevisionService revisionService;
|
||||
|
||||
private final IEmrRevisionAppService emrRevisionAppService;
|
||||
|
||||
@PostMapping("/record")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
||||
@Operation(summary = "记录修改留痕")
|
||||
public R<EmrRevision> recordRevision(@RequestBody EmrRevision revision) {
|
||||
return R.ok(emrRevisionAppService.recordRevision(revision));
|
||||
}
|
||||
|
||||
@GetMapping("/list/{emrId}")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@Operation(summary = "获取修改历史列表")
|
||||
public R<?> getRevisions(@PathVariable Long emrId) {
|
||||
return R.ok(emrRevisionAppService.getRevisions(emrId));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@Operation(summary = "分页查询修改留痕")
|
||||
public R<?> getPage(
|
||||
@RequestParam(value = "emrId", required = false) Long emrId,
|
||||
@RequestParam(value = "encounterId", required = false) Long encounterId,
|
||||
@@ -39,35 +60,16 @@ public class EmrRevisionController {
|
||||
return R.ok(revisionService.page(new Page<>(pageNo, pageSize), w));
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public R<?> getList(@RequestParam("emrId") Long emrId) {
|
||||
LambdaQueryWrapper<EmrRevision> w = new LambdaQueryWrapper<>();
|
||||
w.eq(EmrRevision::getEmrId, emrId)
|
||||
.orderByAsc(EmrRevision::getRevisionNumber);
|
||||
return R.ok(revisionService.list(w));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@Operation(summary = "获取修订详情")
|
||||
public R<?> getById(@PathVariable Long id) {
|
||||
return R.ok(revisionService.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping("/record")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> recordRevision(@RequestBody EmrRevision revision) {
|
||||
// 自动计算版本号
|
||||
LambdaQueryWrapper<EmrRevision> w = new LambdaQueryWrapper<>();
|
||||
w.eq(EmrRevision::getEmrId, revision.getEmrId())
|
||||
.orderByDesc(EmrRevision::getRevisionNumber)
|
||||
.last("LIMIT 1");
|
||||
EmrRevision last = revisionService.getOne(w);
|
||||
revision.setRevisionNumber(last == null ? 1 : last.getRevisionNumber() + 1);
|
||||
revision.setCreateTime(new Date());
|
||||
revisionService.save(revision);
|
||||
return R.ok(revision);
|
||||
return R.ok(emrRevisionAppService.getRevisionDetail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/compare")
|
||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||
@Operation(summary = "对比两个修订版本")
|
||||
public R<?> compareRevisions(
|
||||
@RequestParam("revisionId1") Long id1,
|
||||
@RequestParam("revisionId2") Long id2) {
|
||||
|
||||
@@ -93,3 +93,11 @@ export function saveAnesSummary(data) {
|
||||
export function getAnesSummary(recordId) {
|
||||
return request({ url: '/api/v1/anesthesia/summary/' + recordId, method: 'get' })
|
||||
}
|
||||
|
||||
export function recordPostopFollowup(data) {
|
||||
return request({ url: '/api/v1/anesthesia/postop-followup', method: 'post', data })
|
||||
}
|
||||
|
||||
export function getPostopFollowups(encounterId) {
|
||||
return request({ url: '/api/v1/anesthesia/postop-followup/' + encounterId, method: 'get' })
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import request from "@/utils/request"
|
||||
export function getTimelinessByEncounter(encounterId) { return request({ url: "/emr-revision/timeliness/" + encounterId, method: "get" }) }
|
||||
export function getTimelinessStatistics(params) { return request({ url: "/emr-revision/statistics", method: "get", params }) }
|
||||
export function getPendingEmrCount(params) { return request({ url: "/emr-archive/pending-count", method: "get", params }) }
|
||||
|
||||
// 查询超期病历列表
|
||||
export function getOverdueList(params) { return request({ url: "/emr-archive/overdue/list", method: "get", params }) }
|
||||
|
||||
export function recordEmrRevision(data) { return request({ url: "/emr/revision/record", method: "post", data }) }
|
||||
export function getEmrRevisionList(emrId) { return request({ url: "/emr/revision/list/" + emrId, method: "get" }) }
|
||||
export function getEmrRevisionPage(params) { return request({ url: "/emr/revision/page", method: "get", params }) }
|
||||
export function getEmrRevisionDetail(id) { return request({ url: "/emr/revision/" + id, method: "get" }) }
|
||||
export function compareEmrRevisions(id1, id2) { return request({ url: "/emr/revision/compare", method: "get", params: { revisionId1: id1, revisionId2: id2 } }) }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function createRevision(data) { return request({ url: '/api/v1/emr/revision', method: 'post', data }) }
|
||||
export function getRevisionHistory(emrId) { return request({ url: '/api/v1/emr/revision/' + emrId, method: 'get' }) }
|
||||
export function executeCompletenessCheck(emrId) { return request({ url: '/api/v1/emr/completeness-check/' + emrId, method: 'post' }) }
|
||||
@@ -7,3 +8,9 @@ export function getTimelinessByEncounter(encounterId) { return request({ url: '/
|
||||
export function getOverdueList() { return request({ url: '/api/v1/emr/timeliness/overdue', method: 'get' }) }
|
||||
export function getTimelinessStatistics(params) { return request({ url: '/api/v1/emr/timeliness/statistics', method: 'get', params }) }
|
||||
export function checkTimeliness(data) { return request({ url: '/api/v1/emr/timeliness/check', method: 'post', data }) }
|
||||
|
||||
export function recordEmrRevision(data) { return request({ url: '/emr/revision/record', method: 'post', data }) }
|
||||
export function getEmrRevisionList(emrId) { return request({ url: '/emr/revision/list/' + emrId, method: 'get' }) }
|
||||
export function getEmrRevisionPage(params) { return request({ url: '/emr/revision/page', method: 'get', params }) }
|
||||
export function getEmrRevisionDetail(id) { return request({ url: '/emr/revision/' + id, method: 'get' }) }
|
||||
export function compareEmrRevisions(id1, id2) { return request({ url: '/emr/revision/compare', method: 'get', params: { revisionId1: id1, revisionId2: id2 } }) }
|
||||
|
||||
277
healthlink-his-ui/src/views/inpatientDoctor/EmrRevisionTrack.vue
Normal file
277
healthlink-his-ui/src/views/inpatientDoctor/EmrRevisionTrack.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<div class="emr-revision-track">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>修改留痕记录</span>
|
||||
<div>
|
||||
<el-button v-if="compareIds.length === 2" type="primary" size="small" @click="handleCompare">对比选中</el-button>
|
||||
<el-button type="info" size="small" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-timeline v-if="revisions.length > 0">
|
||||
<el-timeline-item
|
||||
v-for="item in revisions"
|
||||
:key="item.id"
|
||||
:timestamp="item.createTime"
|
||||
placement="top"
|
||||
:type="getTimelineType(item.operationType)"
|
||||
size="large"
|
||||
>
|
||||
<el-card shadow="never" class="revision-card">
|
||||
<div class="revision-header">
|
||||
<div class="revision-meta">
|
||||
<el-tag size="small" :type="getOpTagType(item.operationType)">{{ getOpLabel(item.operationType) }}</el-tag>
|
||||
<span class="revision-number">版本 #{{ item.revisionNumber }}</span>
|
||||
<span class="revision-operator">{{ item.operatorName }}</span>
|
||||
</div>
|
||||
<div class="revision-actions">
|
||||
<el-checkbox
|
||||
:model-value="compareIds.includes(item.id)"
|
||||
:disabled="!compareIds.includes(item.id) && compareIds.length >= 2"
|
||||
@change="(val) => toggleCompare(item.id, val)"
|
||||
/>
|
||||
<el-button link type="primary" size="small" @click="handleViewDetail(item)">查看详情</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.diffContent" class="revision-diff">
|
||||
<div class="diff-label">修改内容:</div>
|
||||
<pre class="diff-content">{{ item.diffContent }}</pre>
|
||||
</div>
|
||||
<div v-if="item.snapshotContent" class="revision-snapshot">
|
||||
<div class="diff-label">快照内容:</div>
|
||||
<pre class="diff-content">{{ truncateContent(item.snapshotContent) }}</pre>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<el-empty v-else description="暂无修改记录" />
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="detailVisible" title="修订详情" width="700px" destroy-on-close top="5vh">
|
||||
<div v-if="currentRevision" class="detail-content">
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="版本号">{{ currentRevision.revisionNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作类型">
|
||||
<el-tag size="small" :type="getOpTagType(currentRevision.operationType)">{{ getOpLabel(currentRevision.operationType) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ currentRevision.operatorName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作时间">{{ currentRevision.createTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="病历ID">{{ currentRevision.emrId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="就诊ID">{{ currentRevision.encounterId }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-if="currentRevision.diffContent" style="margin-top: 16px">
|
||||
<div class="diff-label">修改内容:</div>
|
||||
<pre class="detail-pre">{{ currentRevision.diffContent }}</pre>
|
||||
</div>
|
||||
<div v-if="currentRevision.snapshotContent" style="margin-top: 16px">
|
||||
<div class="diff-label">快照内容:</div>
|
||||
<pre class="detail-pre">{{ currentRevision.snapshotContent }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="compareVisible" title="修订对比" width="900px" destroy-on-close top="5vh">
|
||||
<div v-if="compareData" class="compare-content">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>版本 #{{ compareData.revision1?.revisionNumber }} — {{ compareData.revision1?.operatorName }}</span>
|
||||
</template>
|
||||
<pre class="compare-pre">{{ compareData.revision1?.snapshotContent || '无快照' }}</pre>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>版本 #{{ compareData.revision2?.revisionNumber }} — {{ compareData.revision2?.operatorName }}</span>
|
||||
</template>
|
||||
<pre class="compare-pre">{{ compareData.revision2?.snapshotContent || '无快照' }}</pre>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="compareVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getEmrRevisionList, getEmrRevisionDetail, compareEmrRevisions } from '@/api/emr'
|
||||
|
||||
const props = defineProps({
|
||||
emrId: { type: [Number, String], default: null }
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const revisions = ref([])
|
||||
const compareIds = ref([])
|
||||
const detailVisible = ref(false)
|
||||
const compareVisible = ref(false)
|
||||
const currentRevision = ref(null)
|
||||
const compareData = ref(null)
|
||||
|
||||
const OP_LABEL_MAP = { CREATE: '创建', EDIT: '编辑', APPROVE: '审批', SIGN: '签名' }
|
||||
const OP_TAG_MAP = { CREATE: 'success', EDIT: '', APPROVE: 'warning', SIGN: 'info' }
|
||||
const TIMELINE_MAP = { CREATE: 'primary', EDIT: 'success', APPROVE: 'warning', SIGN: 'info' }
|
||||
|
||||
function getOpLabel(type) { return OP_LABEL_MAP[type] || type }
|
||||
function getOpTagType(type) { return OP_TAG_MAP[type] || '' }
|
||||
function getTimelineType(type) { return TIMELINE_MAP[type] || '' }
|
||||
|
||||
function truncateContent(content) {
|
||||
if (!content) return ''
|
||||
return content.length > 200 ? content.substring(0, 200) + '...' : content
|
||||
}
|
||||
|
||||
function loadRevisions() {
|
||||
if (!props.emrId) return
|
||||
loading.value = true
|
||||
getEmrRevisionList(props.emrId).then(res => {
|
||||
revisions.value = res.data || []
|
||||
}).catch(() => {
|
||||
ElMessage.error('查询修改记录失败')
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
compareIds.value = []
|
||||
loadRevisions()
|
||||
}
|
||||
|
||||
function toggleCompare(id, checked) {
|
||||
if (checked) {
|
||||
compareIds.value.push(id)
|
||||
} else {
|
||||
compareIds.value = compareIds.value.filter(i => i !== id)
|
||||
}
|
||||
}
|
||||
|
||||
function handleViewDetail(item) {
|
||||
loading.value = true
|
||||
getEmrRevisionDetail(item.id).then(res => {
|
||||
currentRevision.value = res.data
|
||||
detailVisible.value = true
|
||||
}).catch(() => {
|
||||
ElMessage.error('查询详情失败')
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleCompare() {
|
||||
if (compareIds.value.length !== 2) {
|
||||
ElMessage.warning('请选择两个版本进行对比')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
compareEmrRevisions(compareIds.value[0], compareIds.value[1]).then(res => {
|
||||
compareData.value = res.data
|
||||
compareVisible.value = true
|
||||
}).catch(() => {
|
||||
ElMessage.error('对比查询失败')
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.emrId, () => { loadRevisions() })
|
||||
|
||||
onMounted(() => { loadRevisions() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.emr-revision-track {
|
||||
padding: 12px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.revision-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.revision-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.revision-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.revision-number {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.revision-operator {
|
||||
color: #606266;
|
||||
}
|
||||
.revision-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.diff-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.diff-content {
|
||||
background: #f5f7fa;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.detail-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.detail-pre {
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
}
|
||||
.compare-content {
|
||||
max-height: 65vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.compare-pre {
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user