new Files

This commit is contained in:
2025-05-04 23:07:09 +08:00
parent fe6fc6576d
commit d5be70128c
45 changed files with 924 additions and 5500 deletions

File diff suppressed because it is too large Load Diff

125
front/package-lock.json generated
View File

@ -9,12 +9,14 @@
"version": "0.0.0",
"dependencies": {
"@devui-design/icons": "^1.4.0",
"@vue/compiler-sfc": "^3.5.13",
"axios": "^1.9.0",
"bootstrap": "^5.3.5",
"chart.js": "^4.4.9",
"devui-theme": "^0.0.7",
"jquery": "^3.7.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^3.0.2",
"qs": "^6.14.0",
"stompjs": "^2.3.3",
@ -26,11 +28,15 @@
"devDependencies": {
"@popperjs/core": "^2.11.8",
"@types/node": "^22.15.3",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.18",
"@vitejs/plugin-vue": "^5.2.3",
"esbuild-plugin-vue": "^0.2.4",
"sass-embedded": "^1.86.3",
"unplugin-auto-import": "^19.1.2",
"vite": "^6.2.4",
"unplugin-vue-components": "^28.5.0",
"vite": "6.3.4",
"vite-plugin-mock-dev-server": "^1.8.5",
"vite-plugin-vue-devtools": "^7.7.2"
},
@ -1486,6 +1492,20 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/nprogress": {
"version": "0.2.3",
"resolved": "https://registry.npmmirror.com/@types/nprogress/-/nprogress-0.2.3.tgz",
"integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/path-browserify": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@types/path-browserify/-/path-browserify-1.0.3.tgz",
"integrity": "sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.9.18",
"resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.18.tgz",
@ -1510,7 +1530,7 @@
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
"integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==",
"dev": true,
"license": "MIT",
@ -1600,7 +1620,7 @@
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"license": "MIT",
"dependencies": {
@ -3399,6 +3419,20 @@
"@esbuild/win32-x64": "0.25.2"
}
},
"node_modules/esbuild-plugin-vue": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/esbuild-plugin-vue/-/esbuild-plugin-vue-0.2.4.tgz",
"integrity": "sha512-1noq1Tnnv4WAgdt5UROuNNIwQ7ppE5jlLB2MQD+XdzgprmOksGNTZlAEyYYF8oPMkq0qk1GB8KJJMTr2aE2Nzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"hash-sum": "^2.0.0",
"resolve-from": "^5.0.0"
},
"peerDependencies": {
"esbuild": ">=0.11 <=1"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@ -3527,9 +3561,9 @@
}
},
"node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"version": "6.4.4",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -3821,6 +3855,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash-sum": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz",
"integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
"dev": true,
"license": "MIT"
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -4659,6 +4700,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -4908,6 +4955,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
@ -5701,13 +5758,13 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
"integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
"version": "0.2.13",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.3",
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
@ -5950,6 +6007,42 @@
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/unplugin-vue-components": {
"version": "28.5.0",
"resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-28.5.0.tgz",
"integrity": "sha512-o7fMKU/uI8NiP+E0W62zoduuguWqB0obTfHFtbr1AP2uo2lhUPnPttWUE92yesdiYfo9/0hxIrj38FMc1eaySg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.6.0",
"debug": "^4.4.0",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"tinyglobby": "^0.2.12",
"unplugin": "^2.3.2",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@babel/parser": "^7.15.8",
"@nuxt/kit": "^3.2.2",
"vue": "2 || 3"
},
"peerDependenciesMeta": {
"@babel/parser": {
"optional": true
},
"@nuxt/kit": {
"optional": true
}
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@ -6026,18 +6119,18 @@
}
},
"node_modules/vite": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.0.tgz",
"integrity": "sha512-9aC0n4pr6hIbvi1YOpFjwQ+QOTGssvbJKoeYkuHHGWwlXfdxQlI8L2qNMo9awEEcCPSiS+5mJZk5jH1PAqoDeQ==",
"version": "6.3.4",
"resolved": "https://registry.npmmirror.com/vite/-/vite-6.3.4.tgz",
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.3",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.12"
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
@ -6250,7 +6343,7 @@
},
"node_modules/vue": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"license": "MIT",
"dependencies": {

View File

@ -10,12 +10,14 @@
},
"dependencies": {
"@devui-design/icons": "^1.4.0",
"@vue/compiler-sfc": "^3.5.13",
"axios": "^1.9.0",
"bootstrap": "^5.3.5",
"chart.js": "^4.4.9",
"devui-theme": "^0.0.7",
"jquery": "^3.7.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^3.0.2",
"qs": "^6.14.0",
"stompjs": "^2.3.3",
@ -27,11 +29,15 @@
"devDependencies": {
"@popperjs/core": "^2.11.8",
"@types/node": "^22.15.3",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.18",
"@vitejs/plugin-vue": "^5.2.3",
"esbuild-plugin-vue": "^0.2.4",
"sass-embedded": "^1.86.3",
"unplugin-auto-import": "^19.1.2",
"vite": "^6.2.4",
"unplugin-vue-components": "^28.5.0",
"vite": "6.3.4",
"vite-plugin-mock-dev-server": "^1.8.5",
"vite-plugin-vue-devtools": "^7.7.2"
},

View File

@ -1,32 +0,0 @@
<template>
<el-row>
<el-button-group>
<el-button plain type="primary" @click="appendHtml()">添加病程</el-button>
</el-button-group>
</el-row>
<Source src="/code/AppendDoc.vue"></Source>
<Editor doc="https://www.x-emr.cn/doc/233.html" @load="onLoad" style="margin: 10px 0;"></Editor>
</template>
<script setup>
import axios from 'axios'//引入axios
let editor
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
//添加日常病程
const appendHtml = function() {
axios.get('https://www.x-emr.cn/doc/233.html').then((result) => {
editor.appendHtml(result.data)
})
}
</script>

View File

@ -1,96 +0,0 @@
<template>
<Source src="/code/BindData.vue"></Source>
<el-row>
<el-col :span="16">
<Editor @load="onLoad" style="margin: 10px 0;"></Editor>
</el-col>
<el-col :span="8">
<el-card style="margin: 10px;">
<el-form @change="bindData()" label-width="auto">
<el-form-item label="姓名">
<el-input v-model="patient.pat_name" ></el-input>
</el-form-item>
<el-form-item label="性别">
<el-input v-model="patient.pat_sex"></el-input>
</el-form-item>
<el-form-item label="年龄">
<el-input v-model="patient.pat_age"></el-input>
</el-form-item>
<el-form-item label="就诊科室">
<el-input v-model="patient.visit_dept"></el-input>
</el-form-item>
<el-form-item label="就诊号">
<el-input v-model="patient.pat_id"></el-input>
</el-form-item>
<el-form-item label="就诊时间">
<el-date-picker v-model="patient.visit_time" type="datetime" value-format="YYYY-MM-DD hh:mm:ss"></el-date-picker>
<el-radio-group v-model="patient.firstcall" style="margin-left: 10px;">
<el-radio value="1">初诊</el-radio>
<el-radio value="2">复诊</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="patient.pat_phone"></el-input>
</el-form-item>
<el-form-item label=" 家庭住址">
<el-input v-model="patient.pat_address"></el-input>
</el-form-item>
<el-form-item label=" 主诉">
<el-input v-model="patient.pat_appeal"></el-input>
</el-form-item>
<el-form-item label=" 现病史">
<el-input v-model="patient.pat_now_history"></el-input>
</el-form-item>
<el-form-item label=" 既往史">
<el-input v-model="patient.pat_past_history"></el-input>
</el-form-item>
<el-form-item label=" 过敏史">
<el-input v-model="patient.pat_allergy_history"></el-input>
</el-form-item>
<el-form-item label=" 诊断">
<el-input v-model="patient.diagnosis"></el-input>
</el-form-item>
<el-form-item label=" 处方">
<el-input v-model="patient.presc" type="textarea"></el-input>
</el-form-item>
<el-form-item label=" 建议">
<el-input v-model="patient.advice"></el-input>
</el-form-item>
<el-form-item label=" 医生签字">
<el-input v-model="patient.doctor_name"></el-input>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
const patient = ref({})
var editor
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
setTimeout(()=>{
//异步加载文档
editor.loadUrl('/mock/bind_data.html').then(()=>{
patient.value = editor.getBindObject()
})
//文档输入后表单值随着变化
editor.document.addEventListener('input', ()=>{
patient.value = editor.getBindObject()
})
}, 0)
}
//表单数据改变
const bindData = () => {
editor.setBindObject(patient.value)
}
</script>

View File

@ -1,19 +0,0 @@
<template>
<Source src="/code/Calculate.vue"></Source>
<el-row>
<Editor doc="/mock/assess_table.html" @load="onLoad" style="margin: 10px 0;" mode="design"></Editor>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
var editor
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
</script>

View File

@ -1,47 +0,0 @@
<template>
<Source src="/code/Command.vue"></Source>
<el-row>
<el-button-group>
<el-button plain type="primary" @click="execCommand('form')">表单模式</el-button>
<el-button plain type="primary" @click="execCommand('design')">设计模式</el-button>
<el-button plain type="primary" @click="execCommand('readonly')">只读模式</el-button>
</el-button-group>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="execCommand('print')">打印</el-button>
<el-button plain type="primary" @click="execCommand('preview')">打印预览</el-button>
<el-button plain type="primary" @click="execCommand('previewPdf')">预览PDF</el-button>
<el-button plain type="primary" @click="execCommand('previewHtml')">预览HTML</el-button>
</el-button-group>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="execCommand('exportHtml')">导出模板</el-button>
<el-button plain type="primary" @click="execCommand('exportJson')">导出数据</el-button>
<el-button plain type="primary" @click="execCommand('exportPdf')">导出PDF</el-button>
<el-button plain type="primary" @click="execCommand('exportWord')">导出Word</el-button>
</el-button-group>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="execCommand('mobile')">移动填报</el-button>
</el-button-group>
</el-row>
<el-row>
<Editor doc="https://www.x-emr.cn/doc/999.html" @load="onLoad" style="margin: 10px 0;" mode="design"></Editor>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
var editor
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
//文档命令I
const execCommand = (cmd) => {
let param = {fileName:'病案首页'}
editor.execCommand(cmd, param)
}
const current = ref("1")
</script>

View File

@ -1,60 +0,0 @@
<template>
<Source src="/code/DataTable.vue"></Source>
<el-row>
<el-col :span="2" style="vertical-align: center;">服务器地址</el-col>
<el-col :span="8"><el-input v-model="dataUrl" ></el-input></el-col>
<el-col :span="1"></el-col>
<el-col :span="1"><el-button plain type="primary" @click="bindDataForTable">填充数据</el-button></el-col>
<el-col :span="1"></el-col>
<el-col :span="1"><el-button plain type="primary" @click="clearDataTable">清除数据</el-button></el-col>
<el-col :span="1"></el-col>
<el-col :span="1"><el-button plain type="primary" @click="execCommand('preview')">打印预览</el-button></el-col>
<el-col :span="1"></el-col>
<el-col :span="1"><el-button plain type="primary" @click="execCommand('print')">打印</el-button></el-col>
</el-row>
<Editor @load="onLoad" doc="/mock/data_table.html" mode="design" style="margin: 10px 0;"></Editor>
</template>
<script>
import axios from 'axios'
export default{
data(){
return{
editor:null,
//服务端地址
dataUrl:'https://www.x-emr.cn/doc/list.json'
}
},
methods:{
//加载编辑器
onLoad: function(e) {
this.editor = e.target.contentWindow.editor
},
//获取数据到表格
bindDataForTable: function() {
axios.get(this.dataUrl).then(res=>{
this.editor.bindDataList('list', res.data)
let html = `<field tabindex="0" id="" type="DropdownList" contenteditable="false" class="blank input" title="请选择" data-list="[{&quot;value&quot;:&quot;0&quot;,&quot;text&quot;:&quot;选项1&quot;},{&quot;value&quot;:&quot;1&quot;,&quot;text&quot;:&quot;选项2&quot;}]" name="" data-code="" data-expression="" event="undefined" multi="false" validate="false" data-show-vaule="" data-show-id="">请选择</field>`
this.editor.$('#list tr:not(:first) td:nth-child(3)').html(html)
})
},
//清除数据表格
clearDataTable: function() {
this.editor.bindDataList('list', [])
},
execCommand : function(cmd){
this.editor.execCommand(cmd)
}
}
}
</script>

View File

@ -1,27 +0,0 @@
<template>
<Source src="/code/DocLang.vue"></Source>
<el-tabs v-model="activeName">
<el-tab-pane label="英文" name="en-us" >
<Editor mode="design" lang="en-us" doc="/mock/en_us.html"></Editor>
</el-tab-pane>
<el-tab-pane label="简体中文" name="zh-cn">
<Editor mode="design" lang="zh-cn" doc="https://www.x-emr.cn/doc/999.html"></Editor>
</el-tab-pane>
<el-tab-pane label="繁体中文" name="zh-tw">
<Editor mode="design" lang="zh-tw" doc="/mock/zh-tw.html"></Editor>
</el-tab-pane>
<el-tab-pane label="藏文" name="zh-bo">
<Editor mode="design" lang="zh-bo" doc="/mock/zh-bo.html"></Editor>
</el-tab-pane>
<el-tab-pane label="维文" name="zh-ug">
<Editor mode="design" lang="zh-ug" doc="/mock/zh-ug.html"></Editor>
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { ref } from 'vue'
const activeName = ref('en-us')
</script>

View File

@ -1,20 +0,0 @@
<template>
<Source src="/code/DocMode.vue"></Source>
<el-tabs v-model="activeName">
<el-tab-pane label="设计模式" name="design">
<Editor mode="design" doc="https://www.x-emr.cn/doc/999.html"></Editor>
</el-tab-pane>
<el-tab-pane label="表单模式" name="form">
<Editor mode="form" doc="https://www.x-emr.cn/doc/999.html"></Editor>
</el-tab-pane>
<el-tab-pane label="阅读模式" name="readonly">
<Editor mode="readonly" doc="https://www.x-emr.cn/doc/999.html"></Editor>
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { ref } from 'vue'
const activeName = ref('design')
</script>

View File

@ -1,153 +0,0 @@
<template>
<h2>echarts 代码参考 https://echarts.apache.org/examples/zh/index.html</h2>
<el-row>
<el-button-group>
<el-button plain type="primary" @click="addSvgEChart()">添加图表(SVG)</el-button>
<el-button plain type="primary" @click="addCanvasEChart()">添加图表(Canvas)</el-button>
</el-button-group>
</el-row>
<Source src="/code/EChart.vue"></Source>
<Editor doc="https://www.x-emr.cn/doc/asdf1.html" @load="onLoad" style="margin: 10px 0;"></Editor>
</template>
<script setup>
import * as echarts from 'echarts';
let editor = null
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
//添加图表
const addSvgEChart = function () {
var _body = editor.document.getElementById('_body')
let chartDom = editor.document.createElement('div')
chartDom.style.width = '100%'
chartDom.style.height = '500px'
_body.appendChild(chartDom)
var myChart = echarts.init(chartDom, null, { renderer: 'svg' });
var option = {
title: {
text: 'Referer of a Website',
subtext: 'Fake Data',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: 'Access From',
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
option && myChart.setOption(option);
}
//添加图表
const addCanvasEChart = function () {
var _body = editor.document.getElementById('_body')
let chartDom = editor.document.createElement('div')
chartDom.style.width = '100%'
chartDom.style.height = '500px'
_body.appendChild(chartDom)
var myChart = echarts.init(chartDom, null, { renderer: 'canvas' });
var option = {
title: {
text: 'Rainfall vs Evaporation',
subtext: 'Fake Data'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['Rainfall', 'Evaporation']
},
toolbox: {
show: true,
feature: {
dataView: { show: true, readOnly: false },
magicType: { show: true, type: ['line', 'bar'] },
restore: { show: true },
saveAsImage: { show: true }
}
},
calculable: true,
xAxis: [
{
type: 'category',
// prettier-ignore
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: 'Rainfall',
type: 'bar',
data: [
2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20.0, 6.4, 3.3
],
markPoint: {
data: [
{ type: 'max', name: 'Max' },
{ type: 'min', name: 'Min' }
]
},
markLine: {
data: [{ type: 'average', name: 'Avg' }]
}
},
{
name: 'Evaporation',
type: 'bar',
data: [
2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3
],
markPoint: {
data: [
{ name: 'Max', value: 182.2, xAxis: 7, yAxis: 183 },
{ name: 'Min', value: 2.3, xAxis: 11, yAxis: 3 }
]
},
markLine: {
data: [{ type: 'average', name: 'Avg' }]
}
}
]
};
option && myChart.setOption(option);
}
</script>

View File

@ -1,18 +0,0 @@
<template>
<!-- 根据实际部署环境修改 editor.html 的路径 -->
<iframe src="./editor.html" v-bind="objectOfAttrs"></iframe>
</template>
<script>
export default {
data() {
return {
objectOfAttrs:{
width:'100%',
height:'800vh',
frameborder: 0
}
}
}
}
</script>

View File

@ -1,15 +0,0 @@
<template>
<h3>
Vue 集成X-EMR 编辑器步骤
</h3>
<h4>1.新建组件 Editor.vue</h4>
<Source src="/code/Editor.vue" open="true"></Source>
<h4>2.模块中使用@load获取组件的editor实例</h4>
<Source src="/code/Simple.vue" open="true"></Source>
<h4>3.自定义修改配置 editor.html</h4>
<Source src="/editor.html" open="true"></Source>
</template>
<script setup>
</script>

View File

@ -1,76 +0,0 @@
<template>
<el-row>
<el-col :span="2" style="vertical-align: center;">服务器地址</el-col>
<el-col :span="10"><el-input v-model="serverUrl" ></el-input></el-col>
<el-col :span="1"></el-col>
<el-col :span="2"><el-button plain type="primary" @click="saveHtml">保存文档</el-button></el-col>
<el-col :span="2"> <el-button plain type="primary" @click="saveHtmlAndData">保存数据&文档</el-button></el-col>
</el-row>
<Source src="/code/SaveDoc.vue"></Source>
<Editor @load="onLoad" doc="https://www.x-emr.cn/doc/999.html" style="margin: 10px 0;"></Editor>
</template>
<script>
import axios from 'axios'
export default{
data(){
return{
editor:null,
//服务端地址
serverUrl:'http://localhost/post'
}
},
methods:{
//初始化
onLoad: function(e) {
this.editor = e.target.contentWindow.editor
},
//仅保存HTML文档
saveHtml: function() {
// 若文档未修改,则无需保存
if(this.editor.edited == false){
this.$message.error('文档未修改,无需保存');
return;
}
// 若文档校验不通过,则无法保存
if(this.editor.validate() == false){
this.$message.error('请查看文档是否有未填项或不合规内容');
return;
}
let data = {'doc': this.editor.getHtml()}
axios.post(this.serverUrl, data).then(res=>{
this.$message.success('保存成功')
})
},
//保存文档及机构化数据
saveHtmlAndData: function() {
// 若文档未修改,则无需保存
if(this.editor.edited == false){
this.$message.error('文档未修改,无需保存');
return;
}
// 若文档校验不通过,则无法保存
if(this.editor.validate() == false){
this.$message.error('请查看文档是否有未填项或不合规内容');
return;
}
let data = {
'doc': this.editor.getHtml(),
'data': this.editor.getBindObject()
}
axios.post(this.serverUrl, data).then(res=>{
this.$message.success('保存成功')
})
},
}
}
</script>

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
<template>
<el-button plain type="primary" @click="execCommand('print')">打印</el-button>
<Editor doc="https://www.x-emr.cn/doc/999.html" @load="onLoad" style="margin: 10px 0;" mode="design"></Editor>
</template>
<script setup>
var editor = null
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
//处理文档命令
const execCommand = (cmd) => {
editor.execCommand(cmd)
}
</script>

View File

@ -1,134 +0,0 @@
<template>
<Source src="/code/VitalSigns.vue"></Source>
<el-row>
<el-form-item label="体温单ID">
<el-col :span="24"><el-input v-model="vitalSignsId" ></el-input></el-col>
</el-form-item>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="createVitalSigns()">创建体温单</el-button>
<el-button plain type="primary" @click="updateVitalSigns()">更新体温单</el-button>
</el-button-group>
<el-button-group style="margin: 0 20px;">
<el-button plain type="primary" @click="createBabyVitalSigns()">新生儿体温单</el-button>
</el-button-group>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="execCommand('preview')">打印预览</el-button>
<el-button plain type="primary" @click="execCommand('print')">打印</el-button>
</el-button-group>
</el-row>
<Editor @load="onLoad" style="margin: 10px 0;"></Editor>
</template>
<script setup>
import { ref } from 'vue'
const vitalSignsId = ref('')
let editor = null
//初始化后
const onLoad = function(e) {
editor = e.target.contentWindow.editor
}
//文档命令
const execCommand = (cmd) => {
editor.execCommand(cmd)
}
//添加体温单
const createVitalSigns = () => {
let data = {
"id": vitalSignsId.value,
"name": "吴晓莉",
"inDate": "2023-08-01",
"diag": "新型冠状病毒肺炎",
"dept": "呼吸内科",
"bed": "801",
"medicalNo": "202300991",
"begin": "2023-08-01",
"operateDate": "2023-08-03",
"notes": "入院-十时二十分,,转入ICU,,,,,,,手术,,,,,,,,,,,,,,,出院,死亡于×时×分",
"sphygmus": "112,110,109,103,108,85,90,83,90,103,108,85,90,83,90,,90,83,90,103,108,85,90,83,90",
"heart": "112,120,118,111,,,,,,,112,120,118,111",
"tempType": "0,1,2,3,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1",
"temperature": "38.7,38.9,38.5,39.1|37.1,38.5,38.5,,,38.5,38.5|36.9,38.4,38.8,,38.5,38.6,38.8,,38.9,38.8,38.5,38.6,38.8,,38.9,38.8,34,34.0",
"breath": "30,30,R,,35,35,35,35,35,35,35,35,,R,R,35,35,,R,,,35,35,,R",
"labels": "血压(mmHg)|入水量(ml)|出水量(ml)|大便(次)|小便(次)|身高(cm)|体重(kg)|过敏药",
"data1": "120/85,121/84,,110/75,",
"data2": "1180ml,,,500ml,,40ml",
"data3": "500ml,,,,500ml,,67ml",
"data4": "2,4,5,3,3,3,2,,2",
"data5": "2,4,5,3,3,3,2,,2",
"data6": "167cm,,,,,,,,,164cm",
"data7": "95kg,,,,,90kg",
"data8": "青霉素,",
"data9": "测试,"
}
vitalSignsId.value = editor.createVitalSigns(data)
}
//更新体温单
const updateVitalSigns = () => {
let data = {
"id": vitalSignsId.value,
"name": "吴晓莉",
"inDate": "2023-08-01",
"diag": "新型冠状病毒肺炎",
"dept": "呼吸内科",
"bed": "801",
"medicalNo": "202300991",
"begin": "2023-09-01",
"operateDate": "2023-08-03",
"notes": ",,,,,,,,,,,,,,出院,死亡于×时×分",
"sphygmus": "112,110,109,103,108,85,90,83,90,103,108,85,90,83,90,90,90,83,90,103,108,85,90,83,90",
"heart": ",,,,,,112,120,118,111,,,110,120,120,118,111,100",
"tempType": ",,,,,,0,1,2,3,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1",
"temperature": ",,,,,,38.7,38.9,38.5,39.1,38.5,38.5,,,38.5,38.5,38.4,38.8,,38.5,38.6,38.8,,38.9,38.838.5,38.6,38.8,,38.9,38.8,34,34.0",
"breath": ",,,,,,30,30,R,,35,35,35,35,35,35,35,35,,R,R,35,35,,R,,,35,35,,R",
"labels": "血压(mmHg)|入水量(ml)|出水量(ml)|大便(次)|小便(次)|身高(cm)|体重(kg)|过敏药",
"data1": ",,,,,,120/85,121/84,,110/75,",
"data2": ",,,,,,1180ml,,,500ml,,40ml",
"data3": "500ml,,,,500ml,,67ml",
"data4": "2,4,5,3,3,3,2,,2",
"data5": "2,4,5,3,3,3,2,,2",
"data6": "167cm,,,,,,,,,164cm",
"data7": "95kg,,,,,90kg",
"data8": "青霉素,",
"data9": "测试,",
"pain":"2,2,4,5,8,8,9|2,6|3,,,4,4,4"
}
vitalSignsId.value = editor.createVitalSigns(data)
}
//添加新生儿体温单
const createBabyVitalSigns = () => {
let data =
{
"id": vitalSignsId.value,
"type": "baby",
"name": "吴晓莉",
"inDate": "2023-08-01",
"sex": "男",
"dept": "妇产科",
"bed": "801",
"medicalNo": "202300991",
"begin": "2023-08-01",
"notes": "出生-十时二十分",
"weight": "3200,,,,3300,,,,3400,,,,3400,,,,3500,,,,3400,,,,",
"heart": "112,120,118,111,,,,,,,112,120,118,111",
"tempType": "2,2,2,3,2,1,2,2,2,1,2,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1",
"temperature": "37.7,37.9,38.5,37.1,37.5,38.5,,,37.5,38.5,37.4,37.8,,37.5,37.6,37.8,,37.9,37.8,37.5,38.6,37.8,,37.9,37.8",
"physicalcool": ",,,37.6,,,,,,,,37.5,,,,,,,,,37.4,,",
"breath": "30,30,R,,35,35,35,35,35,35,35,35,,R,R,35,35,,R,,,35,35,,R",
"labels": "血压(mmHg)|入水量(ml)|出水量(ml)|大便(次)|小便(次)",
"data1": "120/85,121/84,,110/75,",
"data2": "1180ml,,,500ml,,40ml",
"data3": "500ml,,,,500ml,,67ml",
"data4": "2,4,5,3,3,3,2,,2",
"data5": "2,4,5,3,3,3,2,,2",
}
//第二个参数isBaby:true
editor.createVitalSigns(data)
}
</script>

View File

@ -2,17 +2,16 @@
<html>
<head>
<meta charset="utf-8"/>
<link rel="shortcut icon" href="./favicon.svg" type="image/svg+xml"/>
<link rel="stylesheet" href="./vender/jquery/zTreeStyle/zTreeStyle.css" type="text/css">
<script src = "./vender/jquery/jquery.js"></script>
<script src = "./vender/date97/WdatePicker.js"></script>
<script src = "./vender/jquery/jquery.ztree.core.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.core.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.exedit.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.exhide.min.js"></script>
<script src = "./vender/codemirror.js"></script>
<script src = "./vender/fabric.js"></script>
<!-- 局域网环境, 请下载 editor.js替换以下路径 -->
<script src="./vender/editor.js"></script>
<script src="js/editor.js"></script>
<script>
$(function() {
//配置项可以不设置,会使用默认设置

View File

@ -1,54 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="shortcut icon" href="./favicon.svg" type="image/svg+xml"/>
<link rel="stylesheet" href="./vender/jquery/zTreeStyle/zTreeStyle.css" type="text/css">
<script src = "./vender/jquery/jquery.js"></script>
<script src = "./vender/date97/WdatePicker.js"></script>
<script src = "./vender/jquery/jquery.ztree.core.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.exedit.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.exhide.min.js"></script>
<script src = "./vender/codemirror.js"></script>
<script src = "./vender/fabric.js"></script>
<script src="./vender/requirejs/require.js"></script>
<script>
require.config({
baseUrl: 'http://localhost:81/src',
paths: {'editor': 'vender/editor'}
})
require(['public/vender/editor.js'], function () {
//配置项可以不设置,会使用默认设置
let option = {
license:'gaR8jJur/A30SFnd5RHwJT4vNz7zuTe+5UVjd3EztbJyrcUa2ZMAc0WXBJZMJs5D+lpGh+a7p49pT8G1di9alwDwAzKnsz0BTNhHKUsLrU4uMy6I5iQ6l0OMB76w/VP2u1Qf8PCJQiO388mrc8dEcEZjeSVls1O3GedGINg3Od0=',
mode:'form', //默认模式 form:表单模式design:设计模式
pdfUrl:'https://www.x-emr.cn/pdf/post', //pdf生成服务
dictionary: [ //知识库
{name: '体征', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=20', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '症状', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=20', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '卫生信息数据元', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=40', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '电子病历数据集', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=50', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '国家医保标准', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=80', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '省数据平台标准', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=90', itemUrl:'https://www.x-emr.cn/dictitem'},
]
}
//从模块的Editor组件中获取modedoclang属性 (该段代码可删除)
let mode = window.frameElement.getAttribute('mode')
let doc = window.frameElement.getAttribute('doc')
let lang = window.frameElement.getAttribute('lang')
mode? option.mode = mode: null
lang? option.lang = lang: null
//初始化编辑器初始化代码可以放入组件的load事件中
editor.init(option).then(()=>{
doc? editor.loadUrl(doc) : null
})
})
</script>
</head>
<body></body>
</html>

View File

@ -0,0 +1,40 @@
<template>
<component :is="linkType" v-bind="linkProps(to)">
<slot />
</component>
</template>
<script setup lang="ts">
import { computed } from "vue";
defineOptions({
name: "AppLink",
inheritAttrs: false,
});
import { isExternal } from "@/utils/index";
const props = defineProps({
to: {
type: Object,
required: true,
},
});
const isExternalLink = computed(() => {
return isExternal(props.to.path || "");
});
const linkType = computed(() => (isExternalLink.value ? "a" : "router-link"));
const linkProps = (to: any) => {
if (isExternalLink.value) {
return {
href: to.path,
target: "_blank",
rel: "noopener noreferrer",
};
}
return { to: to };
};
</script>

View File

@ -1,18 +0,0 @@
<template>
<!-- 根据实际部署环境修改 editor.html 的路径 -->
<iframe src="/public/editor.html" v-bind="objectOfAttrs"></iframe>
</template>
<script>
export default {
data() {
return {
objectOfAttrs:{
width:'100%',
height:'800vh',
frameborder: 0
}
}
}
}
</script>

View File

@ -74,6 +74,7 @@
</template>
<script setup lang="ts">
import {ref} from 'vue';
import { computed } from "vue";
import { LoginFormData } from "@/api/auth.api";
import { LocationQuery, RouteLocationRaw, useRoute } from "vue-router";
import router from "@/router";

View File

@ -0,0 +1,14 @@
<template>
<!-- 根据实际部署环境修改 editor.html 的路径 -->
<iframe src="/editor.html" v-bind="objectOfAttrs"></iframe>
</template>
<script setup>
const objectOfAttrs = {
width: '100%',
height: '800vh',
frameborder: 0
}
</script>

View File

@ -0,0 +1,9 @@
import type { App } from "vue";
import { hasPerm } from "./permission";
// 全局注册 directive
export function setupDirective(app: App<Element>) {
// 使 v-hasPerm 在所有组件中都可用
app.directive("hasPerm", hasPerm);
}

View File

@ -0,0 +1,64 @@
import type { Directive, DirectiveBinding } from "vue";
import { useUserStore } from "@/store";
/**
* 按钮权限
*/
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const requiredPerms = binding.value;
// 校验传入的权限值是否合法
if (!requiredPerms || (typeof requiredPerms !== "string" && !Array.isArray(requiredPerms))) {
throw new Error(
"需要提供权限标识例如v-has-perm=\"'sys:user:add'\" 或 v-has-perm=\"['sys:user:add', 'sys:user:edit']\""
);
}
const { roles, perms } = useUserStore().userInfo;
// 超级管理员拥有所有权限,如果是”*:*:*”权限标识,则不需要进行权限校验
if (roles.includes("ROOT") || requiredPerms.includes("*:*:*")) {
return;
}
// 检查权限
const hasAuth = Array.isArray(requiredPerms)
? requiredPerms.some((perm) => perms.includes(perm))
: perms.includes(requiredPerms);
// 如果没有权限,移除该元素
if (!hasAuth && el.parentNode) {
el.parentNode.removeChild(el);
}
},
};
/**
* 角色权限指令
*/
export const hasRole: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const requiredRoles = binding.value;
// 校验传入的角色值是否合法
if (!requiredRoles || (typeof requiredRoles !== "string" && !Array.isArray(requiredRoles))) {
throw new Error(
"需要提供角色标识例如v-has-role=\"'ADMIN'\" 或 v-has-role=\"['ADMIN', 'TEST']\""
);
}
const { roles } = useUserStore().userInfo;
// 检查是否有对应角色权限
const hasAuth = Array.isArray(requiredRoles)
? requiredRoles.some((role) => roles.includes(role))
: roles.includes(requiredRoles);
// 如果没有权限,移除元素
if (!hasAuth && el.parentNode) {
el.parentNode.removeChild(el);
}
},
};

View File

@ -1,294 +0,0 @@
import { Client, IMessage, StompSubscription } from "@stomp/stompjs";
import { getAccessToken } from "@/utils/auth";
import { ref, watch } from "vue";
export interface UseStompOptions {
/** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
brokerURL?: string;
/** 用于鉴权的 token不传时使用 getAccessToken() 的返回值 */
token?: string;
/** 重连延迟,单位毫秒,默认为 8000 */
reconnectDelay?: number;
/** 连接超时时间,单位毫秒,默认为 10000 */
connectionTimeout?: number;
/** 是否开启指数退避重连策略 */
useExponentialBackoff?: boolean;
/** 最大重连次数,默认为 5 */
maxReconnectAttempts?: number;
/** 最大重连延迟,单位毫秒,默认为 60000 */
maxReconnectDelay?: number;
/** 是否开启调试日志 */
debug?: boolean;
}
/**
* STOMP WebSocket连接Hook
* 用于管理WebSocket连接的建立、断开、重连和消息订阅
*/
export function useStomp(options: UseStompOptions = {}) {
// 默认值brokerURL 从环境变量中获取token 从 getAccessToken() 获取
const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || "";
// 不再使用defaultToken每次连接时直接获取最新token
const brokerURL = ref(options.brokerURL ?? defaultBrokerURL);
// 不再存储token改为在初始化时获取
const reconnectDelay = options.reconnectDelay ?? 8000;
const connectionTimeout = options.connectionTimeout ?? 10000;
const useExponentialBackoff = options.useExponentialBackoff ?? false;
const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
const maxReconnectDelay = options.maxReconnectDelay ?? 60000;
// 连接状态标记
const isConnected = ref(false);
// 重连尝试次数
const reconnectCount = ref(0);
// 重连计时器
let reconnectTimer: any = null;
// 连接超时计时器
let connectionTimeoutTimer: any = null;
// 存储所有订阅
const subscriptions = new Map<string, StompSubscription>();
// 用于保存 STOMP 客户端的实例
let client = ref<Client | null>(null);
/**
* 初始化 STOMP 客户端
*/
const initializeClient = () => {
if (client.value) {
return;
}
// 每次连接前重新获取最新令牌不依赖之前的token值
const currentToken = getAccessToken();
// 检查令牌是否为空,如果为空则不进行连接
if (!currentToken) {
console.error("WebSocket连接失败授权令牌为空请先登录");
return;
}
// 创建 STOMP 客户端
client.value = new Client({
brokerURL: brokerURL.value,
connectHeaders: {
Authorization: `Bearer ${currentToken}`,
},
debug: options.debug ? console.log : () => {},
reconnectDelay: useExponentialBackoff ? 0 : reconnectDelay, // 使用自定义退避策略时禁用内置重连
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
// 设置连接监听器
client.value.onConnect = () => {
isConnected.value = true;
reconnectCount.value = 0;
clearTimeout(connectionTimeoutTimer);
console.log("WebSocket连接已建立");
};
// 设置断开连接监听器
client.value.onDisconnect = () => {
isConnected.value = false;
console.log("WebSocket连接已断开");
// 如果使用自定义指数退避重连策略,则在这里处理
if (useExponentialBackoff && reconnectCount.value < maxReconnectAttempts) {
handleReconnect();
}
};
// 设置 Web Socket 关闭监听器
client.value.onWebSocketClose = (event) => {
isConnected.value = false;
console.log(`WebSocket已关闭: ${event?.code} ${event?.reason}`);
// 如果是授权问题导致的关闭,尝试重新获取令牌
if (event?.code === 1000 || event?.code === 1006 || event?.code === 1008) {
console.log("可能是授权问题导致连接关闭,尝试重新建立连接");
// 等待一段时间后再尝试重连,避免立即重连
setTimeout(() => {
// 强制重新初始化客户端,获取最新令牌
client.value = null;
// 检查当前是否有有效令牌
const freshToken = getAccessToken();
if (freshToken) {
initializeClient();
connect();
} else {
console.warn("没有有效令牌暂不重连WebSocket");
}
}, 3000);
}
};
// 设置错误监听器
client.value.onStompError = (frame) => {
console.error("STOMP错误:", frame.headers, frame.body);
// 检查是否是授权错误
if (
frame.headers?.message?.includes("Unauthorized") ||
frame.body?.includes("Unauthorized") ||
frame.body?.includes("Token")
) {
console.warn("WebSocket授权错误请检查登录状态");
}
};
};
/**
* 处理重连逻辑
*/
const handleReconnect = () => {
if (reconnectCount.value >= maxReconnectAttempts) {
console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`);
return;
}
reconnectCount.value++;
console.log(`尝试重连(${reconnectCount.value}/${maxReconnectAttempts})...`);
// 使用指数退避策略增加重连间隔
const delay = useExponentialBackoff
? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay)
: reconnectDelay;
// 清除之前的计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
}
// 设置重连计时器
reconnectTimer = setTimeout(() => {
if (!isConnected.value && client.value) {
client.value.activate();
}
}, delay);
};
// 监听 brokerURL 的变化,若地址改变则重新初始化
watch(brokerURL, (newURL, oldURL) => {
if (newURL !== oldURL) {
console.log(`brokerURL changed from ${oldURL} to ${newURL}`);
// 断开当前连接,重新激活客户端
if (client.value && client.value.connected) {
client.value.deactivate();
}
brokerURL.value = newURL;
initializeClient(); // 重新初始化客户端
}
});
// 初始化客户端
initializeClient();
/**
* 激活连接(如果已经连接或正在激活则直接返回)
*/
const connect = () => {
if (!client.value) {
initializeClient();
}
if (client.value && (client.value.connected || client.value.active)) {
return;
}
if (!client.value) {
console.error("STOMP客户端初始化失败");
return;
}
// 设置连接超时
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = setTimeout(() => {
if (!isConnected.value) {
console.warn("WebSocket连接超时");
if (useExponentialBackoff) {
handleReconnect();
}
}
}, connectionTimeout);
client.value.activate();
};
/**
* 订阅指定主题
* @param destination 目标主题地址
* @param callback 接收到消息时的回调函数
* @returns 返回订阅 id用于后续取消订阅
*/
const subscribe = (destination: string, callback: (message: IMessage) => void): string => {
if (!client.value) {
return "";
}
if (!client.value.connected) {
return "";
}
try {
const subscription = client.value.subscribe(destination, callback);
subscriptions.set(subscription.id, subscription);
console.log(`订阅成功: ${destination}, ID: ${subscription.id}`);
return subscription.id;
} catch (error) {
console.error(`订阅失败(${destination}):`, error);
return "";
}
};
/**
* 取消指定订阅
* @param subscriptionId 要取消的订阅 id
*/
const unsubscribe = (subscriptionId: string) => {
const subscription = subscriptions.get(subscriptionId);
if (subscription) {
subscription.unsubscribe();
subscriptions.delete(subscriptionId);
}
};
/**
* 主动断开连接(如果未连接则不执行)
*/
const disconnect = () => {
if (client.value && !(client.value.connected || client.value.active)) {
console.log("Already disconnected, skipping disconnect() call.");
return;
}
// 清除所有计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = null;
}
client.value?.deactivate();
isConnected.value = false;
reconnectCount.value = 0;
};
return {
client,
isConnected,
reconnectCount,
connect,
subscribe,
unsubscribe,
disconnect,
brokerURL,
};
}

View File

@ -1,11 +0,0 @@
/**
* WebSocket相关Hook入口文件
* 统一导出所有WebSocket相关Hook
*/
// 核心基础Hook
export { useStomp } from "./core/useStomp";
// 业务服务Hook
export { useOnlineCount } from "./services/useOnlineCount";
export { useDictSync } from "./services/useDictSync";

View File

@ -1,189 +0,0 @@
import { useDictStoreHook } from "@/store/modules/dict.store";
import { useStomp } from "../core/useStomp";
import { IMessage } from "@stomp/stompjs";
import { ref } from "vue";
// 字典消息类型
export interface DictMessage {
dictCode: string;
timestamp: number;
}
// 字典事件回调类型
export type DictMessageCallback = (message: DictMessage) => void;
// 全局单例实例
let instance: ReturnType<typeof createDictSyncHook> | null = null;
/**
* 创建字典同步Hook
* 负责监听后端字典变更并同步到前端
*/
function createDictSyncHook() {
const dictStore = useDictStoreHook();
// 使用现有的useStomp配置适合字典场景的重连参数
const { isConnected, connect, subscribe, unsubscribe, disconnect } = useStomp({
reconnectDelay: 10000, // 使用更长的重连延迟 - 10秒
connectionTimeout: 15000, // 更长的连接超时时间 - 15秒
useExponentialBackoff: false, // 字典数据不需要指数退避策略
});
// 存储订阅ID
const subscriptionIds = ref<string[]>([]);
// 已订阅的主题
const subscribedTopics = ref<Set<string>>(new Set());
// 消息回调函数列表
const messageCallbacks = ref<DictMessageCallback[]>([]);
/**
* 注册字典消息回调
* @param callback 回调函数
*/
const onDictMessage = (callback: DictMessageCallback) => {
messageCallbacks.value.push(callback);
return () => {
// 返回取消注册的函数
const index = messageCallbacks.value.indexOf(callback);
if (index !== -1) {
messageCallbacks.value.splice(index, 1);
}
};
};
/**
* 初始化WebSocket
*/
const initWebSocket = async () => {
try {
// 连接WebSocket
connect();
// 设置字典订阅
setupDictSubscription();
} catch (error) {
console.error("[WebSocket] 初始化失败:", error);
}
};
/**
* 关闭WebSocket
*/
const closeWebSocket = () => {
// 取消所有订阅
subscriptionIds.value.forEach((id) => {
unsubscribe(id);
});
subscriptionIds.value = [];
subscribedTopics.value.clear();
// 断开连接
disconnect();
};
/**
* 设置字典订阅
*/
const setupDictSubscription = () => {
const topic = "/topic/dict";
// 防止重复订阅
if (subscribedTopics.value.has(topic)) {
console.log(`跳过重复订阅: ${topic}`);
return;
}
console.log(`开始尝试订阅字典主题: ${topic}`);
// 使用简化的重试逻辑依赖useStomp的连接管理
const attemptSubscribe = () => {
if (!isConnected.value) {
console.log("等待WebSocket连接建立...");
// 3秒后再次尝试
setTimeout(attemptSubscribe, 3000);
return;
}
// 检查是否已订阅
if (subscribedTopics.value.has(topic)) {
return;
}
console.log(`连接已建立,开始订阅: ${topic}`);
// 订阅字典更新
const subId = subscribe(topic, (message: IMessage) => {
handleDictEvent(message);
});
if (subId) {
subscriptionIds.value.push(subId);
subscribedTopics.value.add(topic);
console.log(`字典主题订阅成功: ${topic}`);
} else {
console.warn(`字典主题订阅失败: ${topic}`);
}
};
// 开始尝试订阅
attemptSubscribe();
};
/**
* 处理字典事件
* @param message STOMP消息
*/
const handleDictEvent = (message: IMessage) => {
if (!message.body) return;
try {
// 记录接收到的消息
console.log(`收到字典更新消息: ${message.body}`);
// 尝试解析消息
const parsedData = JSON.parse(message.body) as DictMessage;
const dictCode = parsedData.dictCode;
if (!dictCode) return;
// 清除缓存,等待按需加载
dictStore.removeDictItem(dictCode);
console.log(`字典缓存已清除: ${dictCode}`);
// 调用所有注册的回调函数
messageCallbacks.value.forEach((callback) => {
try {
callback(parsedData);
} catch (callbackError) {
console.error("[WebSocket] 回调执行失败:", callbackError);
}
});
// 显示提示消息
console.info(`字典 ${dictCode} 已变更,将在下次使用时自动加载`);
} catch (error) {
console.error("[WebSocket] 解析消息失败:", error);
}
};
return {
isConnected,
initWebSocket,
closeWebSocket,
handleDictEvent,
onDictMessage,
};
}
/**
* 字典同步Hook
* 用于监听后端字典变更并同步到前端
*/
export function useDictSync() {
if (!instance) {
instance = createDictSyncHook();
}
return instance;
}

View File

@ -1,169 +0,0 @@
import { ref, onMounted, onUnmounted, watch } from "vue";
import { useStomp } from "../core/useStomp";
import { ElMessage } from "element-plus";
import { getAccessToken } from "@/utils/auth";
/**
* 在线用户计数Hook
* 用于订阅后端推送的在线用户数量变化
*/
export function useOnlineCount() {
// 在线用户数量
const onlineUserCount = ref(0);
// 最后更新时间戳
const lastUpdateTime = ref(0);
// 连接状态
const isConnected = ref(false);
// 连接正在尝试中
const isConnecting = ref(false);
// 使用Stomp客户端 - 配置使用指数退避策略
const {
connect,
subscribe,
unsubscribe,
disconnect,
isConnected: stompConnected,
} = useStomp({
reconnectDelay: 5000, // 初始重连延迟5秒
maxReconnectAttempts: 3, // 最大重连3次
connectionTimeout: 10000, // 连接超时10秒
useExponentialBackoff: true, // 启用指数退避
});
// 订阅ID
let subscriptionId = "";
// 连接超时计时器
let connectionTimeoutTimer: any = null;
// 监听Stomp连接状态
watch(stompConnected, (connected) => {
if (connected && isConnecting.value) {
isConnected.value = true;
isConnecting.value = false;
// 一旦连接成功,立即订阅主题
subscribeToOnlineCount();
console.log("WebSocket连接成功已订阅在线用户计数主题");
}
});
/**
* 订阅在线用户计数主题
*/
const subscribeToOnlineCount = () => {
if (!stompConnected.value) return;
// 如果已经订阅,先取消订阅
if (subscriptionId) {
unsubscribe(subscriptionId);
}
// 订阅在线用户计数主题
subscriptionId = subscribe("/topic/online-count", (message) => {
try {
const data = message.body;
const jsonData = JSON.parse(data);
const count = typeof jsonData === "number" ? jsonData : jsonData.count;
if (!isNaN(count)) {
onlineUserCount.value = count;
lastUpdateTime.value = Date.now();
}
} catch (error) {
console.error("解析在线用户数量失败:", error);
}
});
};
/**
* 初始化WebSocket连接并订阅在线用户主题
*/
const initWebSocket = () => {
if (isConnecting.value) return;
// 检查是否有可用的令牌
const hasToken = !!getAccessToken();
if (!hasToken) {
console.log("没有检测到有效令牌不尝试WebSocket连接");
return;
}
isConnecting.value = true;
// 连接WebSocket
connect();
// 设置连接超时显示UI提示
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = setTimeout(() => {
if (!isConnected.value) {
console.warn("WebSocket连接超时将自动尝试重连");
ElMessage.warning("正在尝试连接服务器,请稍候...");
// 超时后尝试重新连接
closeWebSocket();
setTimeout(() => {
// 再次检查令牌有效性
if (getAccessToken()) {
initWebSocket();
} else {
console.log("令牌无效,放弃重连");
}
}, 3000);
}
}, 10000); // 较长的UI提示超时
// 监听连接状态变化,连接成功后清除超时计时器
const unwatch = watch(stompConnected, (connected) => {
if (connected) {
clearTimeout(connectionTimeoutTimer);
unwatch();
}
});
};
/**
* 关闭WebSocket连接
*/
const closeWebSocket = () => {
if (subscriptionId) {
unsubscribe(subscriptionId);
subscriptionId = "";
}
// 清除连接超时计时器
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = null;
}
disconnect();
isConnected.value = false;
isConnecting.value = false;
};
// 组件挂载时初始化WebSocket
onMounted(() => {
initWebSocket();
});
// 组件卸载时关闭WebSocket
onUnmounted(() => {
closeWebSocket();
});
return {
onlineUserCount,
lastUpdateTime,
isConnected,
isConnecting,
initWebSocket,
closeWebSocket,
};
}

View File

@ -0,0 +1,36 @@
<template>
<section class="app-main">
<router-view>
<template #default="{ Component, route }">
<transition enter-active-class="animate__animated animate__fadeIn" mode="out-in">
<!-- <keep-alive :include="cachedViews">-->
<component :is="Component" :key="route.path" />
<!-- </keep-alive>-->
</transition>
</template>
</router-view>
</section>
</template>
<script setup lang="ts">
import { useSettingsStore, useTagsViewStore } from "@/store";
// import variables from "@/styles/variables.module.scss";
// 缓存页面集合
const cachedViews = computed(() => useTagsViewStore().cachedViews);
// const appMainHeight = computed(() => {
// if (useSettingsStore().tagsView) {
// return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
// } else {
// return `calc(100vh - ${variables["navbar-height"]})`;
// }
// });
</script>
<style lang="scss" scoped>
.app-main {
position: relative;
overflow-y: auto;
background-color: var(--el-bg-color-page);
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<!-- 菜单图标 -->
<template v-if="icon">
<d-icon v-if="isElIcon" class="el-icon">
<component :is="iconComponent" />
</d-icon>
<div v-else :class="`i-svg:${icon}`" />
</template>
<template v-else>
<div class="i-svg:menu" />
</template>
<!-- 菜单标题 -->
<span v-if="title" class="ml-1">{{ title }}</span>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
icon?: string;
title?: string;
}>();
const isElIcon = computed(() => props.icon?.startsWith("icon"));
const iconComponent = computed(() => props.icon?.replace("icon-", ""));
</script>
<style lang="scss" scoped>
.el-icon {
width: 14px !important;
margin-right: 0 !important;
color: currentcolor;
}
[class^="i-svg:"] {
width: 14px;
height: 14px;
color: currentcolor !important;
}
.hideSidebar {
.el-sub-menu,
.el-menu-item {
.el-icon {
margin-left: 20px;
}
}
[class^="i-svg:"] {
margin-left: 20px;
}
}
</style>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import SidebarMenuItem from "./SidebarMenuItem.vue";
import path from "path-browserify";
import type { RouteRecordRaw } from "vue-router";
import { LayoutMode } from "@/enums/settings/layout.enum";
import { SidebarColor } from "@/enums/settings/theme.enum";
import { useSettingsStore, useAppStore } from "@/store";
import { isExternal } from "@/utils/index";
import router from "@/router";
import variables from "@/styles/variables.module.scss";
import {Form, Menu} from "vue-devui";
const props = defineProps({
data: {
type: Array<RouteRecordRaw>,
default: () => [],
},
basePath: {
type: String,
required: true,
example: "/system",
},
});
const menuRef = ref<InstanceType<typeof Menu>>();
const settingsStore = useSettingsStore();
const appStore = useAppStore();
const currentRoute = useRoute();
// 存储已展开的菜单项索引
const expandedMenuIndexes = ref<string[]>([]);
const handleMenuSelect = (key: any) => {
router.push(`/${key["key"]}`)
}
/**
* 获取完整路径
*
* @param routePath 当前路由的相对路径 /user
* @returns 完整的绝对路径 D://vue3-element-admin/system/user
*/
function resolveFullPath(routePath: string) {
if (isExternal(routePath)) {
return routePath;
}
if (isExternal(props.basePath)) {
return props.basePath;
}
// 解析路径,生成完整的绝对路径
return path.resolve(props.basePath, routePath);
}
</script>
<template>
<d-menu
mode="vertical"
:default-select-keys="currentRoute.path"
width="256px"
@select="handleMenuSelect"
>
<SidebarMenuItem
v-for="route in data"
:key="route.path"
:item="route"
:base-path="resolveFullPath(route.path)"
/>
</d-menu>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,142 @@
<script setup lang="ts">
import {PropType} from "vue";
import SidebarMenuItemTitle from "@/layout/components/SideBar/components/SideBarMenuItemTitle.vue";
import AppLink from "@/components/AppLink/index.vue";
defineOptions({
name: "SidebarMenuItem",
inheritAttrs: false,
});
import path from "path-browserify";
import { RouteRecordRaw } from "vue-router";
import { isExternal } from "@/utils";
const props = defineProps({
/**
* 当前路由对象
*/
item: {
type: Object as PropType<RouteRecordRaw>,
required: true,
},
/**
* 父级完整路径
*/
basePath: {
type: String,
required: true,
},
/**
* 是否为嵌套路由
*/
isNest: {
type: Boolean,
default: false,
},
});
// 可见的唯一子节点
const onlyOneChild = ref();
/**
* 检查是否仅有一个可见子节点
*
* @param children 子路由数组
* @param parent 父级路由
* @returns 是否仅有一个可见子节点
*/
function hasOneShowingChild(children: RouteRecordRaw[] = [], parent: RouteRecordRaw) {
// 过滤出可见子节点
const showingChildren = children.filter((route: RouteRecordRaw) => {
if (!route.meta?.hidden) {
onlyOneChild.value = route;
return true;
}
return false;
});
// 仅有一个节点
if (showingChildren.length === 1) {
return true;
}
// 无子节点时
if (showingChildren.length === 0) {
// 父节点设置为唯一显示节点,并标记为无子节点
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
}
/**
* 获取完整路径,适配外部链接
*
* @param routePath 路由路径
* @returns 绝对路径
*/
function resolvePath(routePath: string) {
if (isExternal(routePath)) return routePath;
if (isExternal(props.basePath)) return props.basePath;
// 拼接父路径和当前路径
return path.resolve(props.basePath, routePath);
}
</script>
<template>
<div v-if="!item.meta || !item.meta.hidden">
<!--叶子节点显示叶子节点或唯一子节点且父节点未配置始终显示 -->
<template
v-if="
// 未配置始终显示,使用唯一子节点替换父节点显示为叶子节点
(hasOneShowingChild(item.children, item) &&
!item.meta?.alwaysShow &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren)) ||
// 即使配置了始终显示,但无子节点,也显示为叶子节点
(item.meta?.alwaysShow && !item.children)
"
>
<AppLink
v-if="onlyOneChild.meta"
:to="{
path: resolvePath(onlyOneChild.path),
query: onlyOneChild.meta.params,
}"
>
<d-menu-item
:key="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
>
<SidebarMenuItemTitle
:icon="onlyOneChild.meta.icon || item.meta?.icon"
:title="onlyOneChild.meta.title"
/>
</d-menu-item>
</AppLink>
</template>
<!--【非叶子节点】显示含多个子节点的父菜单,或始终显示的单子节点 -->
<d-sub-menu v-else :index="resolvePath(item.path)" teleported>
<template #title>
<SidebarMenuItemTitle v-if="item.meta" :icon="item.meta.icon" :title="item.meta.title" />
</template>
<SidebarMenuItem
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
/>
</d-sub-menu>
</div>
</template>
<style scoped>
</style>

View File

@ -1,10 +1,16 @@
<script setup>
import router from "@/router/index.ts";
<script setup lang="ts">
import SidebarMenu from './components/SidebarMenu.vue'
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
import { ref } from 'vue'
const handleMenuSelect = (key) => {
router.push(`/${key["key"]}`)
}
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const layout = computed(() => settingsStore.layout);
const isSidebarCollapsed = computed(() => !appStore.sidebar.opened);
// 当前激活的菜单项
const activeMenu = ref('HomePage')
@ -12,81 +18,83 @@ const activeMenu = ref('HomePage')
</script>
<template>
<d-menu
mode="vertical"
:default-select-keys="['HomePage']"
width="256px"
@select="handleMenuSelect"
>
<!-- 首页 -->
<d-menu-item key="HomePage">
<template #icon><i class="icon-homepage"></i></template>
<span>首页</span>
</d-menu-item>
<!-- 诊疗工作站 -->
<d-sub-menu title="诊疗工作站" key="zl-station">
<template #icon><i class="icon-system"></i></template>
<d-menu-item key="menjizhen-item"><span>门急诊医生站</span></d-menu-item>
<d-menu-item key="zhuyuan-item"><span>住院医生站</span></d-menu-item>
<d-menu-item key="rjssmz-item"><span>日间手术门诊工作</span></d-menu-item>
<d-menu-item key="rjsszy-item"><span>日间手术住院工作</span></d-menu-item>
<d-menu-item key="hzjlgl-item"><span>会诊记录管理</span></d-menu-item>
</d-sub-menu>
<!-- 病历管理 -->
<d-sub-menu title="病历管理" key="bl-manage">
<template #icon><i class="icon-system"></i></template>
<d-menu-item key="cyblbj-item"><span>出院病历编辑</span></d-menu-item>
<d-menu-item key="cybldy-item"><span>出院病历打印</span></d-menu-item>
<d-menu-item key="mzbljs-item"><span>门诊病历检索</span></d-menu-item>
<d-menu-item key="zybljs-item"><span>院病历检索</span></d-menu-item>
<d-menu-item key="blxgjl-item"><span>病历修改记录</span></d-menu-item>
<d-menu-item key="blfcxf-item"><span>病历封存解封</span></d-menu-item>
<d-menu-item key="bljs-item"><span>病历解锁</span></d-menu-item>
</d-sub-menu>
<d-sub-menu title="我的申请" key="my-apply">
<template #icon>
<i class="icon-system"></i>
</template>
<d-menu-item key="my-apply-item">
<span>我的申请</span>
</d-menu-item>
</d-sub-menu>
<d-sub-menu title="我的审批" key="my-approve">
<template #icon>
<i class="icon-system"></i>
</template>
<d-menu-item key="my-approve-item">
<span>我的审批</span>
</d-menu-item>
</d-sub-menu>
<d-sub-menu title="个人质检" key="personal-check">
<template #icon>
<i class="icon-system"></i>
</template>
</d-sub-menu>
<d-sub-menu title="质控管理" key="quality-control">
<template #icon>
<i class="icon-system"></i>
</template>
<d-menu-item key="quality-control-item">
<span>质控管理</span>
</d-menu-item>
</d-sub-menu>
<d-sub-menu title="系统管理" key="system-manage">
<template #icon>
<i class="icon-system"></i>
</template>
<d-menu-item key="user-manage">
<span>用户管理</span>
</d-menu-item>
<d-menu-item key="role-manage">
<span>角色管理</span>
</d-menu-item>
<d-menu-item key="permission-manage">
<span>权限管理</span>
</d-menu-item>
</d-sub-menu>
</d-menu>
<SidebarMenu :data="permissionStore.routes" base-path="" />
<!-- <d-menu-->
<!-- mode="vertical"-->
<!-- :default-select-keys="['HomePage']"-->
<!-- width="256px"-->
<!-- @select="handleMenuSelect"-->
<!-- >-->
<!-- &lt;!&ndash; 首页 &ndash;&gt;-->
<!-- <d-menu-item key="HomePage">-->
<!-- <template #icon><i class="icon-homepage"></i></template>-->
<!-- <span>首页</span>-->
<!-- </d-menu-item>-->
<!-- &lt;!&ndash; 诊疗工作站 &ndash;&gt;-->
<!-- <d-sub-menu title="诊疗工作站" key="zl-station">-->
<!-- <template #icon><i class="icon-system"></i></template>-->
<!-- <d-menu-item key="menjizhen-item"><span>门急诊医生</span></d-menu-item>-->
<!-- <d-menu-item key="zhuyuan-item"><span>住院医生</span></d-menu-item>-->
<!-- <d-menu-item key="rjssmz-item"><span>日间手术门诊工作站</span></d-menu-item>-->
<!-- <d-menu-item key="rjsszy-item"><span>日间手术住院工作站</span></d-menu-item>-->
<!-- <d-menu-item key="hzjlgl-item"><span>会诊记录管理</span></d-menu-item>-->
<!-- </d-sub-menu>-->
<!-- &lt;!&ndash; 病历管理 &ndash;&gt;-->
<!-- <d-sub-menu title="病历管理" key="bl-manage">-->
<!-- <template #icon><i class="icon-system"></i></template>-->
<!-- <d-menu-item key="cyblbj-item"><span>出院病历编辑</span></d-menu-item>-->
<!-- <d-menu-item key="cybldy-item"><span>院病历打印</span></d-menu-item>-->
<!-- <d-menu-item key="mzbljs-item"><span>门诊病历检索</span></d-menu-item>-->
<!-- <d-menu-item key="zybljs-item"><span>住院病历检索</span></d-menu-item>-->
<!-- <d-menu-item key="blxgjl-item"><span>病历修改记录</span></d-menu-item>-->
<!-- <d-menu-item key="blfcxf-item"><span>病历封存解封</span></d-menu-item>-->
<!-- <d-menu-item key="bljs-item"><span>病历解锁</span></d-menu-item>-->
<!-- </d-sub-menu>-->
<!-- <d-sub-menu title="我的申请" key="my-apply">-->
<!-- <template #icon>-->
<!-- <i class="icon-system"></i>-->
<!-- </template>-->
<!-- <d-menu-item key="my-apply-item">-->
<!-- <span>我的申请</span>-->
<!-- </d-menu-item>-->
<!-- </d-sub-menu>-->
<!-- <d-sub-menu title="我的审批" key="my-approve">-->
<!-- <template #icon>-->
<!-- <i class="icon-system"></i>-->
<!-- </template>-->
<!-- <d-menu-item key="my-approve-item">-->
<!-- <span>我的审批</span>-->
<!-- </d-menu-item>-->
<!-- </d-sub-menu>-->
<!-- <d-sub-menu title="个人质检" key="personal-check">-->
<!-- <template #icon>-->
<!-- <i class="icon-system"></i>-->
<!-- </template>-->
<!-- </d-sub-menu>-->
<!-- <d-sub-menu title="质控管理" key="quality-control">-->
<!-- <template #icon>-->
<!-- <i class="icon-system"></i>-->
<!-- </template>-->
<!-- <d-menu-item key="quality-control-item">-->
<!-- <span>质控管理</span>-->
<!-- </d-menu-item>-->
<!-- </d-sub-menu>-->
<!-- <d-sub-menu title="系统管理" key="system-manage">-->
<!-- <template #icon>-->
<!-- <i class="icon-system"></i>-->
<!-- </template>-->
<!-- <d-menu-item key="user-manage">-->
<!-- <span>用户管理</span>-->
<!-- </d-menu-item>-->
<!-- <d-menu-item key="role-manage">-->
<!-- <span>角色管理</span>-->
<!-- </d-menu-item>-->
<!-- <d-menu-item key="permission-manage">-->
<!-- <span>权限管理</span>-->
<!-- </d-menu-item>-->
<!-- </d-sub-menu>-->
<!-- </d-menu>-->
</template>
<style scoped>

View File

@ -1,49 +1,71 @@
<script setup>
import {reactive, ref, shallowRef} from 'vue'
<script setup lang="ts">
import AppMain from "@/layout/components/AppMain/index.vue";
import Sidebar from "@/layout/components/Sidebar/index.vue"; // 根据你的实际路径调整
// // 当前激活的菜单项
// const activeMenu = ref('item1')
import { useAppStore, useSettingsStore, usePermissionStore } from "@/store";
// // 当前显示的组件
// const currentComponent = shallowRef(HomePage)
// 配置
import defaultSettings from "@/settings";
// 组件映射表
// const componentMap = {
// 'item1': HomePage,
// 'menjizhen-item': menjizhenItemView,
// 'notFoundPage': NotFoundPage,
// // 其他菜单项对应的组件...
// }
// 枚举
import { DeviceEnum } from "@/enums/settings/device.enum";
import { LayoutMode } from "@/enums/settings/layout.enum";
// // 处理菜单选择
// const handleMenuSelect = (key) => {
// activeMenu.value = key["key"]
// if (componentMap[key["key"]]) {
// currentComponent.value = componentMap[key["key"]]
// } else {
// console.warn(`未找到菜单项 ${key} 对应的组件`)
// console.warn(key["key"])
// currentComponent.value = NotFoundPage
// }
// }
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const width = useWindowSize().width;
//
const toggleClick = function() {
let menu = document.querySelector('.menu-aside')
// 常量
const WIDTH_DESKTOP = 992; // 响应式布局容器固定宽度(大屏 >=1200px中屏 >=992px小屏 >=768px
menu.style.transition = menu.style.transition || 'all 0.3s ease-in-out'
// 计算属性
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE); // 是否为移动设备
const isSidebarOpen = computed(() => appStore.sidebar.opened); // 侧边栏是否展开
const isShowTagsView = computed(() => settingsStore.tagsView); // 是否显示标签视图
const layout = computed(() => settingsStore.layout); // 当前布局模式left、top、mix
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); // 顶部菜单激活路径
const mixedLayoutLeftRoutes = computed(() => permissionStore.mixedLayoutLeftRoutes); // 混合布局左侧菜单路由
if (menu.style.display == 'none') {
menu.style.display = 'block'
//设置侧边栏的显示和隐藏
const toggleClick = (): void => {
const menu = document.querySelector('.menu-aside') as HTMLElement | null;
if (menu) {
menu.style.transition = menu.style.transition || 'all 0.3s ease-in-out';
if (menu.style.display === 'none') {
menu.style.display = 'block';
} else {
menu.style.display = 'none';
}
} else {
menu.style.display = 'none'
console.warn('Menu element not found');
}
}
};
// 监听路由变化,如果是移动设备且侧边栏展开,则关闭侧边栏
const route = useRoute();
watch(route, () => {
if (isMobile.value && isSidebarOpen.value) {
appStore.closeSideBar();
}
});
// 计算属性:布局样式
const layoutClass = computed(() => ({
hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened,
mobile: appStore.device === DeviceEnum.MOBILE,
[`layout-${settingsStore.layout}`]: true,
}));
</script>
<template>
<d-layout style="width: 100vw; ">
<d-layout class="layout" style="width: 100vw; ">
<!-- 顶栏 -->
<d-header style="position: fixed; width: 100%; z-index: 100;background: #fff;height: 56px;box-shadow: 0 2px 4px rgba(0,0,0,0.04);">
<div class="nav-collapse i icon-nav-collapse me-4 ms-4" @click="toggleClick()"></div>
@ -91,8 +113,6 @@ const toggleClick = function() {
<a href="/logout" style="color:rgba(0,0,0,0.63)">注销</a>
</span>
</div>
</d-header>
<!-- 内层布局侧边 + 主区 -->
<d-layout style=" margin-top: 64px; width: 100%;" > <!-- 添加顶部间距避免被header遮挡 -->
@ -102,7 +122,7 @@ const toggleClick = function() {
z-index: 99;"
class="menu-aside"
>
<Sidebar class="layout__sidebar" />
</d-aside>
<!-- 主显示区 - 添加左侧边距 -->
<d-content
@ -111,7 +131,7 @@ const toggleClick = function() {
overflow-x: hidden; /* 禁止内容溢出 */
">
<!-- <component :is="currentComponent"/>-->
<router-view></router-view>
<AppMain />
</d-content>
</d-layout>
<d-footer style="position: fixed; bottom: 0; width: 100%; z-index: 100;background: #fff;height: 16px;">

View File

@ -1,8 +1,9 @@
import 'vue-devui/style.css';
import '@devui-design/icons/icomoon/devui-icon.css';
import { ThemeServiceInit, infinityTheme } from 'devui-theme';
ThemeServiceInit({ infinityTheme }, 'infinityTheme');
// import { ThemeServiceInit, infinityTheme } from 'devui-theme';
// ThemeServiceInit({ infinityTheme }, 'infinityTheme');
import DevUI from 'vue-devui'
import 'devui-theme/styles-var/devui-var.scss'
@ -12,11 +13,13 @@ import 'bootstrap/dist/js/bootstrap.min.js'
import { createApp } from 'vue';
import App from './App.vue';
import setupPlugins from "@/plugins";
import setupEditor from "@/components/Editor.vue";
import Editor from '@/components/X-EMR/Editor.vue'
const app = createApp(App);
// 病历编辑器
app.component('Editor', setupEditor)
app.use(setupPlugins);
app.use(DevUI)
// 病历编辑器
app.component('Editor', Editor)
app.mount('#app')

View File

@ -66,10 +66,10 @@ const sizeChange = (size) => {
const id = ref('tab1');
//X-EMR-VUE
const patient = ref({})
var editor = null
var editor = null;
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor

View File

@ -1,12 +1,9 @@
import type { App } from "vue";
import VueDevUI from 'vue-devui';
import { setupRouter } from "@/router";
import { setupStore } from "@/store";
import { setupPermission } from "./permission";
export default {
install(app: App<Element>) {
// 路由(router)
@ -15,7 +12,5 @@ export default {
setupStore(app);
// 路由守卫
setupPermission();
app.use(VueDevUI);
},
};

View File

@ -11,21 +11,26 @@ import Page404 from '@/pages/404/notFoundPage.vue'
export const constantRoutes : RouteRecordRaw[] = [
{
path: '/',
component: Home
name: 'main',
component: () => import('@/layout/index.vue'),
children: [
{
path: '/Home',
name: 'Home',
component: Home
},
{
path:'menjizhen-item',
name: 'MenjizhenItem',
component: mjz
},
{
path: 'zhuyuan-item',
name: 'ZhuyuanItem',
component: zy
}
]
},
{
path: '/HomePage',
redirect: '/'
},
{
path: '/menjizhen-item',
component: () => import('@/pages/zl-station/menjizhenItemView.vue')
},
{
path: '/zhuyuan-item',
component: zy
},
{
path: '/login',
name: 'LoginView',

File diff suppressed because it is too large Load Diff

View File

@ -1,100 +0,0 @@
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module "vue" {
export interface GlobalComponents {
AppLink: (typeof import("./../components/AppLink/index.vue"))["default"];
AppMain: (typeof import("./../layout/components/AppMain/index.vue"))["default"];
Breadcrumb: (typeof import("./../components/Breadcrumb/index.vue"))["default"];
CopyButton: (typeof import("./../components/CopyButton/index.vue"))["default"];
CURD: (typeof import("./../components/CURD/index.vue"))["default"];
Dict: (typeof import("./../components/Dict/index.vue"))["default"];
DictLabel: (typeof import("./../components/Dict/DictLabel.vue"))["default"];
ECharts: (typeof import("./../components/ECharts/index.vue"))["default"];
ElBacktop: (typeof import("element-plus/es"))["ElBacktop"];
ElBreadcrumb: (typeof import("element-plus/es"))["ElBreadcrumb"];
ElBreadcrumbItem: (typeof import("element-plus/es"))["ElBreadcrumbItem"];
ElButton: (typeof import("element-plus/es"))["ElButton"];
ElCard: (typeof import("element-plus/es"))["ElCard"];
ElCascader: (typeof import("element-plus/es"))["ElCascader"];
ElCheckbox: (typeof import("element-plus/es"))["ElCheckbox"];
ElCheckboxGroup: (typeof import("element-plus/es"))["ElCheckboxGroup"];
ElCol: (typeof import("element-plus/es"))["ElCol"];
ElColorPicker: (typeof import("element-plus/es"))["ElColorPicker"];
ElConfigProvider: (typeof import("element-plus/es"))["ElConfigProvider"];
ElDatePicker: (typeof import("element-plus/es"))["ElDatePicker"];
ElDialog: (typeof import("element-plus/es"))["ElDialog"];
ElDivider: (typeof import("element-plus/es"))["ElDivider"];
ElDrawer: (typeof import("element-plus/es"))["ElDrawer"];
ElDropdown: (typeof import("element-plus/es"))["ElDropdown"];
ElDropdownItem: (typeof import("element-plus/es"))["ElDropdownItem"];
ElDropdownMenu: (typeof import("element-plus/es"))["ElDropdownMenu"];
ElForm: (typeof import("element-plus/es"))["ElForm"];
ElFormItem: (typeof import("element-plus/es"))["ElFormItem"];
ElIcon: (typeof import("element-plus/es"))["ElIcon"];
ElImage: (typeof import("element-plus/es"))["ElImage"];
ElInput: (typeof import("element-plus/es"))["ElInput"];
ElInputTag: (typeof import("element-plus/es"))["ElInputTag"];
ElInputNumber: (typeof import("element-plus/es"))["ElInputNumber"];
ElLink: (typeof import("element-plus/es"))["ElLink"];
ElMenu: (typeof import("element-plus/es"))["ElMenu"];
ElMenuItem: (typeof import("element-plus/es"))["ElMenuItem"];
ElOption: (typeof import("element-plus/es"))["ElOption"];
ElPagination: (typeof import("element-plus/es"))["ElPagination"];
ElPopover: (typeof import("element-plus/es"))["ElPopover"];
ElRadio: (typeof import("element-plus/es"))["ElRadio"];
ElRadioGroup: (typeof import("element-plus/es"))["ElRadioGroup"];
ElRow: (typeof import("element-plus/es"))["ElRow"];
ElScrollbar: (typeof import("element-plus/es"))["ElScrollbar"];
ElSelect: (typeof import("element-plus/es"))["ElSelect"];
ElStatistic: (typeof import("element-plus/es"))["ElStatistic"];
ElSubMenu: (typeof import("element-plus/es"))["ElSubMenu"];
ElSwitch: (typeof import("element-plus/es"))["ElSwitch"];
ElTable: (typeof import("element-plus/es"))["ElTable"];
ElTableColumn: (typeof import("element-plus/es"))["ElTableColumn"];
ElTag: (typeof import("element-plus/es"))["ElTag"];
ElText: (typeof import("element-plus/es"))["ElText"];
ElTimeSelect: (typeof import("element-plus/es"))["ElTimeSelect"];
ElTooltip: (typeof import("element-plus/es"))["ElTooltip"];
ElTree: (typeof import("element-plus/es"))["ElTree"];
ElTreeSelect: (typeof import("element-plus/es"))["ElTreeSelect"];
ElUpload: (typeof import("element-plus/es"))["ElUpload"];
ElWatermark: (typeof import("element-plus/es"))["ElWatermark"];
ElSkeleton: (typeof import("element-plus/es"))["ElSkeleton"];
FileUpload: (typeof import("./../components/Upload/FileUpload.vue"))["default"];
Form: (typeof import("./../components/CURD/Form.vue"))["default"];
Fullscreen: (typeof import("./../components/Fullscreen/index.vue"))["default"];
GithubCorner: (typeof import("./../components/GithubCorner/index.vue"))["default"];
Hamburger: (typeof import("./../components/Hamburger/index.vue"))["default"];
IconSelect: (typeof import("./../components/IconSelect/index.vue"))["default"];
LangSelect: (typeof import("./../components/LangSelect/index.vue"))["default"];
MenuSearch: (typeof import("./../components/MenuSearch/index.vue"))["default"];
MultiImageUpload: (typeof import("./../components/Upload/MultiImageUpload.vue"))["default"];
NoticeDropdown: (typeof import("./../components/Notice/NoticeDropdown.vue"))["default"];
LayoutSelect: (typeof import("./../layout/components/Settings/components/LayoutSelect.vue"))["default"];
PageContent: (typeof import("./../components/CURD/PageContent.vue"))["default"];
PageModal: (typeof import("./../components/CURD/PageModal.vue"))["default"];
PageSearch: (typeof import("./../components/CURD/PageSearch.vue"))["default"];
Pagination: (typeof import("./../components/Pagination/index.vue"))["default"];
RouterLink: (typeof import("vue-router"))["RouterLink"];
RouterView: (typeof import("vue-router"))["RouterView"];
Settings: (typeof import("./../layout/components/Settings/index.vue"))["default"];
Sidebar: (typeof import("./../layout/components/Sidebar/index.vue"))["default"];
SidebarLogo: (typeof import("./../layout/components/Sidebar/components/SidebarLogo.vue"))["default"];
SidebarMenu: (typeof import("./../layout/components/Sidebar/components/SidebarMenu.vue"))["default"];
SidebarMenuItem: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItem.vue"))["default"];
SidebarMenuItemTitle: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItemTitle.vue"))["default"];
SidebarMixTopMenu: (typeof import("./../layout/components/Sidebar/components/SidebarMixTopMenu.vue"))["default"];
SingleImageUpload: (typeof import("./../components/Upload/SingleImageUpload.vue"))["default"];
SizeSelect: (typeof import("./../components/SizeSelect/index.vue"))["default"];
TableSelect: (typeof import("./../components/TableSelect/index.vue"))["default"];
TagsView: (typeof import("./../layout/components/TagsView/index.vue"))["default"];
WangEditor: (typeof import("./../components/WangEditor/index.vue"))["default"];
}
export interface ComponentCustomProperties {
vLoading: (typeof import("element-plus/es"))["ElLoadingDirective"];
}
}

View File

@ -1,12 +0,0 @@
// translate router.meta.title, be used in breadcrumb sidebar tagsview
import i18n from "@/lang/index";
export function translateRouteTitle(title: any) {
// 判断是否存在国际化配置,如果没有原生返回
const hasKey = i18n.global.te("route." + title);
if (hasKey) {
const translatedTitle = i18n.global.t("route." + title);
return translatedTitle;
}
return title;
}

View File

@ -25,9 +25,21 @@
"allowJs": true,
// 类型声明相关配置
"types": ["node", "vite/client"]
"types": ["node", "vite/client", "unplugin-auto-import", "unplugin-vue-components"]
},
"include": ["mock/**/*.ts", "src/**/*.ts", "src/**/*.vue", "vite.config.ts"],
"include": [
"mock/**/*.ts",
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue",
"vite.config.ts",
"node_modules/unplugin-auto-import/auto-imports.d.ts",
"node_modules/unplugin-auto-import/auto-imports.d.ts",
"node_modules/unplugin-auto-import/auto-imports.d.ts",
"node_modules/unplugin-auto-import/auto-imports.d.ts",
"node_modules/unplugin-auto-import/auto-imports.d.ts",
"node_modules/unplugin-auto-import/auto-imports.d.ts"
],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,14 +1,17 @@
import { fileURLToPath, URL } from 'node:url'
import { type ConfigEnv, loadEnv, defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import { resolve } from "path";
import mockDevServerPlugin from "vite-plugin-mock-dev-server";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { name, version, engines, dependencies, devDependencies } from "./package.json";
import {DevUiResolver} from "unplugin-vue-components/resolvers";
// 平台的名称、版本、运行所需的 node 版本、依赖、构建时间的类型提示
const __APP_INFO__ = {