Przeglądaj źródła

门诊电子病历,cymessagebox

xiaochan 1 rok temu
rodzic
commit
3080bdbbc3

+ 109 - 0
src/components/xiao-chan/cy-dialog/cy-dialog.vue

@@ -0,0 +1,109 @@
+<script setup lang="ts">
+import {useCompRef} from "@/utils/useCompRef";
+import {ElDialog, ElIcon} from "element-plus";
+import {useVModels} from "@vueuse/core";
+// @ts-ignore
+import {Close} from '@element-plus/icons-vue'
+
+const props = withDefaults(
+    defineProps<{
+      modelValue: boolean,
+      title?: string
+    }>(),
+    {}
+)
+
+const emit = defineEmits(['update:modelValue'])
+
+const {modelValue} = useVModels(props, emit)
+
+const dialogRef = useCompRef(ElDialog)
+
+</script>
+
+<template>
+  <el-dialog v-model="modelValue"
+             :title="props.title"
+             class="cy-root"
+             ref="dialogRef"
+             :show-close="false"
+             modal-class="cy_dialog-modal"
+             draggable>
+    <template #header="{close, titleId, titleClass}">
+      <span :class="titleClass">
+        {{ props.title }}
+      </span>
+      <div class="cy-message-box_close" @click.stop="close">
+        <ElIcon>
+          <Close/>
+        </ElIcon>
+      </div>
+    </template>
+    <slot/>
+  </el-dialog>
+</template>
+
+<style lang="scss">
+
+
+.cy_dialog-modal {
+  --el-overlay-color-lighter: rgba(0, 0, 0, .2);
+
+  .dialog-fade-enter-active {
+    animation: anim-open .3s ease !important;
+  }
+
+  @keyframes anim-open {
+    0% {
+      transform: scale(.8);
+      background-color: transparent;
+    }
+    80% {
+      transform: scale(1.1);
+      background-color: transparent;
+    }
+    100% {
+      transform: scale(1);
+    }
+  }
+}
+
+.cy-root {
+  border-radius: 20px;
+
+  header {
+    border-radius: 20px;
+    border: 0;
+    background-color: white;
+    position: relative;
+    text-align: center;
+    padding: 10px 16px;
+
+
+    .cy-message-box_close {
+      position: absolute;
+      top: -6px;
+      right: -6px;
+      border: 0;
+      height: 34px;
+      width: 34px;
+      box-sizing: border-box;
+      background: inherit;
+      border-radius: 12px;
+      box-shadow: 0 5px 20px 0 rgba(0, 0, 0, .05);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 20px;
+      color: black;
+      cursor: pointer;
+
+      &:hover {
+        -webkit-box-shadow: 0 0 4px 0 rgba(0, 0, 0, .05);
+        box-shadow: 0 0 4px 0 rgba(0, 0, 0, .05);
+        transform: translate(-2px, 2px);
+      }
+    }
+  }
+}
+</style>

+ 70 - 59
src/components/xiao-chan/cy-message-box/cy-message-box.ts

@@ -1,16 +1,14 @@
 // @ts-ignore
 import messageBox from "./index.vue";
-import {h, render} from "vue";
+import {createVNode, render} from "vue";
 import {uuid} from "../../../utils/getUuid";
 
-const currentBox = new Map<string, HTMLDivElement>()
-
 interface CyMessageBox {
     message?: string;
     showCancel?: boolean,
     confirmClick?: (value: string) => void;
     cancel?: (val: string) => void;
-    type?: 'success' | 'error' | 'warning' | 'info',
+    type?: 'success' | 'error' | 'warning' | 'info' | 'delete',
     confirmButtonText?: string,
     cancelButtonText?: string,
     showInput?: boolean,
@@ -29,56 +27,22 @@ interface CyMessageBox {
     selectOption?: {
         value: string,
         label: string
-    }[]
-}
-
-export function getCyId(): string {
-    return 'cy-' + uuid(5)
+    }[],
+    closeOnPressEscape?: boolean;
+    closeOnClickModal?: boolean;
 }
 
-function closeById(id: string) {
-    if (currentBox.has(id)) {
-        currentBox.get(id)?.['close']()
-    }
-}
-
-function alert(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'warning', config?: CyMessageBox): Promise<string> {
-    if (config) {
-        config.showCancel = false
-    }
-    return renderFunc(message, type, config || {showCancel: false})
-}
-
-function confirm(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'warning', config?: CyMessageBox): Promise<string> {
-    return renderFunc(message, type, config)
-}
-
-function prompt(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'warning', config?: CyMessageBox) {
-    if (config) {
-        config.showInput = true
-    }
-    return renderFunc(message, type, config || {showInput: true})
-}
-
-
-function renderFunc(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'warning', config: CyMessageBox): Promise<string> {
-    function id() {
-        if (typeof config === 'undefined') {
-            return false
-        }
-        return typeof config.boxId !== 'undefined';
-    }
-
-    const withID = id()
-
-    if (withID && document.getElementById(config.boxId)) {
-        return Promise.reject('messageBox,id重复。')
-    }
+const currentBox: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>()
 
+const showMessage = (options: CyMessageBox): Promise<string> => {
     const div = document.createElement('div');
     div.style.setProperty('--cy-body-transform', 'translate(0px, 0px)')
     document.body.appendChild(div);
-    div.id = withID ? config.boxId : getCyId();
+    div.id = options.boxId || 'cy-' + uuid(5);
+
+    if (currentBox.has(div.id)) {
+        closeByHtml(currentBox.get(div.id))
+    }
 
     currentBox.set(div.id, div);
 
@@ -92,8 +56,6 @@ function renderFunc(message: string, type: 'success' | 'error' | 'warning' | 'in
 
     return new Promise((resolve, reject) => {
         const data: CyMessageBox = {
-            message,
-            inputRows: config && config.inputRows || 0,
             confirmClick: (val) => {
                 resolve(val)
                 closeBox()
@@ -102,22 +64,71 @@ function renderFunc(message: string, type: 'success' | 'error' | 'warning' | 'in
                 reject(val)
                 closeBox()
             },
-            type,
             setClose: (val: any) => {
                 div['close'] = val
             },
-        }
+        };
 
-        Object.assign(data, config)
-        const vNode = h(messageBox, data, () => null)
+        Object.assign(data, options)
+        const vNode = createVNode(messageBox, data as any, () => null)
         render(vNode, div)
     })
 }
 
-export const CyMessageBox = {
-    alert,
-    confirm,
-    prompt,
-    closeById
+function closeByHtml(html: HTMLDivElement) {
+    try {
+        html && html['close']()
+    } catch {
+
+    }
+}
+
+function MessageBox(options: CyMessageBox) {
+    return showMessage(options)
+}
+
+const MESSAGE_BOX_VARIANTS = ['alert', 'confirm', 'prompt']
+const MESSAGE_BOX_DEFAULT_OPTS: {
+    [key: string]: CyMessageBox
+} = {
+    alert: {showCancel: false, closeOnPressEscape: false, closeOnClickModal: false},
+    confirm: {showCancel: true},
+    prompt: {showInput: true}
+}
+
+MESSAGE_BOX_VARIANTS.forEach(item => {
+    MessageBox[item] = messageBoxFactory(item)
+})
+
+MessageBox.close = (id?: string) => {
+    if (id) {
+        if (currentBox.has(id)) {
+            closeByHtml(currentBox.get(id))
+        }
+    } else {
+        currentBox.forEach(item => {
+            closeByHtml(item)
+        })
+    }
+}
+
+function messageBoxFactory(boxType: typeof MESSAGE_BOX_VARIANTS[number]) {
+    return (options: CyMessageBox) => {
+        return MessageBox(Object.assign({...MESSAGE_BOX_DEFAULT_OPTS[boxType]}, options))
+    }
+}
+
+interface CyMessageBoxShortcutMethod {
+    (options: CyMessageBox): Promise<string>
+}
+
+interface IElMessageBox {
+    (options: CyMessageBox): Promise<string>,
+
+    alert: CyMessageBoxShortcutMethod;
+    confirm: CyMessageBoxShortcutMethod;
+    prompt: CyMessageBoxShortcutMethod;
+    close: (boxId?: string) => void
 }
 
+export default MessageBox as IElMessageBox

+ 6 - 0
src/components/xiao-chan/cy-message-box/index.ts

@@ -0,0 +1,6 @@
+import MessageBox from "./cy-message-box";
+import type {Plugin} from 'vue'
+
+type SFCWithInstall<T> = T & Plugin
+const _MessageBox = MessageBox as SFCWithInstall<typeof MessageBox>
+export const CyMessageBox = _MessageBox

+ 191 - 132
src/components/xiao-chan/cy-message-box/index.vue

@@ -8,11 +8,13 @@ import {
   Warning,
   SuccessFilled,
   InfoFilled,
-  CircleCloseFilled
+  CircleCloseFilled,
+  DeleteFilled
 } from '@element-plus/icons-vue'
-import {getCyId} from "@/components/xiao-chan/cy-message-box/cy-message-box";
 import {useCompRef} from "@/utils/useCompRef";
 import {useCyDraggable} from "@/utils/use-cy-draggable";
+import {uuid} from "@/utils/getUuid";
+import ElFocusTrap from "@/components/xiao-chan/focus-trap/src/focus-trap.vue";
 
 const props = withDefaults(defineProps<{
   message?: string;
@@ -38,8 +40,11 @@ const props = withDefaults(defineProps<{
   selectOption?: {
     value: string,
     label: string
-  }[]
+  }[],
+  closeOnPressEscape?: boolean;
+  closeOnClickModal?: boolean
 }>(), {
+  type: 'info',
   showCancel: true,
   confirmButtonText: '确认',
   cancelButtonText: '取消',
@@ -48,7 +53,11 @@ const props = withDefaults(defineProps<{
   dangerouslyUseHTMLString: false,
   draggable: true,
   inputDefaultValue: '',
-  selectOption: null
+  selectOption: null,
+  inputRows: 0,
+  inputMaxLength: 0,
+  closeOnPressEscape: true,
+  closeOnClickModal: true
 })
 
 const visible = ref(false)
@@ -56,11 +65,12 @@ const headerRef = ref<HTMLHeadElement>(null)
 const inputValue = ref(props.inputDefaultValue)
 const errorMsg = ref('')
 const inputRef = useCompRef(ElInput)
-const confirmRef = ref<HTMLButtonElement>()
-const bodyRef = ref<HTMLDivElement>()
-const mainRef = ref<HTMLDivElement>()
+const confirmRef = ref<HTMLElement>()
+const bodyRef = ref<HTMLElement>()
+const mainRef = ref<HTMLElement>()
+const focusStartRef = ref<HTMLElement>()
 const isSelectInput = ref<boolean>(props.selectOption !== null)
-const inputId = ref(getCyId())
+const inputId = ref('cy-' + uuid(5, 62))
 
 let closeMode = ''
 
@@ -96,11 +106,6 @@ function onAfterLeave() {
  */
 function onAfterEnter() {
   mainStyle.value.display = 'flex';
-  if (props.showInput && !isSelectInput.value) {
-    inputRef.value.focus();
-  } else {
-    confirmRef.value.focus()
-  }
 }
 
 function handleConfirm() {
@@ -112,6 +117,11 @@ function handleConfirm() {
   }
 }
 
+function verificationFailed(value: string) {
+  errorMsg.value = value
+  popupJitter()
+}
+
 function handlePrompt() {
   function next() {
     visible.value = false;
@@ -120,24 +130,32 @@ function handlePrompt() {
 
   if (!props.allowToBeEmpty) {
     if (inputValue.value === '') {
-      errorMsg.value = '不允许为空。'
+      verificationFailed('不允许为空。')
+      return
+    }
+  }
+
+  if (props.inputMaxLength !== 0) {
+    if (inputValue.value.length > props.inputMaxLength) {
+      verificationFailed(`最大长度不得超过${props.inputMaxLength}个字。`)
       return
     }
   }
 
+
   if (props.inputValidator) {
     const inputValidator = props.inputValidator(inputValue.value)
     if (typeof inputValidator === 'boolean') {
       if (inputValidator) {
         next()
       } else {
-        errorMsg.value = props.inputErrorMessage
+        verificationFailed(props.inputErrorMessage)
       }
     }
 
     if (typeof inputValidator === 'string') {
       if (inputValidator) {
-        errorMsg.value = inputValidator
+        verificationFailed(inputValidator)
       } else {
         next()
       }
@@ -148,38 +166,56 @@ function handlePrompt() {
   }
 }
 
-function handleClose(val) {
+function handleClose(val: 'close' | 'cancel') {
   bodyRef.value.style.transition = ''
   visible.value = false
   closeMode = val
 }
 
+const headerData = {
+  success: {
+    icon: h(SuccessFilled),
+    title: '成功',
+    color: '#67c23a'
+  },
+  warning: {
+    icon: h(Warning),
+    title: '警告',
+    color: '#e6a23c'
+  },
+  info: {
+    icon: h(InfoFilled),
+    title: '提示',
+    color: '#909399'
+  },
+  error: {
+    icon: h(CircleCloseFilled),
+    title: '错误',
+    color: '#f56c6c'
+  },
+  delete: {
+    icon: h(DeleteFilled),
+    title: '删除',
+    color: '#f56c6c'
+  }
+}
+
+function headerColor() {
+  return headerData[props.type].color
+}
 
 function headerIcon() {
-  switch (props.type) {
-    case "success":
-      return h(SuccessFilled, null, null)
-    case "warning":
-      return h(Warning, null, null)
-    case 'info':
-      return h(InfoFilled, null, null)
-    case 'error':
-      return h(CircleCloseFilled)
-  }
+  return headerData[props.type].icon
 }
 
 function titleRender() {
-  const title = {
-    'success': '成功',
-    'warning': '警告',
-    'info': '提示',
-    'error': '错误',
-  }
-  return h('span', {style: {marginLeft: '8px'}}, props.title || title[props.type])
+  return h('span', {style: {marginLeft: '8px'}}, props.title || headerData[props.type].title)
 }
 
-
-async function maskClick() {
+/**
+ * 弹窗抖动
+ */
+async function popupJitter() {
   const oldTransform = bodyRef.value.style.transform
   bodyRef.value.style.transform = oldTransform + ' scale(1.1)';
   await sleep(100)
@@ -191,18 +227,40 @@ const draggable = computed(() => props.draggable)
 useCyDraggable(bodyRef, headerRef, draggable,
     () => {
       bodyRef.value.style.transition = 'none'
+      inputRef.value.blur();
     }, () => {
       mainRef.value.parentElement.style.setProperty('--cy-body-transform', bodyRef.value.style.transform)
       bodyRef.value.style.transition = ''
     }
 )
 
+function onCloseRequested() {
+  if (props.closeOnPressEscape) {
+    handleClose('close')
+  } else {
+    popupJitter()
+  }
+}
+
+function maskClick() {
+  if (props.closeOnClickModal) {
+    handleClose('close')
+  } else {
+    popupJitter()
+  }
+}
+
 
 onMounted(async () => {
   mainStyle.value.zIndex = nextZIndex()
   contentStyle.value.zIndex = nextZIndex()
   document.body.className = 'el-popup-parent--hidden'
   await nextTick()
+  if (props.showInput) {
+    focusStartRef.value = mainRef.value
+  } else {
+    focusStartRef.value = confirmRef?.value ?? mainRef.value
+  }
   await sleep(100)
   visible.value = true
   props.setClose(close)
@@ -220,88 +278,92 @@ function close() {
        role="dialog"
        aria-modal="true"
        @click.stop="maskClick">
-    <transition name="cy-message_show" @after-enter="onAfterEnter" @after-leave="onAfterLeave">
-      <div class="cy-message-box_body"
-           tabindex="-1"
-           v-show="visible"
-           :style="contentStyle"
-           @click.stop
-           ref="bodyRef">
-        <div class="cy-message-box_close" @click.stop="handleClose('close')">
-          <ElIcon>
-            <Close/>
-          </ElIcon>
-        </div>
-        <header ref="headerRef">
-          <el-icon :class="['cy-' + props.type]"
-                   class="cy-message_header-icon">
-            <component :is="headerIcon"/>
-          </el-icon>
-          <Component :is="titleRender"/>
-        </header>
-        <div class="cy_message-box_content">
-          <div class="message" v-if="props.message">
-            <component
-                v-if="props.dangerouslyUseHTMLString"
-                :for="inputId"
-                :is="props.showInput ? 'label' : 'span'"
-                v-html="props.message">
-            </component>
-            <component v-else
-                       :is="props.showInput ? 'label' : 'span'"
-                       :for="inputId">
-              {{ props.message }}
-            </component>
-          </div>
-          <div v-if="props.showInput" style="margin-top: 9px">
-            <ElSelect v-model="inputValue"
-                      v-if="isSelectInput"
-                      :id="inputId"
-                      style="width: 100%"
-                      :multiple="false"
-                      :filterable="true"
-                      :allow-create="true"
-                      :default-first-option="true"
-                      :reserve-keyword="false"
-                      ref="inputRef">
-              <ElOption v-for="item in props.selectOption" :key="item.value" :label="item.label" :value="item.value"/>
-            </ElSelect>
-
-            <ElInput
-                v-else
-                ref="inputRef"
-                :autofocus="true"
-                :rows="props.inputRows"
-                :type="props.inputRows === 0 ? '' : 'textarea'"
-                :minlength="props.inputMinLength"
-                :maxlength="props.inputMaxLength"
-                :show-word-limit="true"
-                :id="inputId"
-                v-model.trim="inputValue"/>
+    <el-focus-trap
+        :trapped="visible"
+        :focus-trap-el="mainRef"
+        :focus-start-el="focusStartRef"
+        @release-requested="onCloseRequested">
+      <transition name="cy-message_show" @after-enter="onAfterEnter" @after-leave="onAfterLeave">
+        <div class="cy-message-box_body"
+             tabindex="-1"
+             v-show="visible"
+             :style="contentStyle"
+             @click.stop
+             ref="bodyRef">
+          <div class="cy-message-box_close" @click.stop="handleClose('close')">
+            <ElIcon>
+              <Close/>
+            </ElIcon>
           </div>
-          <div v-if="props.showInput" class="error_msg">
-            {{ errorMsg }}
+          <header ref="headerRef">
+            <el-icon :style="{color: headerColor() }"
+                     class="cy-message_header-icon">
+              <component :is="headerIcon"/>
+            </el-icon>
+            <Component :is="titleRender"/>
+          </header>
+          <div class="cy_message-box_content">
+            <div class="message" v-if="props.message">
+              <component
+                  v-if="props.dangerouslyUseHTMLString"
+                  :for="inputId"
+                  :is="props.showInput ? 'label' : 'span'"
+                  v-html="props.message">
+              </component>
+              <component v-else
+                         :is="props.showInput ? 'label' : 'span'"
+                         :for="inputId">
+                {{ props.message }}
+              </component>
+            </div>
+            <div v-if="props.showInput" style="margin-top: 9px">
+              <ElSelect v-model="inputValue"
+                        v-if="isSelectInput"
+                        :id="inputId"
+                        style="width: 100%"
+                        :multiple="false"
+                        :filterable="true"
+                        :allow-create="true"
+                        :default-first-option="true"
+                        :reserve-keyword="false"
+                        ref="inputRef">
+                <ElOption v-for="item in props.selectOption" :key="item.value" :label="item.label" :value="item.value"/>
+              </ElSelect>
+              <ElInput
+                  v-else
+                  ref="inputRef"
+                  :autofocus="true"
+                  :rows="props.inputRows"
+                  :type="props.inputRows === 0 ? '' : 'textarea'"
+                  :minlength="props.inputMinLength"
+                  :maxlength="props.inputMaxLength"
+                  :show-word-limit="true"
+                  :id="inputId"
+                  v-model.trim="inputValue"/>
+            </div>
+            <div v-if="props.showInput" class="error_msg">
+              {{ errorMsg }}
+            </div>
           </div>
+          <footer>
+            <button class="cancel"
+                    style="margin-right: 12px"
+                    v-if="props.showCancel"
+                    @click="handleClose('cancel')"
+                    :style="{'--cy--bg-color': '245,108,108','--cy-color': '#F56C6C'}">
+              {{ props.cancelButtonText }}
+            </button>
+            <button class="confirm"
+                    ref="confirmRef"
+                    :style="{'--cy--bg-color': '26, 92, 255','--cy-color': '#2C69FF'}"
+                    @click="handleConfirm">
+              {{ props.confirmButtonText }}
+            </button>
+          </footer>
         </div>
-        <footer>
-          <button class="cancel"
-                  style="margin-right: 12px"
-                  v-if="props.showCancel"
-                  @click="handleClose('cancel')"
-                  :style="{'--cy--bg-color': '245,108,108','--cy-color': '#F56C6C'}">
-            {{ props.cancelButtonText }}
-          </button>
-          <button class="confirm"
-                  ref="confirmRef"
-                  :style="{'--cy--bg-color': '26, 92, 255','--cy-color': '#2C69FF'}"
-                  @click="handleConfirm">
-            {{ props.confirmButtonText }}
-          </button>
-        </footer>
-      </div>
-    </transition>
+      </transition>
+    </el-focus-trap>
   </div>
-
 </template>
 
 <style lang="scss">
@@ -322,18 +384,20 @@ function close() {
 }
 
 .cy-message-box_main {
-  background: rgba(0, 0, 0, .5);
+  background: rgba(0, 0, 0, .2);
   position: fixed;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 
   .cy-message-box_body {
     display: inline-block;
     height: max-content;
     width: 418px;
-    margin: auto;
     background-color: white;
     border-radius: 20px;
     padding: 5px;
@@ -386,21 +450,6 @@ function close() {
         font-size: 24px;
       }
 
-      .cy-warning {
-        color: #e6a23c;
-      }
-
-      .cy-success {
-        color: #67c23a;
-      }
-
-      .cy-info {
-        color: #909399;
-      }
-
-      .cy-error {
-        color: #f56c6c;
-      }
     }
 
     .cy_message-box_content {
@@ -446,7 +495,6 @@ function close() {
       padding: 0 16px 10px 16px;
 
       button {
-        border: 0;
         width: 60px;
         line-height: 28px;
         text-align: center;
@@ -458,6 +506,17 @@ function close() {
         list-style: none;
         user-select: none;
         background: transparent;
+        box-sizing: border-box;
+        border: 1px solid transparent;
+        transition: border .3s ease-out;
+
+        &:focus-visible {
+          border: 1px solid var(--cy-color);
+        }
+
+        &:hover {
+          border: 1px solid transparent !important;
+        }
 
         &:hover:before {
           transform: scale(1.3);

+ 8 - 0
src/components/xiao-chan/focus-trap/index.ts

@@ -0,0 +1,8 @@
+//@ts-ignore
+import ElFocusTrap from './src/focus-trap.vue'
+
+export {ElFocusTrap}
+
+export default ElFocusTrap
+export * from './src/tokens'
+export * from './src/utils'

+ 331 - 0
src/components/xiao-chan/focus-trap/src/focus-trap.vue

@@ -0,0 +1,331 @@
+<template>
+  <slot :handle-keydown="onKeydown"/>
+</template>
+<script lang="ts">
+import {
+  defineComponent,
+  nextTick,
+  onBeforeUnmount,
+  onMounted,
+  provide,
+  ref,
+  unref,
+  watch,
+} from 'vue'
+import {isNil} from 'lodash-unified'
+import {EVENT_CODE} from 'element-plus'
+import {useEscapeKeydown} from 'element-plus'
+import {
+  createFocusOutPreventedEvent,
+  focusFirstDescendant,
+  focusableStack,
+  getEdges,
+  isFocusCausedByUserEvent,
+  obtainAllFocusableElements,
+  tryFocus,
+  useFocusReason,
+} from './utils'
+import {
+  FOCUS_AFTER_RELEASED,
+  FOCUS_AFTER_TRAPPED,
+  FOCUS_AFTER_TRAPPED_OPTS,
+  FOCUS_TRAP_INJECTION_KEY,
+  ON_RELEASE_FOCUS_EVT,
+  ON_TRAP_FOCUS_EVT,
+} from './tokens'
+
+import type {PropType} from 'vue'
+import type {FocusLayer} from './utils'
+import {isString} from "xe-utils";
+import sleep from "@/utils/sleep";
+
+export default defineComponent({
+  name: 'ElFocusTrap',
+  inheritAttrs: false,
+  props: {
+    loop: Boolean,
+    trapped: Boolean,
+    focusTrapEl: Object as PropType<HTMLElement>,
+    focusStartEl: {
+      type: [Object, String] as PropType<'container' | 'first' | HTMLElement>,
+      default: 'first',
+    },
+  },
+  emits: [
+    ON_TRAP_FOCUS_EVT,
+    ON_RELEASE_FOCUS_EVT,
+    'focusin',
+    'focusout',
+    'focusout-prevented',
+    'release-requested',
+  ],
+  setup(props, {emit}) {
+    const forwardRef = ref<HTMLElement | undefined>()
+    let lastFocusBeforeTrapped: HTMLElement | null
+    let lastFocusAfterTrapped: HTMLElement | null
+
+    const {focusReason} = useFocusReason()
+
+    useEscapeKeydown((event) => {
+      if (props.trapped && !focusLayer.paused) {
+        emit('release-requested', event)
+      }
+    })
+
+    const focusLayer: FocusLayer = {
+      paused: false,
+      pause() {
+        this.paused = true
+      },
+      resume() {
+        this.paused = false
+      },
+    }
+
+    const onKeydown = (e: KeyboardEvent) => {
+      if (!props.loop && !props.trapped) return
+      if (focusLayer.paused) return
+
+      const {key, altKey, ctrlKey, metaKey, currentTarget, shiftKey} = e
+      const {loop} = props
+      const isTabbing =
+          key === EVENT_CODE.tab && !altKey && !ctrlKey && !metaKey
+
+      const currentFocusingEl = document.activeElement
+      if (isTabbing && currentFocusingEl) {
+        const container = currentTarget as HTMLElement
+        const [first, last] = getEdges(container)
+        const isTabbable = first && last
+        if (!isTabbable) {
+          if (currentFocusingEl === container) {
+            const focusoutPreventedEvent = createFocusOutPreventedEvent({
+              focusReason: focusReason.value,
+            })
+            emit('focusout-prevented', focusoutPreventedEvent)
+            if (!focusoutPreventedEvent.defaultPrevented) {
+              e.preventDefault()
+            }
+          }
+        } else {
+          if (!shiftKey && currentFocusingEl === last) {
+            const focusoutPreventedEvent = createFocusOutPreventedEvent({
+              focusReason: focusReason.value,
+            })
+            emit('focusout-prevented', focusoutPreventedEvent)
+            if (!focusoutPreventedEvent.defaultPrevented) {
+              e.preventDefault()
+              if (loop) tryFocus(first, true)
+            }
+          } else if (
+              shiftKey &&
+              [first, container].includes(currentFocusingEl as HTMLElement)
+          ) {
+            const focusoutPreventedEvent = createFocusOutPreventedEvent({
+              focusReason: focusReason.value,
+            })
+            emit('focusout-prevented', focusoutPreventedEvent)
+            if (!focusoutPreventedEvent.defaultPrevented) {
+              e.preventDefault()
+              if (loop) tryFocus(last, true)
+            }
+          }
+        }
+      }
+    }
+
+    provide(FOCUS_TRAP_INJECTION_KEY, {
+      focusTrapRef: forwardRef,
+      onKeydown,
+    })
+
+    watch(
+        () => props.focusTrapEl,
+        (focusTrapEl) => {
+          if (focusTrapEl) {
+            forwardRef.value = focusTrapEl
+          }
+        },
+        {immediate: true}
+    )
+
+    watch([forwardRef], ([forwardRef], [oldForwardRef]) => {
+      if (forwardRef) {
+        forwardRef.addEventListener('keydown', onKeydown)
+        forwardRef.addEventListener('focusin', onFocusIn)
+        forwardRef.addEventListener('focusout', onFocusOut)
+      }
+      if (oldForwardRef) {
+        oldForwardRef.removeEventListener('keydown', onKeydown)
+        oldForwardRef.removeEventListener('focusin', onFocusIn)
+        oldForwardRef.removeEventListener('focusout', onFocusOut)
+      }
+    })
+
+    const trapOnFocus = (e: Event) => {
+      emit(ON_TRAP_FOCUS_EVT, e)
+    }
+    const releaseOnFocus = (e: Event) => emit(ON_RELEASE_FOCUS_EVT, e)
+
+    const onFocusIn = (e: FocusEvent) => {
+      const trapContainer = unref(forwardRef)
+      if (!trapContainer) return
+
+      const target = e.target as HTMLElement | null
+      const relatedTarget = e.relatedTarget as HTMLElement | null
+      const isFocusedInTrap = target && trapContainer.contains(target)
+
+      if (!props.trapped) {
+        const isPrevFocusedInTrap =
+            relatedTarget && trapContainer.contains(relatedTarget)
+        if (!isPrevFocusedInTrap) {
+          lastFocusBeforeTrapped = relatedTarget
+        }
+      }
+
+      if (isFocusedInTrap) emit('focusin', e)
+
+      if (focusLayer.paused) return
+
+      if (props.trapped) {
+        if (isFocusedInTrap) {
+          lastFocusAfterTrapped = target
+        } else {
+          tryFocus(lastFocusAfterTrapped, true)
+        }
+      }
+    }
+
+    const onFocusOut = (e: Event) => {
+      const trapContainer = unref(forwardRef)
+      if (focusLayer.paused || !trapContainer) return
+
+      if (props.trapped) {
+        const relatedTarget = (e as FocusEvent)
+            .relatedTarget as HTMLElement | null
+        if (!isNil(relatedTarget) && !trapContainer.contains(relatedTarget)) {
+          // Give embedded focus layer time to pause this layer before reclaiming focus
+          // And only reclaim focus if it should currently be trapping
+          setTimeout(() => {
+            if (!focusLayer.paused && props.trapped) {
+              const focusoutPreventedEvent = createFocusOutPreventedEvent({
+                focusReason: focusReason.value,
+              })
+              emit('focusout-prevented', focusoutPreventedEvent)
+              if (!focusoutPreventedEvent.defaultPrevented) {
+                tryFocus(lastFocusAfterTrapped, true)
+              }
+            }
+          }, 0)
+        }
+      } else {
+        const target = e.target as HTMLElement | null
+        const isFocusedInTrap = target && trapContainer.contains(target)
+        if (!isFocusedInTrap) emit('focusout', e)
+      }
+    }
+
+    async function startTrap() {
+      // Wait for forwardRef to resolve
+      await nextTick()
+      await sleep(200)
+      const trapContainer = unref(forwardRef)
+      if (trapContainer) {
+        focusableStack.push(focusLayer)
+        const prevFocusedElement = trapContainer.contains(
+            document.activeElement
+        )
+            ? lastFocusBeforeTrapped
+            : document.activeElement
+        lastFocusBeforeTrapped = prevFocusedElement as HTMLElement | null
+        const isPrevFocusContained = trapContainer.contains(prevFocusedElement)
+        if (!isPrevFocusContained) {
+          const focusEvent = new Event(
+              FOCUS_AFTER_TRAPPED,
+              FOCUS_AFTER_TRAPPED_OPTS
+          )
+          trapContainer.addEventListener(FOCUS_AFTER_TRAPPED, trapOnFocus)
+          trapContainer.dispatchEvent(focusEvent)
+          if (!focusEvent.defaultPrevented) {
+            nextTick(() => {
+              let focusStartEl = props.focusStartEl
+              if (!isString(focusStartEl)) {
+                tryFocus(focusStartEl)
+                if (document.activeElement !== focusStartEl) {
+                  focusStartEl = 'first'
+                }
+              }
+              if (focusStartEl === 'first') {
+                focusFirstDescendant(
+                    obtainAllFocusableElements(trapContainer),
+                    true
+                )
+              }
+              if (
+                  document.activeElement === prevFocusedElement ||
+                  focusStartEl === 'container'
+              ) {
+                tryFocus(trapContainer)
+              }
+            })
+          }
+        }
+      }
+    }
+
+    function stopTrap() {
+      const trapContainer = unref(forwardRef)
+
+      if (trapContainer) {
+        trapContainer.removeEventListener(FOCUS_AFTER_TRAPPED, trapOnFocus)
+
+        const releasedEvent = new CustomEvent(FOCUS_AFTER_RELEASED, {
+          ...FOCUS_AFTER_TRAPPED_OPTS,
+          detail: {
+            focusReason: focusReason.value,
+          },
+        })
+        trapContainer.addEventListener(FOCUS_AFTER_RELEASED, releaseOnFocus)
+        trapContainer.dispatchEvent(releasedEvent)
+        if (
+            !releasedEvent.defaultPrevented &&
+            (focusReason.value == 'keyboard' ||
+                !isFocusCausedByUserEvent() ||
+                trapContainer.contains(document.activeElement))
+        ) {
+          tryFocus(lastFocusBeforeTrapped ?? document.body)
+        }
+
+        trapContainer.removeEventListener(FOCUS_AFTER_RELEASED, releaseOnFocus)
+        focusableStack.remove(focusLayer)
+      }
+    }
+
+    onMounted(() => {
+      if (props.trapped) {
+        startTrap()
+      }
+
+      watch(
+          () => props.trapped,
+          (trapped) => {
+            if (trapped) {
+              startTrap()
+            } else {
+              stopTrap()
+            }
+          }
+      )
+    })
+
+    onBeforeUnmount(() => {
+      if (props.trapped) {
+        stopTrap()
+      }
+    })
+
+    return {
+      onKeydown,
+    }
+  },
+})
+</script>

+ 24 - 0
src/components/xiao-chan/focus-trap/src/tokens.ts

@@ -0,0 +1,24 @@
+import type { InjectionKey, Ref } from 'vue'
+
+export const FOCUS_AFTER_TRAPPED = 'focus-trap.focus-after-trapped'
+export const FOCUS_AFTER_RELEASED = 'focus-trap.focus-after-released'
+export const FOCUSOUT_PREVENTED = 'focus-trap.focusout-prevented'
+export const FOCUS_AFTER_TRAPPED_OPTS: EventInit = {
+  cancelable: true,
+  bubbles: false,
+}
+export const FOCUSOUT_PREVENTED_OPTS: EventInit = {
+  cancelable: true,
+  bubbles: false,
+}
+
+export const ON_TRAP_FOCUS_EVT = 'focusAfterTrapped'
+export const ON_RELEASE_FOCUS_EVT = 'focusAfterReleased'
+
+export type FocusTrapInjectionContext = {
+  focusTrapRef: Ref<HTMLElement | undefined>
+  onKeydown: (e: KeyboardEvent) => void
+}
+
+export const FOCUS_TRAP_INJECTION_KEY: InjectionKey<FocusTrapInjectionContext> =
+  Symbol('elFocusTrap')

+ 196 - 0
src/components/xiao-chan/focus-trap/src/utils.ts

@@ -0,0 +1,196 @@
+import { onBeforeUnmount, onMounted, ref } from 'vue'
+import { FOCUSOUT_PREVENTED, FOCUSOUT_PREVENTED_OPTS } from './tokens'
+
+const focusReason = ref<'pointer' | 'keyboard'>()
+const lastUserFocusTimestamp = ref<number>(0)
+const lastAutomatedFocusTimestamp = ref<number>(0)
+let focusReasonUserCount = 0
+
+export type FocusLayer = {
+  paused: boolean
+  pause: () => void
+  resume: () => void
+}
+
+export type FocusStack = FocusLayer[]
+
+export const obtainAllFocusableElements = (
+  element: HTMLElement
+): HTMLElement[] => {
+  const nodes: HTMLElement[] = []
+  const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, {
+    acceptNode: (
+      node: Element & {
+        disabled: boolean
+        hidden: boolean
+        type: string
+        tabIndex: number
+      }
+    ) => {
+      const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden'
+      if (node.disabled || node.hidden || isHiddenInput)
+        return NodeFilter.FILTER_SKIP
+      return node.tabIndex >= 0 || node === document.activeElement
+        ? NodeFilter.FILTER_ACCEPT
+        : NodeFilter.FILTER_SKIP
+    },
+  })
+  while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement)
+
+  return nodes
+}
+
+export const getVisibleElement = (
+  elements: HTMLElement[],
+  container: HTMLElement
+) => {
+  for (const element of elements) {
+    if (!isHidden(element, container)) return element
+  }
+}
+
+export const isHidden = (element: HTMLElement, container: HTMLElement) => {
+  if (process.env.NODE_ENV === 'test') return false
+  if (getComputedStyle(element).visibility === 'hidden') return true
+
+  while (element) {
+    if (container && element === container) return false
+    if (getComputedStyle(element).display === 'none') return true
+    element = element.parentElement as HTMLElement
+  }
+
+  return false
+}
+
+export const getEdges = (container: HTMLElement) => {
+  const focusable = obtainAllFocusableElements(container)
+  const first = getVisibleElement(focusable, container)
+  const last = getVisibleElement(focusable.reverse(), container)
+  return [first, last]
+}
+
+const isSelectable = (
+  element: any
+): element is HTMLInputElement & { select: () => void } => {
+  return element instanceof HTMLInputElement && 'select' in element
+}
+
+export const tryFocus = (
+  element?: HTMLElement | { focus: () => void } | null,
+  shouldSelect?: boolean
+) => {
+  if (element && element.focus) {
+    const prevFocusedElement = document.activeElement
+    element.focus({ preventScroll: true })
+    lastAutomatedFocusTimestamp.value = window.performance.now()
+    if (
+      element !== prevFocusedElement &&
+      isSelectable(element) &&
+      shouldSelect
+    ) {
+      element.select()
+    }
+  }
+}
+
+function removeFromStack<T>(list: T[], item: T) {
+  const copy = [...list]
+
+  const idx = list.indexOf(item)
+
+  if (idx !== -1) {
+    copy.splice(idx, 1)
+  }
+  return copy
+}
+
+const createFocusableStack = () => {
+  let stack = [] as FocusStack
+
+  const push = (layer: FocusLayer) => {
+    const currentLayer = stack[0]
+
+    if (currentLayer && layer !== currentLayer) {
+      currentLayer.pause()
+    }
+
+    stack = removeFromStack(stack, layer)
+    stack.unshift(layer)
+  }
+
+  const remove = (layer: FocusLayer) => {
+    stack = removeFromStack(stack, layer)
+    stack[0]?.resume?.()
+  }
+
+  return {
+    push,
+    remove,
+  }
+}
+
+export const focusFirstDescendant = (
+  elements: HTMLElement[],
+  shouldSelect = false
+) => {
+  const prevFocusedElement = document.activeElement
+  for (const element of elements) {
+    tryFocus(element, shouldSelect)
+    if (document.activeElement !== prevFocusedElement) return
+  }
+}
+
+export const focusableStack = createFocusableStack()
+
+export const isFocusCausedByUserEvent = (): boolean => {
+  return lastUserFocusTimestamp.value > lastAutomatedFocusTimestamp.value
+}
+
+const notifyFocusReasonPointer = () => {
+  focusReason.value = 'pointer'
+  lastUserFocusTimestamp.value = window.performance.now()
+}
+
+const notifyFocusReasonKeydown = () => {
+  focusReason.value = 'keyboard'
+  lastUserFocusTimestamp.value = window.performance.now()
+}
+
+export const useFocusReason = (): {
+  focusReason: typeof focusReason
+  lastUserFocusTimestamp: typeof lastUserFocusTimestamp
+  lastAutomatedFocusTimestamp: typeof lastAutomatedFocusTimestamp
+} => {
+  onMounted(() => {
+    if (focusReasonUserCount === 0) {
+      document.addEventListener('mousedown', notifyFocusReasonPointer)
+      document.addEventListener('touchstart', notifyFocusReasonPointer)
+      document.addEventListener('keydown', notifyFocusReasonKeydown)
+    }
+    focusReasonUserCount++
+  })
+
+  onBeforeUnmount(() => {
+    focusReasonUserCount--
+    if (focusReasonUserCount <= 0) {
+      document.removeEventListener('mousedown', notifyFocusReasonPointer)
+      document.removeEventListener('touchstart', notifyFocusReasonPointer)
+      document.removeEventListener('keydown', notifyFocusReasonKeydown)
+    }
+  })
+
+  return {
+    focusReason,
+    lastUserFocusTimestamp,
+    lastAutomatedFocusTimestamp,
+  }
+}
+
+export const createFocusOutPreventedEvent = (
+  detail: CustomEventInit['detail']
+) => {
+  return new CustomEvent(FOCUSOUT_PREVENTED, {
+    ...FOCUSOUT_PREVENTED_OPTS,
+    detail,
+  })
+}

+ 1 - 0
src/components/zhu-yuan-yi-sheng/emr/emr-template/EmrSidebar.vue

@@ -273,6 +273,7 @@ onMounted(() => {
       }
     })
     getEmrTree().then((res) => {
+      console.log(res)
       returnData.value.emrTree = res.all
       returnData.value.deptTree = res.dept
     })

+ 5 - 4
src/components/zhu-yuan-yi-sheng/yi-zhu-lu-ru/yz-edit/YzEditor.vue

@@ -300,7 +300,7 @@ import {
 } from 'vue'
 import {userInfoStore} from "@/utils/store-public";
 import {getServerDate} from "@/utils/moment-utils";
-import {CyMessageBox} from "@/components/xiao-chan/cy-message-box/cy-message-box";
+import {CyMessageBox} from "@/components/xiao-chan/cy-message-box/index";
 
 const props = withDefaults(defineProps<{
   patientInfo: {
@@ -1131,8 +1131,10 @@ onMounted(() => {
       BizException(ExceptionEnum.MESSAGE_ERROR, '请先选择要删除的医嘱')
     }
 
-    CyMessageBox.confirm(`确认是否要删除<span style="color: red"> ${orderName} </span>`, 'warning', {
-      dangerouslyUseHTMLString: true
+    CyMessageBox.confirm({
+      message: `确认是否要删除<span style="color: red"> ${orderName} </span>`,
+      dangerouslyUseHTMLString: true,
+      type: 'delete',
     }).then(() => {
       toDeleteAnOrder(actOrderNo).then(() => {
         yzMitt.emit('queryYz', false)
@@ -1143,7 +1145,6 @@ onMounted(() => {
     }).catch(() => {
 
     })
-
   })
 })
 

+ 8 - 2
src/router/modules/dashboard.js

@@ -8,6 +8,12 @@ const route = [
         hideMenu: true,
         meta: {title: '登录', hideTabs: true},
     },
+    {
+        path: '/mzEmr/:patientId?',
+        component: createNameComponent(() => import('@/views/mz-emr/MzEmr.vue')),
+        hideMenu: true,
+        meta: {title: '登录', hideTabs: true, passRule: true},
+    },
     {
         path: '/todayClinicResource',
         component: createNameComponent(() => import('@/views/single-page/TodayClinicResource.vue')),
@@ -984,11 +990,11 @@ const route = [
                 path: 'drugManage/TcInfo',
                 component: createNameComponent(() => import('@/views/medical-advice/drug-manage/TcInfo.vue')),
                 meta: {title: '生成退药单'},
-            },{
+            }, {
                 path: 'drugManage/FyInfo',
                 component: createNameComponent(() => import('@/views/medical-advice/drug-manage/FyInfo.vue')),
                 meta: {title: '已发药单查询'},
-            },{
+            }, {
                 path: 'drugManage/DsyInfo',
                 component: createNameComponent(() => import('@/views/medical-advice/drug-manage/DsyInfo.vue')),
                 meta: {title: '大输液统计'},

+ 5 - 41
src/views/hospitalization/zhu-yuan-yi-sheng/yi-zhu-lu-ru/YiZhuLuRu.vue

@@ -89,7 +89,7 @@ import {applicationForRevocation} from "@/api/zhu-yuan-yi-sheng/qrder-quash";
 import XEUtils from 'xe-utils'
 import {isDev} from "@/utils/public";
 import {ref, onMounted, onActivated, nextTick} from 'vue'
-import {CyMessageBox} from "@/components/xiao-chan/cy-message-box/cy-message-box";
+import {CyMessageBox} from "@/components/xiao-chan/cy-message-box/index";
 
 let allergen = ref({
   dialog: false,
@@ -169,13 +169,14 @@ const orderQuash = async (val) => {
     BizException(ExceptionEnum.LOGICAL_ERROR, "录入医嘱无需撤销删除即可。");
   }
 
-  CyMessageBox.prompt(`
-申请撤销<span style="color: red">【${val.orderName}】</span>医嘱<br />
+  CyMessageBox.prompt({
+    message: `申请撤销<span style="color: red">【${val.orderName}】</span>医嘱<br />
 可在数据修改 》 医嘱撤销审核页面中查看到自己的申请信息。<br />
 <span style="color:red">作废医嘱不会退费,需要在项目录入中退费,如果是药品先撤销后退费的话,会导致无法生成退药单,
 只能仅退费,停止医嘱会产生退费,作废前可以先进行停止操作看费用会不会退掉,如果停止医嘱没有产生退费可以让护士在项目录入中退费,退费前请记得做费用接收重算,避免重复退费。</span><br />
 <span style="color: #0900ff">医嘱未超过24小时,未产生任何费用,会直接撤销无需审核,其他需要医务部审核通过后才能撤销。</span>
-`, 'warning', {
+`,
+    type: 'warning',
     dangerouslyUseHTMLString: true,
     inputDefaultValue: '医嘱开错',
     selectOption: [
@@ -211,44 +212,7 @@ const orderQuash = async (val) => {
       setYzOrderGroup();
     }
   }).catch(() => {
-
   })
-
-//   ElMessageBox.prompt(`
-// 申请撤销<span style="color: red">【${val.orderName}】</span>医嘱<br />
-// 可在数据修改 》 医嘱撤销审核页面中查看到自己的申请信息。<br />
-// <span style="color:red">作废医嘱不会退费,需要在项目录入中退费,如果是药品先撤销后退费的话,会导致无法生成退药单,
-// 只能仅退费,停止医嘱会产生退费,作废前可以先进行停止操作看费用会不会退掉,如果停止医嘱没有产生退费可以让护士在项目录入中退费,退费前请记得做费用接收重算,避免重复退费。</span><br />
-// <span style="color: #0900ff">医嘱未超过24小时,未产生任何费用,会直接撤销无需审核,其他需要医务部审核通过后才能撤销。</span>
-// `, '提示', {
-//     type: 'warning',
-//     confirmButtonText: '确定',
-//     cancelButtonText: '取消',
-//     inputValidator: (val) => {
-//       val = val.trim();
-//       if (val === null || val.length < 1 || val.length > 50) {
-//         return false;
-//       }
-//     },
-//     dangerouslyUseHTMLString: true,
-//     inputErrorMessage: '作废原因,不能为空,最多可输入20个字。',
-//     closeOnPressEscape: false,
-//     closeOnClickModal: false
-//   }).then(async ({value}) => {
-//     let res = await applicationForRevocation({
-//       actOrderNo: val.actOrderNo,
-//       reqRemark: value,
-//       patNo: val.inpatientNo,
-//       times: val.admissTimes
-//     });
-//     // res === 1的意思是这个医嘱被删除了
-//     if (res === 1) {
-//       XEUtils.remove(yzData.value, (item) => {
-//         return item.actOrderNo === val.actOrderNo;
-//       })
-//       setYzOrderGroup();
-//     }
-//   })
 }
 
 /**

+ 299 - 0
src/views/mz-emr/MzEmr.vue

@@ -0,0 +1,299 @@
+<script setup lang="ts">
+import router from '@/router'
+import {onMounted, ref} from "vue";
+import {
+  useEmrInit,
+  UseEmrInitReturn
+} from "@/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/emr-init-v2";
+import {EditType} from "@/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/edit";
+import {userInfoStore} from "@/utils/store-public";
+
+
+let patNo: {
+  times: number;
+  patNo: string
+} = {
+  times: 2,
+  patNo: '123123'
+}
+
+export interface Property {
+  editorVersion?: string;
+  creator?: string;
+  modifyTime?: string;
+  createTime?: string;
+  modifier?: string;
+  reviewer?: string;
+  reviewTime?: string;
+}
+
+export interface Version {
+  name?: string;
+  properties?: Property;
+}
+
+export interface emrTemplateType {
+  parent?: string;
+  code?: string;
+  versions?: Version[];
+  sortNumber?: number;
+  name?: string;
+  description?: string;
+  _id?: string;
+  type?: string;
+  labels?: string[];
+}
+
+const emrTemplateData: emrTemplateType[] = [
+  {
+    "parent": "7db106c0416511ee8f5be9d1647a689f",
+    "code": "mzczdzbl",
+    "versions": [
+      {
+        "name": "v1.0.20",
+        "properties": {
+          "editorVersion": "v5.1.183",
+          "creator": "root",
+          "modifyTime": "2023-09-08 11:58:14",
+          "createTime": "2023-08-23 11:54:35",
+          "modifier": "root",
+          "reviewer": "root",
+          "reviewTime": "2023-09-13 15:31:03"
+        }
+      }
+    ],
+    "sortNumber": 1,
+    "name": "门诊初诊电子病历",
+    "description": "",
+    "_id": "40e2be70416811ee9b9fcbee7239e0bc",
+    "type": "category",
+    "labels": [
+      ""
+    ]
+  },
+  {
+    "parent": "7db106c0416511ee8f5be9d1647a689f",
+    "code": "mzfzdzbl",
+    "versions": [
+      {
+        "name": "v1.0.12",
+        "properties": {
+          "editorVersion": "v5.1.183",
+          "creator": "root",
+          "modifyTime": "2023-09-08 11:58:28",
+          "createTime": "2023-09-01 16:50:41",
+          "modifier": "root",
+          "reviewer": "root",
+          "reviewTime": "2023-09-13 15:31:03"
+        }
+      }
+    ],
+    "sortNumber": 2,
+    "name": "门诊复诊电子病历",
+    "description": "",
+    "_id": "a0f289b048a411ee9403cb9efee3eba7",
+    "type": "category",
+    "labels": [
+      ""
+    ]
+  },
+  {
+    "parent": "7db106c0416511ee8f5be9d1647a689f",
+    "code": "mzczdzbl",
+    "versions": [
+      {
+        "name": "v1.0.22",
+        "properties": {
+          "editorVersion": "v5.1.183",
+          "creator": "root",
+          "modifyTime": "2023-09-10 17:30:44",
+          "createTime": "2023-09-01 17:14:01",
+          "modifier": "root",
+          "reviewer": "root",
+          "reviewTime": "2023-09-13 15:31:03"
+        }
+      }
+    ],
+    "sortNumber": 3,
+    "name": "急诊电子病历",
+    "description": "",
+    "_id": "e3af514048a711ee9403cb9efee3eba7",
+    "type": "category",
+    "labels": [
+      ""
+    ]
+  },
+  {
+    "parent": "7db106c0416511ee8f5be9d1647a689f",
+    "code": "mzczdzbl",
+    "versions": [
+      {
+        "name": "v1.0.14",
+        "properties": {
+          "editorVersion": "v5.1.183",
+          "creator": "root",
+          "modifyTime": "2023-09-08 11:58:55",
+          "createTime": "2023-09-01 17:18:56",
+          "modifier": "root",
+          "reviewer": "root",
+          "reviewTime": "2023-09-13 15:31:03"
+        }
+      }
+    ],
+    "sortNumber": 4,
+    "name": "急诊留观电子病历",
+    "description": "",
+    "_id": "93026f6048a811ee9403cb9efee3eba7",
+    "type": "category",
+    "labels": [
+      ""
+    ]
+  },
+  {
+    "parent": "7db106c0416511ee8f5be9d1647a689f",
+    "code": "siwangjilu",
+    "versions": [
+      {
+        "name": "v1.0.9",
+        "properties": {
+          "editorVersion": "v5.1.183",
+          "creator": "root",
+          "modifyTime": "2023-09-08 11:59:05",
+          "createTime": "2023-09-05 16:59:25",
+          "modifier": "root",
+          "reviewer": "root",
+          "reviewTime": "2023-09-13 15:31:03"
+        }
+      }
+    ],
+    "sortNumber": 5,
+    "name": "门/急诊死亡记录",
+    "description": "",
+    "_id": "7fe70e704bca11eeae7e71cf27500b2b",
+    "type": "category",
+    "labels": [
+      ""
+    ]
+  },
+  {
+    "parent": "7db106c0416511ee8f5be9d1647a689f",
+    "code": "e04052d04c8411ee8640a3b67e337659",
+    "versions": [],
+    "children": [
+      {
+        "parent": "e7014c004c8411ee8640a3b67e337659",
+        "code": "fab373c04fba11ee806c15315b8f18b0",
+        "versions": [
+          {
+            "name": "v1.0.0",
+            "properties": {
+              "editorVersion": "v5.1.183",
+              "creator": "root",
+              "createTime": "2023-09-10 17:18:28",
+              "reviewer": "root",
+              "reviewTime": "2023-09-13 15:31:03"
+            }
+          }
+        ],
+        "sortNumber": 2,
+        "name": "片段1",
+        "_id": "fccfeb204fba11ee806c15315b8f18b0",
+        "type": "category",
+        "labels": []
+      }
+    ],
+    "sortNumber": 6,
+    "name": "门/急诊病历片段",
+    "description": "",
+    "_id": "e7014c004c8411ee8640a3b67e337659",
+    "type": "group-category",
+    "labels": [
+      ""
+    ]
+  }
+]
+
+
+const emrDivRef = ref<HTMLDivElement>()
+let editor: UseEmrInitReturn;
+
+
+const rowClick = (row: emrTemplateType) => {
+  editor.loadAndSetDocument({
+    categoryId: row._id,
+    categoryCode: row.code,
+  })
+}
+
+function saveDocument() {
+  console.log(editor.editor.getDocument());
+}
+
+const appContext = () => {
+  return {
+    endpoints: {
+      app: "/bdp/dataservice/api",
+      his: import.meta.env.VITE_BASE_URL,
+    },
+    input: {
+      user: userInfoStore.value.code,
+      name: userInfoStore.value.name
+    },
+    login: {
+      token: userInfoStore.value.token,
+      user: {
+        id: userInfoStore.value.code,
+        name: userInfoStore.value.name
+      }
+    },
+    data: {
+      '患者姓名': '张三',
+      '性别名称': '男',
+      '患者年龄': 20,
+      '门/急诊号': '123123',
+      '接诊科室门诊': '眼科',
+    }
+  }
+}
+
+const handlePrint = () => {
+  editor.editor.execute("print", {
+    value: {
+      showPreview: false,
+    }
+  })
+}
+
+onMounted(() => {
+  patNo = JSON.parse(window.atob(router.currentRoute.value.params.patientId))
+
+  useEmrInit(emrDivRef.value, {
+    appContext: appContext
+  }).then(res => {
+    editor = res
+  })
+})
+</script>
+
+<template>
+  <el-container>
+    <el-aside style="width: 200px">
+      <el-button @click="handlePrint">
+        打印
+      </el-button>
+      <el-table :data="emrTemplateData" @row-click="rowClick">
+        <el-table-column label="名称" prop="name"></el-table-column>
+      </el-table>
+    </el-aside>
+    <el-main style="width: 100% ; height: 100%">
+      <div ref="emrDivRef" style="width: 100% ; height: 100%">
+
+      </div>
+    </el-main>
+  </el-container>
+
+</template>
+
+<style scoped lang="scss">
+
+</style>