Parcourir la source

添加电子病历socket

DESKTOP-0GD05B0\Administrator il y a 2 ans
Parent
commit
7306b9f8c8

+ 9 - 0
src/api/public-api.js

@@ -180,3 +180,12 @@ export function getOperationGuide(folderName) {
     })
 }
 
+export function getUserInfoByCode(code) {
+    return request({
+        url: '/publicApi/getUserInfoByCode',
+        method: 'get',
+        params: {code}
+    })
+}
+
+

+ 41 - 0
src/api/zhu-yuan-yi-sheng/emr-socket.js

@@ -0,0 +1,41 @@
+import request from "@/utils/request";
+
+export function getRoomPeople(sid) {
+    return request({
+        url: '/emrSocket/getRoomPeople',
+        method: 'get',
+        params: {sid}
+    })
+}
+
+export function sendAMessage(sid, message) {
+    return request({
+        url: '/emrSocket/sendAMessage',
+        method: 'get',
+        params: {sid, message}
+    })
+}
+
+export function getChatHistoryBySid(sid) {
+    return request({
+        url: '/emrSocket/getChatHistoryBySid',
+        method: 'get',
+        params: {sid}
+    })
+}
+
+export function lockMedicalRecords(id) {
+    return request({
+        url: '/emrSocket/lockMedicalRecords',
+        method: 'get',
+        params: {id}
+    })
+}
+
+export function clearTheCurrentMedicalRecordUser(id) {
+    return request({
+        url: '/emrSocket/clearTheCurrentMedicalRecordUser',
+        method: 'get',
+        params: {id}
+    })
+}

+ 1 - 0
src/components/zhu-yuan-yi-sheng/emr/auxiliary-tools/EmrTest.vue

@@ -287,6 +287,7 @@ onMounted(async () => {
   dateRange.value.push(patInfo.value.admissDate)
   dateRange.value.push(await getServerDateApi())
   await query()
+  // todo 要查询患者危急值
 })
 
 </script>

+ 205 - 0
src/components/zhu-yuan-yi-sheng/emr/web-socket/EmrChatBox.vue

@@ -0,0 +1,205 @@
+<template>
+  <el-dialog v-model="dialog" title="病历人数" width="70%"
+             @closed="emits('closed')">
+    <div class="chat_room_dialog">
+      <div class="left">
+        <el-table :data="props.userList" :height="getWindowSize.h / 1.5">
+          <el-table-column prop="name" label="姓名">
+            <template #default="{row}">
+              <span v-if="row.code === userData.code">本人</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="deptName" label="科室"/>
+        </el-table>
+      </div>
+      <div class="right">
+        <el-scrollbar :height="getWindowSize.h / 1.8 " ref="scrollbarRef">
+          <div ref="innerRef">
+            <div v-for="item in chatHistory" class="list">
+              <div class="chat_right"
+                   v-if="whetherInPerson(item.code)">
+                <div style="display: flex; ">
+                  <div style="font-size: 12px;margin-right: 10px">
+                    <div style="text-align: right">
+                      {{ item.date }} {{ item.deptName }} {{ item.name }}
+                    </div>
+                    <div class="message_str" style="float: right;">
+                      {{ item.message }}
+                    </div>
+                  </div>
+
+                  <div>
+                    <el-avatar shape="square" :src="item.avatar" :size="30"/>
+                  </div>
+                </div>
+              </div>
+
+              <div class="chat_left" v-else>
+                <div style="display: flex;">
+                  <div>
+                    <el-avatar shape="square" :src="item.avatar" :size="30"/>
+                  </div>
+
+                  <div style="font-size: 12px;margin-left: 10px">
+                    <div>{{ item.name }} {{ item.deptName }} {{ item.date }}</div>
+                    <div class="message_str" :class="judgmentQMy(item.message) ? 'specify_me' : '' ">
+                      {{ item.message }}
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-scrollbar>
+
+        <div>
+          <el-input type="textarea"
+                    :rows="3"
+                    v-model="messageStr"
+                    @keydown.enter.stop="clickSend"/>
+
+          <div style="float: right">
+            <el-button @click="modifyTheCurrentMedicalRecord">修改当前病历</el-button>
+            <el-button @click="clickSend">发送</el-button>
+          </div>
+
+        </div>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup name='EmrChatBox'>
+import {getWindowSize} from "@/utils/window-size";
+import store from "@/store";
+import {getChatHistoryBySid, sendAMessage} from "@/api/zhu-yuan-yi-sheng/emr-socket";
+import {stringIsBlank} from "@/utils/blank-utils";
+import {xcMessage} from "@/utils/xiaochan-element-plus";
+
+let userData = store.state.user.info
+
+const chatHistory = ref([])
+const props = defineProps({
+  userList: {
+    type: Array
+  },
+  sid: {
+    type: String
+  },
+  currentEditorUser: {
+    type: Object,
+    default: null
+  }
+})
+
+const emits = defineEmits(['closed'])
+
+const dialog = ref(true)
+const messageStr = ref('')
+const scrollbarRef = ref('')
+const innerRef = ref('')
+
+const clickSend = async () => {
+  if (stringIsBlank(messageStr.value)) {
+    return xcMessage.error('不能发送空白消息。')
+  }
+  await sendAMessage(props.sid, messageStr.value)
+  messageStr.value = ''
+  await queryJump()
+}
+
+const modifyTheCurrentMedicalRecord = async () => {
+  if (props.currentEditorUser === null) {
+    return xcMessage.error('当前没有人正在编辑');
+  }
+  if (stringIsBlank(props.currentEditorUser.name)) {
+    return xcMessage.error('当前没有人正在编辑');
+  }
+  await sendAMessage(props.sid, '@' + props.currentEditorUser.name + '    申请修改病历,请尽快保存,后离开当前病历【系统自动发送】');
+  messageStr.value = ''
+  await queryJump()
+}
+
+const queryJump = async () => {
+  chatHistory.value = await getChatHistoryBySid(props.sid)
+  await nextTick()
+  scrollbarRef.value.setScrollTop(innerRef.value.scrollHeight)
+}
+
+const whetherInPerson = (code) => {
+  return userData.code === code
+}
+
+const judgmentQMy = (val) => {
+  return val.startsWith('@' + userData.name)
+}
+
+onMounted(async () => {
+  await nextTick()
+  await queryJump()
+})
+
+defineExpose({
+  queryJump
+})
+
+</script>
+
+<style scoped lang="scss">
+.chat_room_dialog {
+  display: flex;
+  width: 100%;
+
+  .left {
+    width: 200px;
+  }
+
+  .right {
+    flex: 1;
+    height: max-content;
+  }
+}
+
+.list {
+  width: 100%;
+
+  .specify_me {
+    background-color: red;
+    color: white;
+  }
+
+  .chat_right {
+    display: flex;
+    justify-content: flex-end;
+    padding: 5px 10px;
+  }
+
+  .chat_left {
+    display: flex;
+    padding: 5px 10px;
+  }
+
+  .message_str {
+    border: 1px solid #000;
+    padding: 5px;
+    width: max-content;
+    border-radius: 5px;
+  }
+}
+
+.chat_history_div {
+  height: max-content;
+  display: flex;
+
+  .user_info {
+    padding: 0 10px;
+  }
+
+  .message {
+    border: 1px solid #000;
+    padding: 5px;
+    width: max-content;
+    border-radius: 10px;
+  }
+}
+</style>

+ 188 - 0
src/components/zhu-yuan-yi-sheng/emr/web-socket/EmrWebSocket.vue

@@ -0,0 +1,188 @@
+<template>
+  <div class="chat_room"
+       ref="roomRef"
+       :style="style"
+       @click="dialog = true">
+    <div class="count">
+      {{ userSize }}
+    </div>
+  </div>
+
+  <emr-chat-box
+      v-if="dialog"
+      ref="dialogRef"
+      :current-editor-user="props.currentEditorUser"
+      @closed="dialog = false"
+      :user-list="userList" :sid="sid"/>
+
+
+  <el-dialog v-model="errDialog" title="正在尝试重新连接"
+             :show-close="false"
+             :close-on-press-escape="false"
+             :close-on-click-modal="false">
+    <div v-loading="errDialog" style="width: 100%;height: 400px"/>
+
+  </el-dialog>
+
+</template>
+
+<script setup name='EmrWebSocket' lang="ts">
+import {stringIsBlank} from "@/utils/blank-utils";
+import store from "@/store";
+import {ref, defineProps, onMounted, nextTick, computed} from 'vue'
+import {useDraggable} from '@vueuse/core'
+import {xcMessage} from '@/utils/xiaochan-element-plus'
+import {getRoomPeople} from '@/api/zhu-yuan-yi-sheng/emr-socket'
+import {ElNotification} from "element-plus";
+import EmrChatBox from "@/components/zhu-yuan-yi-sheng/emr/web-socket/EmrChatBox.vue";
+import {$ref} from "vue/macros";
+
+const props = defineProps({
+  patInfo: {
+    type: Object,
+    default: null
+  },
+  currentEditorUser: {
+    type: Object,
+    default: null
+  }
+})
+const roomRef = ref<HTMLElement | null>(null)
+
+const {x, y, style} = useDraggable(roomRef, {
+  initialValue: {x: 40, y: 40},
+})
+
+const dialog = ref<boolean>(false)
+const errDialog = ref<any>(true)
+const userMap = ref<any>({})
+const dialogRef = ref(null)
+
+const socketUrl = import.meta.env.VITE_SOCKET_URL
+
+let webSocket = null
+let userData = store.state.user.info
+let sid = $ref(null)
+
+const onmessageFunc = {
+  "connect": async (data) => {
+    await queryTheNumberOfPeopleInTheRoom()
+    xcMessage.success('连接成功')
+  },
+  "connectToJoin": async (val) => {
+    await queryTheNumberOfPeopleInTheRoom()
+    let userInfo = userMap.value[val]
+    ElNotification({
+      type: 'success',
+      title: '有新的连接加入',
+      dangerouslyUseHTMLString: true,
+      message: `<span>姓名:${userInfo.name}</span>
+                <br>
+                <span>科室:${userInfo.deptName}</span>`
+    })
+    console.log("连接加入 %s, 数据 %o", val, userMap.value)
+  },
+  "exitEmrEditor": async (val) => {
+    let userInfo = userMap.value[val]
+    ElNotification({
+      type: 'error',
+      title: '有连接退出',
+      dangerouslyUseHTMLString: true,
+      message: `<span>姓名:${userInfo.name}</span>
+                <br>
+                <span>科室:${userInfo.deptName}</span>`
+    })
+    console.log("连接退出 %s,数据 %o", val, userMap.value)
+    await queryTheNumberOfPeopleInTheRoom()
+  },
+  "message": async (val) => {
+    if (dialog.value) {
+      dialogRef.value.queryJump();
+    }
+    dialog.value = true
+    console.log("接受消息 %s", val)
+  }
+}
+
+const userSize = computed(() => {
+  return Object.keys(userMap.value).length
+})
+
+const userList = computed(() => {
+  return Object.values(userMap.value)
+})
+
+const queryTheNumberOfPeopleInTheRoom = async () => {
+  userMap.value = await getRoomPeople(sid) as any
+}
+
+function initWebSocket(patNo, times) {
+  if (stringIsBlank(patNo)) {
+    return null
+  }
+
+  if ('WebSocket' in window) {
+    sid = 'emr-' + patNo + '-' + times;
+    const url = socketUrl + sid + '?userCode=' + userData.code;
+    webSocket = new WebSocket(url);
+  } else {
+    alert('该浏览器不支持websocket!');
+    webSocket = 'unsupport';
+  }
+
+  webSocket.onopen = async () => {
+    errDialog.value = false
+  }
+
+
+  webSocket.onmessage = async function (e) {
+    let data = JSON.parse(e.data)
+    for (let key in data) {
+      onmessageFunc[key](data[key])
+    }
+
+  }
+
+  webSocket.onclose = () => {
+    errDialog.value = true
+    initWebSocket(props.patInfo.inpatientNo, props.patInfo.admissTimes)
+  }
+
+}
+
+onMounted(async () => {
+  await nextTick()
+  initWebSocket(props.patInfo.inpatientNo, props.patInfo.admissTimes)
+})
+
+</script>
+
+<style scoped lang="scss">
+.chat_room {
+  position: fixed;
+  width: 40px;
+  height: 40px;
+  color: white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+  background-color: rgb(0 0 0 / 63%);
+  user-select: none;
+  border-radius: 9px;
+  cursor: all-scroll;
+
+  .count {
+    width: 25px;
+    text-align: center;
+    line-height: 25px;
+    background-color: rgba(255, 255, 255, 0.48);
+    border-radius: 12px;
+
+    position: relative;
+
+  }
+
+}
+
+</style>

+ 95 - 32
src/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/EmrMain.vue

@@ -270,16 +270,20 @@
               创建:{{ createName }}
               <br>
               当前:{{ userData.name }}
+              <br>
+              现编辑:{{ currentEditorUser?.name }}
             </div>
 
             <div>
               等级:{{ createLevel }}
               <br>
               等级:{{ doctorLevel }}
+              <br>
+              科室:{{ currentEditorUser?.deptName }}
             </div>
 
             <div class="edit_mode">
-              模式:{{ readonlyPattern() ? '只读' : '编辑' }}
+              模式:{{ editorMod ? '只读' : '编辑' }}
             </div>
 
           </div>
@@ -316,6 +320,10 @@
       </template>
     </xc-dialog-v2>
 
+    <emr-web-socket :pat-info="props.huanZheXinXi"
+                    :current-editor-user="currentEditorUser"
+                    ref="emrSocket"/>
+
   </el-container>
 </template>
 
@@ -339,7 +347,6 @@ import {
   submitMedicalRecord
 } from "@/api/zhu-yuan-yi-sheng/emr-patient";
 import {useDocumentVisibility} from "@vueuse/core";
-import EmrAssistant from "@/components/zhu-yuan-yi-sheng/emr/EmrAssistant.vue";
 import XcDialogV2 from "@/components/xiao-chan/dialog/XcDialogV2.vue";
 import {getWardsApi} from "@/api/login";
 import {xcMessage} from "@/utils/xiaochan-element-plus";
@@ -347,9 +354,10 @@ import EmrUnorderedList from "@/components/zhu-yuan-yi-sheng/emr/EmrUnorderedLis
 import HistoricalEmr from "@/components/zhu-yuan-yi-sheng/emr/HistoricalEmr.vue";
 import EmrPopup from "@/components/zhu-yuan-yi-sheng/emr/EmrPopup.vue";
 import EmrAuxiliaryTools from "@/components/zhu-yuan-yi-sheng/emr/auxiliary-tools/EmrAuxiliaryTools.vue";
-import {isDev} from "@/utils/public";
 import sleep from "@/utils/sleep";
-import {initWebSocket} from "@/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/emr-socket";
+import EmrWebSocket from "@/components/zhu-yuan-yi-sheng/emr/web-socket/EmrWebSocket.vue";
+import {stringIsBlank, stringNotBlank} from "@/utils/blank-utils";
+import {clearTheCurrentMedicalRecordUser, lockMedicalRecords} from "@/api/zhu-yuan-yi-sheng/emr-socket";
 
 const props = defineProps({
   huanZheXinXi: {
@@ -368,6 +376,7 @@ const props = defineProps({
 
 const currentEmr = ref(null)
 const emrRef = ref(null)
+const emrSocket = ref(null)
 
 let recoveryDialog = $ref(false)
 let categoryCode = $ref('')
@@ -403,6 +412,9 @@ let wardList = []
 let createId = null;
 // 获取提取到的数据。
 let extractData = $ref({})
+let editorMod = $ref(false)
+
+let currentEditorUser = $ref(null)
 
 let foldBothSides = $ref({
   isLeft: true,
@@ -463,6 +475,8 @@ const emrEvent = {
     isRevisionMode()
     // 判断是否只读
     readonlyPattern()
+    // 每一个病历只能一个人编辑
+    editJudgment()
   },
 
   'contentchange': (event) => {
@@ -536,11 +550,54 @@ let saveDialog = $ref({
   }
 })
 
+const editJudgment = async () => {
+  if (stringIsBlank(documentId)) {
+    return
+  }
+  if (categoryCode.includes('shoucibingchengjilu')) {
+    currentEditorUser = await lockMedicalRecords(documentId)
+    if (currentEditorUser === null) {
+      xcMessage.success('可以编辑')
+    } else {
+      ElMessageBox.alert(`【${currentEditorUser.name}】正在编辑当前病历,请医生先退出病历后才能编辑。`, '提示', {
+        type: 'error',
+        dangerouslyUseHTMLString: true,
+      })
+      editor.setEditorMode('readonly')
+    }
+  } else {
+    if (readonlyPattern()) {
+      return;
+    }
+    currentEditorUser = await lockMedicalRecords(documentId)
+    if (currentEditorUser === null) {
+      xcMessage.success('可以编辑')
+    } else {
+      ElMessageBox.alert(`【${currentEditorUser.name}】正在编辑当前病历,请医生先退出病历后才能编辑。`, '提示', {
+        type: 'error',
+        dangerouslyUseHTMLString: true,
+      })
+      editor.setEditorMode('readonly')
+    }
+  }
+  readonlyPattern()
+}
+
+watch(() => documentId, async (value, oldValue) => {
+  await clearMedicalRecordEditorUser(oldValue)
+}, {deep: true})
+
+const clearMedicalRecordEditorUser = async (id) => {
+  if (stringNotBlank(id)) {
+    await clearTheCurrentMedicalRecordUser(id)
+  }
+}
+
 // 点击保存病历
 const clickSaveData = async () => {
   // 只读模式无法保存数据
   if (readonlyPattern()) {
-    BizException(ExceptionEnum.LOGICAL_ERROR, "当前为只读模式,无法保存病历。");
+    BizException(ExceptionEnum.MESSAGE_ERROR, "当前为只读模式,无法保存病历。");
   }
   if (editor === null) return
   waitForLoadingToComplete()
@@ -677,6 +734,9 @@ const whetherThereIsAMedicalRecordId = () => {
 }
 
 const checkEmrChange = (cb) => {
+  if (readonlyPattern()) {
+    isEditorChange = false
+  }
   if (isEditorChange) {
     ElMessageBox.confirm("您改变了数据是否要保存,不保存可能会丢失数据。", '提示', {
       type: "warning",
@@ -700,6 +760,7 @@ const checkEmrChange = (cb) => {
 let createName = $ref()
 let createLevel = $ref(3)
 const nodeClick = (val, jumpOrNot, templateType) => {
+
   createName = val.createName
   createId = val.createId
   if (jumpOrNot) {
@@ -713,9 +774,11 @@ const nodeClick = (val, jumpOrNot, templateType) => {
       }
     }
   } else {
+
     checkEmrChange(() => {
       updateCaseHistoryUrl(val);
-    })
+    });
+
   }
 }
 
@@ -761,7 +824,8 @@ const positioningTime = (val, code) => {
 const monitorPageRefresh = (event) => {
   if (isEditorChange) {
     event.preventDefault();
-    return event.returnValue = "是否确实要退出?还有未保存的数据!";
+    clearMedicalRecordEditorUser(documentId)
+    event.returnValue = "是否确实要退出?还有未保存的数据!";
   }
 }
 
@@ -781,27 +845,28 @@ const clickSnippet = ({content, styles, code}) => {
       delEmrCopy(temp)
     }
   }
-  if (!readonlyPattern()) {
-    let root = editor.model.document.getRoot()
-    let lastView = root.getChild(root.childCount - 1).view
-    lastView.setAttribute('readonly', false)
-    //移动到文档结尾
-    editor.setCursor('DOCUMENT_END');
-    editor.scrollToCursor()
-    let insertContent = {
-      // 内容
-      value: content,
-      // 样式
-      styles: styles,
-      isFragment: true,
-      // 就是在这里填充的值
-      defaultData: data
-    }
-    editor.execute("insertContents", insertContent);
-    lastView.setAttribute('readonly', true)
-    editor.setCursor('DOCUMENT_END');
-    editor.scrollToCursor()
+  if (readonlyPattern()) {
+    return
+  }
+  let root = editor.model.document.getRoot()
+  let lastView = root.getChild(root.childCount - 1).view
+  lastView.setAttribute('readonly', false)
+  // 移动到文档结尾
+  editor.setCursor('DOCUMENT_END');
+  editor.scrollToCursor()
+  let insertContent = {
+    // 内容
+    value: content,
+    // 样式
+    styles: styles,
+    isFragment: true,
+    // 就是在这里填充的值
+    defaultData: data
   }
+  editor.execute("insertContents", insertContent);
+  lastView.setAttribute('readonly', true)
+  editor.setCursor('DOCUMENT_END');
+  editor.scrollToCursor()
 }
 
 // 用户信息
@@ -1027,6 +1092,7 @@ const drgIntelligentGrouping = () => {
 
 // true 是只读 false 不是只读
 const readonlyPattern = () => {
+  editorMod = (editor !== null && editor.getEditorMode() === 'readonly')
   return editor !== null && editor.getEditorMode() === 'readonly';
 }
 
@@ -1294,12 +1360,8 @@ const autoSaveFunc = () => {
   }
 }
 
-onMounted(async () => {
-  if (props.huanZheXinXi.inpatientNo) {
-    initWebSocket(props.huanZheXinXi.inpatientNo + '-' + userData.code);
-  }
-
 
+onMounted(async () => {
   autoSave = store.state.app.emrAutosave;
   autoSaveChange()
   extractData = await getExtractDataElement(props.huanZheXinXi.inpatientNo, props.huanZheXinXi.admissTimes)
@@ -1334,6 +1396,7 @@ onDeactivated(() => {
 onBeforeRouteLeave((to, from, next) => {
   checkEmrChange(() => {
     next();
+    clearMedicalRecordEditorUser(documentId)
   })
 })
 

+ 0 - 1
src/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/emr-init.js

@@ -1,5 +1,4 @@
 import store from '@/store'
-import router from "@/router";
 import {deletePatientEmrByDocumentId, insertEmrData} from "@/api/zhu-yuan-yi-sheng/emr-patient";
 import {stringNotBlank} from "@/utils/blank-utils";
 import {xcMessage} from "@/utils/xiaochan-element-plus";

+ 0 - 21
src/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/emr-socket.js

@@ -1,21 +0,0 @@
-const socketUrl = import.meta.env.VITE_SOCKET_URL
-
-let webSocket = null
-
-export function initWebSocket(sid) {
-    if ('WebSocket' in window) {
-        if (webSocket === null) {
-            const url = socketUrl + sid + '-Emr'
-            webSocket = new WebSocket(url)
-        }
-    } else {
-        alert('该浏览器不支持websocket!')
-        webSocket = 'unsupport'
-    }
-
-    webSocket.onopen = function () {
-        console.log('电子病历连接成功。')
-    }
-
-
-}

+ 5 - 0
src/views/hospitalization/zhu-yuan-yi-sheng/yi-zhu-lu-ru/TemplateMaintenance.vue

@@ -44,6 +44,11 @@
           </xc-combo-grid>
         </el-form-item>
       </el-col>
+      <el-col :span="span" v-if="yiZhuData.orderCode === '06054'">
+        <el-form-item class="bi_tian" label="请输入处置医嘱名称:" prop="orderName">
+          <el-input v-model="yiZhuData.orderName"/>
+        </el-form-item>
+      </el-col>
       <el-col :span="span">
         <el-form-item label="规格:" prop="orderName">
           <div style="border-bottom: 1px solid #000; height: 29px">