feat: Spring Boot 3.5.14 全量升级 + 组件升级

核心升级:
- Spring Boot 2.7.18 → 3.5.14
- MyBatis Plus 3.5.5 → 3.5.16 (spring-boot3-starter)
- Springdoc 1.8.0 → 2.8.6 (OpenAPI 3)
- Flowable 6.8.0 → 7.1.0
- Druid 1.2.x → 1.2.28 (boot3-starter)
- kotlin-reflect 1.9.10 → 1.9.25

迁移适配:
- javax → jakarta 命名空间 (620+ 文件)
- Swagger 注解迁移到 OpenAPI 3 (@Tag/@Schema/@Operation/@Parameter)
- Spring Security 6.2 适配 (antMatchers→requestMatchers, EnableMethodSecurity)
- Druid 包名迁移 (boot→boot3)
- Redis 配置路径迁移 (spring.redis→spring.data.redis)
- Flyway 适配 (flyway-database-postgresql)
- Flowable 7.x 适配 (MULE_TASK_IMAGE 移除)

修复:
- spring-boot-maven-plugin 2.5.15→3.5.14 (SPI服务发现失效)
- mybatis-plus-boot-starter 3.5.5→3.5.16 (kotlin-reflect+fastjson2冲突)
- Flowable database-schema-update 启用自动建表

验证: 23/23 测试通过, 1374 API端点正常
This commit is contained in:
2026-06-04 22:39:10 +08:00
parent b8d719429d
commit 1d21661a78
781 changed files with 57907 additions and 1301 deletions

View File

@@ -0,0 +1,286 @@
<template>
<el-drawer
v-model="showSettings"
:with-header="false"
direction="rtl"
size="300px"
>
<div class="setting-drawer-title">
<h3 class="drawer-title">
主题风格设置
</h3>
</div>
<div class="setting-drawer-block-checbox">
<div
class="setting-drawer-block-checbox-item"
@click="handleTheme('theme-dark')"
>
<img
src="@/assets/images/dark.svg"
alt="dark"
>
<div
v-if="sideTheme === 'theme-dark'"
class="setting-drawer-block-checbox-selectIcon"
style="display: block;"
>
<i
aria-label="图标: check"
class="anticon anticon-check"
>
<svg
viewBox="64 64 896 896"
data-icon="check"
width="1em"
height="1em"
:fill="theme"
aria-hidden="true"
focusable="false"
class
>
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
</svg>
</i>
</div>
</div>
<div
class="setting-drawer-block-checbox-item"
@click="handleTheme('theme-light')"
>
<img
src="@/assets/images/light.svg"
alt="light"
>
<div
v-if="sideTheme === 'theme-light'"
class="setting-drawer-block-checbox-selectIcon"
style="display: block;"
>
<i
aria-label="图标: check"
class="anticon anticon-check"
>
<svg
viewBox="64 64 896 896"
data-icon="check"
width="1em"
height="1em"
:fill="theme"
aria-hidden="true"
focusable="false"
class
>
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
</svg>
</i>
</div>
</div>
</div>
<div class="drawer-item">
<span>主题颜色</span>
<span class="comp-style">
<el-color-picker
v-model="theme"
:predefine="predefineColors"
@change="themeChange"
/>
</span>
</div>
<el-divider />
<h3 class="drawer-title">
系统布局配置
</h3>
<div class="drawer-item">
<span>开启 TopNav</span>
<span class="comp-style">
<el-switch
v-model="settingsStore.topNav"
class="drawer-switch"
@change="topNavChange"
/>
</span>
</div>
<div class="drawer-item">
<span>开启 Tags-Views</span>
<span class="comp-style">
<el-switch
v-model="settingsStore.tagsView"
class="drawer-switch"
/>
</span>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<span class="comp-style">
<el-switch
v-model="settingsStore.fixedHeader"
class="drawer-switch"
/>
</span>
</div>
<div class="drawer-item">
<span>显示 Logo</span>
<span class="comp-style">
<el-switch
v-model="settingsStore.sidebarLogo"
class="drawer-switch"
/>
</span>
</div>
<div class="drawer-item">
<span>动态标题</span>
<span class="comp-style">
<el-switch
v-model="settingsStore.dynamicTitle"
class="drawer-switch"
/>
</span>
</div>
<el-divider />
<el-button
type="primary"
plain
icon="DocumentAdd"
@click="saveSetting"
>
保存配置
</el-button>
<el-button
plain
icon="Refresh"
@click="resetSetting"
>
重置配置
</el-button>
</el-drawer>
</template>
<script setup>
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import {handleThemeStyle} from '@/utils/theme'
const { proxy } = getCurrentInstance();
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const showSettings = ref(false);
const theme = ref(settingsStore.theme);
const sideTheme = ref(settingsStore.sideTheme);
const storeSettings = computed(() => settingsStore);
const predefineColors = ref(["#3B82F6", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
/** 是否需要topnav */
function topNavChange(val) {
if (!val) {
appStore.toggleSideBarHide(false);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
}
}
function themeChange(val) {
settingsStore.theme = val;
handleThemeStyle(val);
}
function handleTheme(val) {
settingsStore.sideTheme = val;
sideTheme.value = val;
}
function saveSetting() {
proxy.$modal.loading("正在保存到本地,请稍候...");
let layoutSetting = {
"topNav": storeSettings.value.topNav,
"tagsView": storeSettings.value.tagsView,
"fixedHeader": storeSettings.value.fixedHeader,
"sidebarLogo": storeSettings.value.sidebarLogo,
"dynamicTitle": storeSettings.value.dynamicTitle,
"sideTheme": storeSettings.value.sideTheme,
"theme": storeSettings.value.theme
};
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
setTimeout(proxy.$modal.closeLoading(), 1000)
}
function resetSetting() {
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
localStorage.removeItem("layout-setting")
setTimeout("window.location.reload()", 1000)
}
function openSetting() {
showSettings.value = true;
}
defineExpose({
openSetting,
})
</script>
<style lang='scss' scoped>
.setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
line-height: 22px;
font-weight: bold;
.drawer-title {
font-size: 14px;
}
}
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.setting-drawer-block-checbox-item {
position: relative;
margin-right: 16px;
border-radius: 2px;
cursor: pointer;
img {
width: 48px;
height: 48px;
}
.custom-img {
width: 48px;
height: 38px;
border-radius: 5px;
box-shadow: 1px 1px 2px #898484;
}
.setting-drawer-block-checbox-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
font-weight: 700;
font-size: 14px;
}
}
}
.drawer-item {
color: rgba(0, 0, 0, 0.65);
padding: 12px 0;
font-size: 14px;
.comp-style {
float: right;
margin: -3px 8px 0px 0px;
}
}
</style>

View File

@@ -0,0 +1,88 @@
{
"name": "openhis",
"version": "3.8.10",
"description": "OpenHIS管理系统",
"author": "OpenHIS",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite --mode dev",
"build:prod": "vite build --mode prod",
"build:stage": "vite build --mode staging",
"build:test": "vite build --mode test",
"build:dev": "vite build --mode dev",
"preview": "vite preview",
"build:spug": "vite build --mode spug",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"lint": "eslint . --ext .js,.vue src/",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "playwright show-report"
},
"repository": {
"type": "git",
"url": "giturl"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@vue/shared": "^3.5.25",
"@vueup/vue-quill": "^1.5.1",
"@vueuse/core": "^14.3.0",
"axios": "^1.16.1",
"china-division": "^2.7.0",
"d3": "^7.9.0",
"dayjs": "^1.11.19",
"decimal.js": "^10.5.0",
"echarts": "^5.4.3",
"element-china-area-data": "^6.1.0",
"element-plus": "^2.14.1",
"file-saver": "^2.0.5",
"fuse.js": "^7.0.0",
"html2pdf.js": "^0.10.3",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"json-bigint": "^1.0.0",
"lodash-es": "^4.17.21",
"nprogress": "^0.2.0",
"pinia": "^2.2.0",
"pinyin": "^4.0.0-alpha.2",
"province-city-china": "^8.5.8",
"qrcodejs2": "^0.0.2",
"segmentit": "^2.0.3",
"sortablejs": "^1.15.7",
"v-region": "^3.3.0",
"vue": "^3.5.25",
"vue-area-linkage": "^5.1.0",
"vue-cropper": "^1.1.1",
"vue-plugin-hiprint": "^0.0.60",
"vue-router": "^4.3.0",
"vxe-table": "^4.19.6",
"xe-utils": "^4.0.8"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@types/node": "^25.0.1",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/test-utils": "^2.4.6",
"eslint": "^10.4.1",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-vue": "^10.9.1",
"globals": "^17.5.0",
"happy-dom": "^20.8.3",
"jsdom": "^28.1.0",
"pg": "^8.18.0",
"sass": "^1.100.0",
"typescript": "^5.9.3",
"unplugin-auto-import": "^0.18.6",
"vite": "^6.4.3",
"vite-plugin-compression": "0.5.1",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-mcp": "^0.3.2",
"vitest": "^4.0.18",
"vue-tsc": "^3.3.3"
}
}

View File

@@ -0,0 +1,103 @@
import router from './router'
import {ElMessage} from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import {getToken} from '@/utils/auth'
import {isHttp} from '@/utils/validate'
import {isRelogin} from '@/utils/request'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
// 全局变量,用于控制公告弹窗只显示一次
let hasShownNoticePopup = false
NProgress.configure({ showSpinner: false });
const whiteList = ['/login', '/register'];
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
to.meta.title && useSettingsStore().setTitle(to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
if (useUserStore().roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
useUserStore().getInfo().then(() => {
isRelogin.show = false
usePermissionStore().generateRoutes().then(accessRoutes => {
// 根据roles权限生成可访问的路由表
accessRoutes.forEach(route => {
if (!isHttp(route.path)) {
// 检查是否已经存在同名路由
if (!router.hasRoute(route.name)) {
router.addRoute(route) // 动态添加可访问路由表
}
}
})
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
useUserStore().logOut().then(() => {
ElMessage.error(err)
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
// 登录成功后显示公告弹窗(仅限非登录页面且未显示过)
const token = getToken()
const isLoginPage = router.currentRoute.value.path === '/login'
if (token && !isLoginPage && !hasShownNoticePopup) {
// 延迟显示,确保页面完全加载
setTimeout(() => {
showNoticePopupGlobally()
hasShownNoticePopup = true
}, 1500)
}
})
// 全局函数:显示公告弹窗
function showNoticePopupGlobally() {
try {
// 通过多种方式尝试获取并显示公告弹窗
const layouts = document.querySelectorAll('.app-wrapper')
for (const layout of layouts) {
const noticePopupRef = layout.__vue_app__?.config.globalProperties.$refs?.noticePopupRef
if (noticePopupRef && noticePopupRef.showNotice) {
noticePopupRef.showNotice()
return
}
}
// 如果直接获取失败,尝试通过事件总线方式
window.dispatchEvent(new CustomEvent('show-notice-popup'))
} catch (error) {
console.error('显示公告弹窗失败:', error)
}
}

View File

@@ -0,0 +1,190 @@
const useTagsViewStore = defineStore(
'tags-view',
{
state: () => ({
visitedViews: [],
cachedViews: [],
iframeViews: []
}),
actions: {
addView(view) {
this.addVisitedView(view)
this.addCachedView(view)
},
addIframeView(view) {
if (this.iframeViews.some(v => v.path === view.path)) return
this.iframeViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
},
addVisitedView(view) {
if (this.visitedViews.some(v => v.path === view.path)) return
this.visitedViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
if(this.visitedViews.length==2){
sessionStorage.setItem('visitedViews',this.visitedViews[1].name)
if(this.visitedViews[1].query.supplyBusNo){ // 编辑
sessionStorage.setItem('visitedViewsQuery',this.visitedViews[1].query.supplyBusNo)
}else{
sessionStorage.setItem('visitedViewsQuery',"")
}
}
},
addCachedView(view) {
if (this.cachedViews.includes(view.name)) return
if (!view.meta.noCache) {
this.cachedViews.push(view.name)
}
},
delView(view) {
return new Promise(resolve => {
this.delVisitedView(view)
this.delCachedView(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delVisitedView(view) {
return new Promise(resolve => {
for (const [i, v] of this.visitedViews.entries()) {
if (v.path === view.path) {
this.visitedViews.splice(i, 1)
break
}
}
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
resolve([...this.visitedViews])
})
},
delIframeView(view) {
return new Promise(resolve => {
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
resolve([...this.iframeViews])
})
},
delCachedView(view) {
return new Promise(resolve => {
const index = this.cachedViews.indexOf(view.name)
index > -1 && this.cachedViews.splice(index, 1)
resolve([...this.cachedViews])
})
},
delOthersViews(view) {
return new Promise(resolve => {
this.delOthersVisitedViews(view)
this.delOthersCachedViews(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delOthersVisitedViews(view) {
return new Promise(resolve => {
this.visitedViews = this.visitedViews.filter(v => {
return v.meta.affix || v.path === view.path
})
this.iframeViews = this.iframeViews.filter(item => item.path === view.path)
resolve([...this.visitedViews])
})
},
delOthersCachedViews(view) {
return new Promise(resolve => {
const index = this.cachedViews.indexOf(view.name)
if (index > -1) {
this.cachedViews = this.cachedViews.slice(index, index + 1)
} else {
this.cachedViews = []
}
resolve([...this.cachedViews])
})
},
delAllViews(view) {
return new Promise(resolve => {
this.delAllVisitedViews(view)
this.delAllCachedViews(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delAllVisitedViews(view) {
return new Promise(resolve => {
const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
this.visitedViews = affixTags
this.iframeViews = []
resolve([...this.visitedViews])
})
},
delAllCachedViews(view) {
return new Promise(resolve => {
this.cachedViews = []
resolve([...this.cachedViews])
})
},
updateVisitedView(view) {
for (let v of this.visitedViews) {
if (v.path === view.path) {
v = Object.assign(v, view)
break
}
}
},
delRightTags(view) {
return new Promise(resolve => {
const index = this.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
return
}
this.visitedViews = this.visitedViews.filter((item, idx) => {
if (idx <= index || (item.meta && item.meta.affix)) {
return true
}
const i = this.cachedViews.indexOf(item.name)
if (i > -1) {
this.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = this.iframeViews.findIndex(v => v.path === item.path)
this.iframeViews.splice(fi, 1)
}
return false
})
resolve([...this.visitedViews])
})
},
delLeftTags(view) {
return new Promise(resolve => {
const index = this.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
return
}
this.visitedViews = this.visitedViews.filter((item, idx) => {
if (idx >= index || (item.meta && item.meta.affix)) {
return true
}
const i = this.cachedViews.indexOf(item.name)
if (i > -1) {
this.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = this.iframeViews.findIndex(v => v.path === item.path)
this.iframeViews.splice(fi, 1)
}
return false
})
resolve([...this.visitedViews])
})
}
}
})
export default useTagsViewStore

View File

@@ -0,0 +1,190 @@
const useTagsViewStore = defineStore(
'tags-view',
{
state: () => ({
visitedViews: [],
cachedViews: [],
iframeViews: []
}),
actions: {
addView(view) {
this.addVisitedView(view)
this.addCachedView(view)
},
addIframeView(view) {
if (this.iframeViews.some(v => v.path === view.path)) return
this.iframeViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
},
addVisitedView(view) {
if (this.visitedViews.some(v => v.path === view.path)) return
this.visitedViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
if(this.visitedViews.length==2){
sessionStorage.setItem('visitedViews',this.visitedViews[1].name)
if(this.visitedViews[1].query.supplyBusNo){ // 编辑
sessionStorage.setItem('visitedViewsQuery',this.visitedViews[1].query.supplyBusNo)
}else{
sessionStorage.setItem('visitedViewsQuery',"")
}
}
},
addCachedView(view) {
if (this.cachedViews.includes(view.name)) return
if (!view.meta.noCache) {
this.cachedViews.push(view.name)
}
},
delView(view) {
return new Promise(resolve => {
this.delVisitedView(view)
this.delCachedView(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delVisitedView(view) {
return new Promise(resolve => {
for (const [i, v] of this.visitedViews.entries()) {
if (v.path === view.path) {
this.visitedViews.splice(i, 1)
break
}
}
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
resolve([...this.visitedViews])
})
},
delIframeView(view) {
return new Promise(resolve => {
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
resolve([...this.iframeViews])
})
},
delCachedView(view) {
return new Promise(resolve => {
const index = this.cachedViews.indexOf(view.name)
index > -1 && this.cachedViews.splice(index, 1)
resolve([...this.cachedViews])
})
},
delOthersViews(view) {
return new Promise(resolve => {
this.delOthersVisitedViews(view)
this.delOthersCachedViews(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delOthersVisitedViews(view) {
return new Promise(resolve => {
this.visitedViews = this.visitedViews.filter(v => {
return v.meta.affix || v.path === view.path
})
this.iframeViews = this.iframeViews.filter(item => item.path === view.path)
resolve([...this.visitedViews])
})
},
delOthersCachedViews(view) {
return new Promise(resolve => {
const index = this.cachedViews.indexOf(view.name)
if (index > -1) {
this.cachedViews = this.cachedViews.slice(index, index + 1)
} else {
this.cachedViews = []
}
resolve([...this.cachedViews])
})
},
delAllViews(view) {
return new Promise(resolve => {
this.delAllVisitedViews(view)
this.delAllCachedViews(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delAllVisitedViews(view) {
return new Promise(resolve => {
const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
this.visitedViews = affixTags
this.iframeViews = []
resolve([...this.visitedViews])
})
},
delAllCachedViews(view) {
return new Promise(resolve => {
this.cachedViews = []
resolve([...this.cachedViews])
})
},
updateVisitedView(view) {
for (let v of this.visitedViews) {
if (v.path === view.path) {
v = Object.assign(v, view)
break
}
}
},
delRightTags(view) {
return new Promise(resolve => {
const index = this.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
return
}
this.visitedViews = this.visitedViews.filter((item, idx) => {
if (idx <= index || (item.meta && item.meta.affix)) {
return true
}
const i = this.cachedViews.indexOf(item.name)
if (i > -1) {
this.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = this.iframeViews.findIndex(v => v.path === item.path)
this.iframeViews.splice(fi, 1)
}
return false
})
resolve([...this.visitedViews])
})
},
delLeftTags(view) {
return new Promise(resolve => {
const index = this.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
return
}
this.visitedViews = this.visitedViews.filter((item, idx) => {
if (idx >= index || (item.meta && item.meta.affix)) {
return true
}
const i = this.cachedViews.indexOf(item.name)
if (i > -1) {
this.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = this.iframeViews.findIndex(v => v.path === item.path)
this.iframeViews.splice(fi, 1)
}
return false
})
resolve([...this.visitedViews])
})
}
}
})
export default useTagsViewStore

View File

@@ -0,0 +1,99 @@
import {getInfo, login, logout} from '@/api/login'
import {getToken, removeToken, setToken} from '@/utils/auth'
import defAva from '@/assets/images/user.png'
import {defineStore} from 'pinia'
const useUserStore = defineStore(
'user',
{
state: () => ({
token: getToken(),
id: '',
name: '',
avatar: '',
orgId: '',
practitionerId: '',
orgName: '',
nickName: '',
fixmedinsCode: '', // 医疗机构编码
roles: [],
permissions: [],
tenantId: '',
tenantName: '', // 租户名称
hospitalName:'',
optionMap: {} // 租户配置项Map从sys_tenant_option表读取
}),
actions: {
// 登录
login(userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
const tenantId = userInfo.tenantId
return new Promise((resolve, reject) => {
login(username, password, code, uuid ,tenantId).then(res => {
setToken(res.token)
this.token = res.token
this.tenantId = tenantId
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
getInfo() {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? defAva : import.meta.env.VITE_APP_BASE_API + user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
this.roles = res.roles
this.permissions = res.permissions
} else {
this.roles = ['ROLE_DEFAULT']
}
// console.log('user info:', user);
this.id = user.userId
this.name = user.userName // 用户账号对应数据库的user_name字段如'admin'
this.orgId = user.orgId
this.orgName = user.orgName
this.nickName = user.nickName
this.practitionerId = res.practitionerId
this.fixmedinsCode = res.optionJson.fixmedinsCode
this.avatar = avatar
this.optionMap = res.optionMap || {}
// 优先从optionMap获取配置如果没有则从optionJson获取
this.hospitalName = this.optionMap.hospitalName || res.optionJson.hospitalName || ''
this.tenantName = res.tenantName || ''
resolve(res)
}).catch(error => {
reject(error)
})
})
},
// 退出系统
logOut() {
return new Promise((resolve, reject) => {
logout(this.token).then(() => {
this.token = ''
this.roles = []
this.permissions = []
this.tenantId = ''
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},
removeRoles(){
this.roles = []
}
}
})
export default useUserStore

View File

@@ -0,0 +1,127 @@
/**
* 判断url是否是http或https
* @param {string} path
* @returns {Boolean}
*/
export function isHttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1;
}
/**
* 判断path是否为外链
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor'];
return valid_map.indexOf(str.trim()) >= 0;
}
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg =
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
return reg.test(url);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validLowerCase(str) {
const reg = /^[a-z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUpperCase(str) {
const reg = /^[A-Z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validAlphabets(str) {
const reg = /^[A-Za-z]+$/;
return reg.test(str);
}
/**
* @param {string} email
* @returns {Boolean}
*/
export function validEmail(email) {
const reg =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return reg.test(email);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function isString(str) {
if (typeof str === 'string' || str instanceof String) {
return true;
}
return false;
}
/**
* @param {Array} arg
* @returns {Boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === 'undefined') {
return Object.prototype.toString.call(arg) === '[object Array]';
}
return Array.isArray(arg);
}
// 手机号正则
export function isValidCNPhoneNumber(phone) {
const regex = /^1[3-9]\d{9}$/;
return regex.test(phone);
}
// 身份证号正则
export function isValidCNidCardNumber(idCard) {
const regex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
return regex.test(idCard);
}
// 根据身份证号获取性别和年龄
export function getGenderAndAge(idCard) {
// 确保身份证号码是18位
if (idCard.length !== 18) {
return { error: '身份证号码必须是18位' };
}
// 提取出生年月日
const birthDate = idCard.substr(6, 8); // YYYYMMDD
const year = birthDate.substr(0, 4);
const month = birthDate.substr(4, 2);
const day = birthDate.substr(6, 2);
const dateOfBirth = new Date(`${year}-${month}-${day}`);
// 计算年龄
let age = new Date().getFullYear() - dateOfBirth.getFullYear();
const m = new Date().getMonth() - dateOfBirth.getMonth();
if (m < 0 || (m === 0 && new Date().getDate() < dateOfBirth.getDate())) {
age--;
}
// 提取性别身份证第17位奇数=男, 偶数=女)对应数据库字典 1=男 2=女
const gender = idCard.charAt(16) % 2 === 0 ? 2 : 1;
return { age, gender };
}

View File

@@ -0,0 +1,806 @@
<template>
<div class="app-container">
<el-form
v-show="showSearch"
ref="queryRef"
:model="queryParams"
:inline="true"
label-width="90px"
>
<el-tabs
v-model="activeName"
class="demo-tabs"
@tab-click="handleClick"
>
<el-tab-pane
label="药品定价"
name="1"
>
<el-row :gutter="16">
<!-- <el-col :span="4" style="width: 20%"> -->
<el-form-item
label-width="100"
label="财务类别"
prop="chargeItem"
>
<el-select
v-model="queryParams.typeCode"
placeholder="请选择财务类别"
clearable
:disabled="editShow"
@change="handleQuery"
>
<el-option
v-for="dict in fin_type_code"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<!-- </el-col> -->
<!-- <el-col :span="4" style="width: 20%"> -->
<el-form-item
label-width="100"
label="状态"
prop="chargeItem"
>
<el-select
v-model="queryParams.statusEnum"
placeholder="请选择状态"
clearable
:disabled="editShow"
@change="handleQuery"
>
<el-option
v-for="dict in options"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<!-- </el-col> -->
<!-- <el-col :span="4" style="width: 20%"> -->
<el-form-item
label-width="100"
label="名称"
prop="searchKey"
>
<el-input
v-model="queryParams.searchKey"
placeholder="名称/编码/拼音"
clearable
@keyup.enter="handleQuery"
@blur="handleQuery"
/>
</el-form-item>
<!-- </el-col> -->
</el-row>
<el-table
v-loading="loading"
:data="definitionList"
tooltip-effect="dark"
:show-overflow-tooltip="true"
>
<el-table-column
type="selection"
width="40"
align="center"
fixed="left"
/>
<el-table-column
label="项目名称"
width="200"
prop="chargeName"
align="center"
>
<template #default="scope">
{{ scope.row.chargeName ? scope.row.chargeName : "-" }}
</template>
</el-table-column>
<el-table-column
label="所属科室"
width="200"
prop="orgId_dictText"
align="center"
>
<template #default="scope">
{{ scope.row.orgId_dictText ? scope.row.orgId_dictText : "-" }}
</template>
</el-table-column>
<el-table-column
label="财务类别"
width="200"
prop=" typeCode_dictText"
align="center"
>
<template #default="scope">
{{
scope.row.typeCode_dictText
? scope.row.typeCode_dictText
: "-"
}}
</template>
</el-table-column>
<el-table-column
label="医保类别"
width="200"
prop="ybType_dictText"
align="center"
>
<template #default="scope">
{{
scope.row.ybType_dictText ? scope.row.ybType_dictText : "-"
}}
</template>
</el-table-column>
<el-table-column
label="基础价格"
width="200"
prop="price"
align="center"
>
<template #default="scope">
{{ scope.row.price ? thousandNumber(scope.row.price) : "-" }}
</template>
</el-table-column>
<el-table-column
label="费用明细个数"
width="200"
prop="detailCount"
align="center"
>
<template #default="scope">
<div v-if="scope.row.detailCount != 0">
<el-button
link
type="primary"
@click="handleDetails(scope.row)"
>
{{ thousandNumber(scope.row.detailCount) }}
</el-button>
</div>
<div v-else>
{{ scope.row.detailCount == 0 ? "0" : "-" }}
</div>
</template>
</el-table-column>
<el-table-column
label="状态"
width="200"
prop="statusEnum_enumText"
align="center"
>
<template #default="scope">
{{
scope.row.statusEnum_enumText
? scope.row.statusEnum_enumText
: "-"
}}
</template>
</el-table-column>
<el-table-column
min-width="290"
label="操作"
align="center"
class-name="small-padding fixed-width"
fixed="right"
>
<template #default="scope">
<el-button
link
type="primary"
@click="handleUpdate(scope.row)"
>
修改
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-tab-pane>
<el-tab-pane
label="器具定价"
name="2"
>
<el-row :gutter="16">
<!-- <el-col :span="4" style="width: 20%"> -->
<el-form-item
label-width="100"
label="财务类别"
prop="chargeItem"
>
<el-select
v-model="queryParams.typeCode"
placeholder="请选择财务类别"
clearable
:disabled="editShow"
@change="handleQuery"
>
<el-option
v-for="dict in fin_type_code"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<!-- </el-col> -->
<!-- <el-col :span="4" style="width: 20%"> -->
<el-form-item
label-width="100"
label="状态"
prop="chargeItem"
>
<el-select
v-model="queryParams.statusEnum"
placeholder="请选择状态"
clearable
:disabled="editShow"
@change="handleQuery"
>
<el-option
v-for="dict in options"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<!-- </el-col> -->
<!-- <el-col :span="4" style="width: 20%"> -->
<el-form-item
label-width="100"
label="名称"
prop="searchKey"
>
<el-input
v-model="queryParams.searchKey"
placeholder="名称/编码/拼音"
clearable
@keyup.enter="handleQuery"
@blur="handleQuery"
/>
</el-form-item>
<!-- </el-col> -->
</el-row>
<el-table
v-loading="loading"
:data="definitionList"
tooltip-effect="dark"
:show-overflow-tooltip="true"
>
<el-table-column
type="selection"
width="40"
align="center"
fixed="left"
/>
<el-table-column
label="项目名称"
width="200"
prop="chargeName"
align="center"
>
<template #default="scope">
{{ scope.row.chargeName ? scope.row.chargeName : "-" }}
</template>
</el-table-column>
<el-table-column
label="所属科室"
width="200"
prop="orgId_dictText"
align="center"
>
<template #default="scope">
{{ scope.row.orgId_dictText ? scope.row.orgId_dictText : "-" }}
</template>
</el-table-column>
<el-table-column
label="财务类别"
width="200"
prop=" typeCode_dictText"
align="center"
>
<template #default="scope">
{{
scope.row.typeCode_dictText
? scope.row.typeCode_dictText
: "-"
}}
</template>
</el-table-column>
<el-table-column
label="医保类别"
width="200"
prop="ybType_dictText"
align="center"
>
<template #default="scope">
{{
scope.row.ybType_dictText ? scope.row.ybType_dictText : "-"
}}
</template>
</el-table-column>
<el-table-column
label="基础价格"
width="200"
prop="price"
align="center"
>
<template #default="scope">
{{ scope.row.price ? thousandNumber(scope.row.price) : "-" }}
</template>
</el-table-column>
<el-table-column
label="费用明细个数"
width="200"
prop="detailCount"
align="center"
>
<template #default="scope">
<div v-if="scope.row.detailCount != 0">
<el-button
link
type="primary"
@click="handleDetails(scope.row)"
>
{{ thousandNumber(scope.row.detailCount) }}
</el-button>
</div>
<div v-else>
{{ scope.row.detailCount == 0 ? "0" : "-" }}
</div>
</template>
</el-table-column>
<el-table-column
label="状态"
width="200"
prop="statusEnum_enumText"
align="center"
>
<template #default="scope">
{{
scope.row.statusEnum_enumText
? scope.row.statusEnum_enumText
: "-"
}}
</template>
</el-table-column>
<el-table-column
min-width="290"
label="操作"
align="center"
class-name="small-padding fixed-width"
fixed="right"
>
<template #default="scope">
<el-button
link
type="primary"
@click="handleUpdate(scope.row)"
>
修改
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-tab-pane>
<el-tab-pane
label="活动定价"
name="3"
>
<el-row :gutter="16">
<!-- <el-col :span="4" style="width: 20%"> -->
<el-form-item
label-width="100"
label="财务类别"
prop="chargeItem"
>
<el-select
v-model="queryParams.typeCode"
placeholder="请选择财务类别"
clearable
:disabled="editShow"
@change="handleQuery"
>
<el-option
v-for="dict in fin_type_code"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<!-- </el-col> -->
<!-- <el-col :span="4" style="width: 20%"> -->
<el-form-item
label-width="100"
label="状态"
prop="chargeItem"
>
<el-select
v-model="queryParams.statusEnum"
placeholder="请选择状态"
clearable
:disabled="editShow"
@change="handleQuery"
>
<el-option
v-for="dict in options"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<!-- </el-col> -->
<!-- <el-col :span="4" style="width: 20%"> -->
<el-form-item
label-width="100"
label="名称"
prop="searchKey"
>
<el-input
v-model="queryParams.searchKey"
placeholder="名称/编码/拼音"
clearable
@keyup.enter="handleQuery"
@blur="handleQuery"
/>
</el-form-item>
<!-- </el-col> -->
</el-row>
<el-table
v-loading="loading"
:data="definitionList"
tooltip-effect="dark"
:show-overflow-tooltip="true"
>
<el-table-column
type="selection"
width="40"
align="center"
fixed="left"
/>
<el-table-column
label="项目名称"
width="200"
prop="chargeName"
align="center"
>
<template #default="scope">
{{ scope.row.chargeName ? scope.row.chargeName : "-" }}
</template>
</el-table-column>
<el-table-column
label="所属科室"
width="200"
prop="orgId_dictText"
align="center"
>
<template #default="scope">
{{ scope.row.orgId_dictText ? scope.row.orgId_dictText : "-" }}
</template>
</el-table-column>
<el-table-column
label="财务类别"
width="200"
prop=" typeCode_dictText"
align="center"
>
<template #default="scope">
{{
scope.row.typeCode_dictText
? scope.row.typeCode_dictText
: "-"
}}
</template>
</el-table-column>
<el-table-column
label="医保类别"
width="200"
prop="ybType_dictText"
align="center"
>
<template #default="scope">
{{
scope.row.ybType_dictText ? scope.row.ybType_dictText : "-"
}}
</template>
</el-table-column>
<el-table-column
label="基础价格"
width="200"
prop="price"
align="center"
>
<template #default="scope">
{{ scope.row.price ? thousandNumber(scope.row.price) : "-" }}
</template>
</el-table-column>
<el-table-column
label="费用明细个数"
width="200"
prop="detailCount"
align="center"
>
<template #default="scope">
<div v-if="scope.row.detailCount != 0">
<el-button
link
type="primary"
@click="handleDetails(scope.row)"
>
{{ thousandNumber(scope.row.detailCount) }}
</el-button>
</div>
<div v-else>
{{ scope.row.detailCount == 0 ? "0" : "-" }}
</div>
</template>
</el-table-column>
<el-table-column
label="状态"
width="200"
prop="statusEnum_enumText"
align="center"
>
<template #default="scope">
{{
scope.row.statusEnum_enumText
? scope.row.statusEnum_enumText
: "-"
}}
</template>
</el-table-column>
<el-table-column
min-width="290"
label="操作"
align="center"
class-name="small-padding fixed-width"
fixed="right"
>
<template #default="scope">
<el-button
link
type="primary"
@click="handleUpdate(scope.row)"
>
修改
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-tab-pane>
</el-tabs>
</el-form>
<el-dialog
v-model="openDetails"
:title="title"
width="600px"
append-to-body
>
<el-table
v-loading="detailLoading"
:data="definitionDetailList"
tooltip-effect="dark"
:show-overflow-tooltip="true"
>
<el-table-column
label="条件"
prop="conditionCode_enumText"
align="center"
>
<template #default="scope">
{{
scope.row.conditionCode_enumText
? scope.row.conditionCode_enumText
: "-"
}}
</template>
</el-table-column>
<el-table-column
label="价格"
width="200"
prop="amount"
align="center"
>
<template #default="scope">
{{ scope.row.amount ? scope.row.amount : "-" }}
</template>
</el-table-column>
</el-table>
</el-dialog>
<edit
:title="title"
:open="open"
:form-data="form"
@submit="submitForm"
@update:open="handleOpenChange"
@update:form="handleFormChange"
/>
</div>
</template>
<script setup>
import {getDetail, initOption, listDefinition, updateDefinition,} from "./components/definition";
import Edit from "./components/edit.vue";
import {thousandNumber} from "@/utils/his.js";
const activeName = ref("1");
const showSearch = ref("true");
const loading = ref(true);
const detailLoading = ref(true);
const definitionList = ref([]);
const definitionDetailList = ref([]);
const total = ref(0);
const { proxy } = getCurrentInstance();
const options = ref([]);
const title = ref("");
const open = ref(false);
const openDetails = ref(false);
const { fin_type_code } = proxy.useDict("fin_type_code");
const data = reactive({
form: {},
queryParams: {
search: "",
definitionType: "",
chargeItem: "",
searchKey: "",
pageNo: 1,
pageSize: 10,
},
});
const { queryParams, form } = toRefs(data);
const handleClick = (tab, event) => {
console.log(tab, event);
activeName.value = tab.props.name;
queryParams.value.pageNo = 1;
handleInit();
getList();
};
/** 查询委托单信息列表 */
function getList() {
loading.value = true;
queryParams.value.chargeItemContext = activeName.value;
listDefinition(queryParams.value).then((response) => {
definitionList.value = response.data.records;
total.value = response.data.total;
loading.value = false;
});
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNo = 1;
getList();
}
// 表单重置
function reset() {
form.value = {
id: null,
itemNo: null,
chargeName: null,
totalVolume: null,
unitCode: null,
partPercent: null,
conditionYbCode: null,
price: null,
amount: null,
partMinUnitCode: null,
partConditionPrice: null,
partPrice: null,
description: null,
statusEnum: null,
itemId: null,
};
proxy.resetForm("einfoRef");
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
form.value = row;
open.value = true;
title.value = "修改项目定价";
}
/** 搜索按钮操作 */
function handleInit() {
queryParams.value.definitionType = activeName.value;
initOption(queryParams.value).then((response) => {
options.value = response.data.publicationStatusOptions;
});
}
const handleOpenChange = (value) => {
open.value = value;
};
function handleDetails(row) {
getDetail(row.id).then((res) => {
if (res.code == 200) {
definitionDetailList.value = res.data;
openDetails.value = true;
detailLoading.value = false;
title.value = "明细详情";
}
});
}
const handleFormChange = (newForm) => {
0;
form.value = { ...newForm };
};
/** 提交按钮 */
function submitForm(form) {
updateDefinition(form).then((response) => {
proxy.$modal.msgSuccess("操作成功");
open.value = false;
getList();
});
}
handleInit();
getList();
</script>
<style lang="scss" scoped>
:deep(.demo-tabs > .el-tabs__content) {
color: #6b778c;
font-size: 32px;
font-weight: 600;
}
:deep(.el-input__wrapper) {
height: 32px;
}
:deep(.el-input__inner) {
height: 30px;
}
:deep(.el-tabs__content) {
height: 80vh;
}
.el-select{
width: 150px!important;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #591: 请修复 Bug #591【住院医生站-临床医嘱】长期医嘱点击“停嘱”未弹出时间录入弹窗执行强停,且医嘱列表缺失“停嘱医生/时间”显示
* 自动生成: 2026-06-02 04:03:44
*/
test.describe('🐛 Bug#591', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#591 请修复 Bug #591【住院医生站-临床医嘱】长期医嘱点击“停嘱”未弹出时间录入弹窗执行强停,且医嘱列表缺失“停嘱医生/时间”显示 @bug591 @regression', async ({ page }) => {
await page.goto('/inpatientDoctor');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-591-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #593: 请修复 Bug #593【住院医生工作站-临床医嘱】长期医嘱模块缺失“取消停嘱”功能,误操作停止的医嘱无法恢复,不满足医院临床双向容错业务逻辑
* 自动生成: 2026-06-02 03:30:55
*/
test.describe('🐛 Bug#593', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#593 请修复 Bug #593【住院医生工作站-临床医嘱】长期医嘱模块缺失“取消停嘱”功能,误操作停止的医嘱无法恢复,不满足医院临床双向容错业务逻辑 @bug593 @regression', async ({ page }) => {
await page.goto('/inpatientDoctor');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-593-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #594: 请修复 Bug #594【住院医生工作站-临床医嘱】开立需皮试药物时系统未弹出皮试确认框,且医嘱输入行“皮试”字段置灰只读无法手动编辑
* 自动生成: 2026-06-02 03:28:41
*/
test.describe('🐛 Bug#594', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#594 请修复 Bug #594【住院医生工作站-临床医嘱】开立需皮试药物时系统未弹出皮试确认框,且医嘱输入行“皮试”字段置灰只读无法手动编辑 @bug594 @regression', async ({ page }) => {
await page.goto('/inpatientDoctor');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-594-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #598: 请修复 Bug #598【住院医生工作站-临床医嘱】临床医嘱列表缺少“开嘱医生”列,无法追溯责任医生
* 自动生成: 2026-06-02 03:25:45
*/
test.describe('🐛 Bug#598', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#598 请修复 Bug #598【住院医生工作站-临床医嘱】临床医嘱列表缺少“开嘱医生”列,无法追溯责任医生 @bug598 @regression', async ({ page }) => {
await page.goto('/inpatientDoctor');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-598-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #606: 请修复 Bug #606门诊术中安排-医嘱】预览列表字段显示及逻辑异常(涉及单位、频次、执行时间)
* 自动生成: 2026-06-02 03:20:28
*/
test.describe('🐛 Bug#606', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#606 请修复 Bug #606门诊术中安排-医嘱】预览列表字段显示及逻辑异常(涉及单位、频次、执行时间) @bug606 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-606-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #617: 请修复 Bug #617[住院登记] “费用性质”字段保存逻辑错误(登记选择医保保存后变为全自费)
* 自动生成: 2026-06-02 02:56:17
*/
test.describe('🐛 Bug#617', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#617 请修复 Bug #617[住院登记] “费用性质”字段保存逻辑错误(登记选择医保保存后变为全自费) @bug617 @regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-617-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #633: 请修复 Bug #633【住院管理-住院医生工作站】点击住院医生工作站的前端页面有错误
* 自动生成: 2026-06-02 01:49:44
*/
test.describe('🐛 Bug#633', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#633 请修复 Bug #633【住院管理-住院医生工作站】点击住院医生工作站的前端页面有错误 @bug633 @regression', async ({ page }) => {
await page.goto('/inpatientDoctor');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-633-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #635: 请修复 Bug #635[门诊医生站-检验] “状态设置”区域冗余建议删除,并完善“急诊”标志的保存与列表显示逻辑
* 自动生成: 2026-06-02 01:34:09
*/
test.describe('🐛 Bug#635', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#635 请修复 Bug #635[门诊医生站-检验] “状态设置”区域冗余建议删除,并完善“急诊”标志的保存与列表显示逻辑 @bug635 @regression', async ({ page }) => {
await page.goto('/doctorstation');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-635-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #636: 请修复 Bug #636[门诊医生站-医嘱] 西药医嘱开立界面“执行次数”字段逻辑冗余,建议优化
* 自动生成: 2026-06-02 01:26:33
*/
test.describe('🐛 Bug#636', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#636 请修复 Bug #636[门诊医生站-医嘱] 西药医嘱开立界面“执行次数”字段逻辑冗余,建议优化 @bug636 @regression', async ({ page }) => {
await page.goto('/doctorstation');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-636-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #637: 请修复 Bug #637[住院护士站-体温单] 选中患者后系统上下文不同步,导致无法触发“变更体温单”录入弹窗
* 自动生成: 2026-06-01 22:58:56
*/
test.describe('🐛 Bug#637', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#637 请修复 Bug #637[住院护士站-体温单] 选中患者后系统上下文不同步,导致无法触发“变更体温单”录入弹窗 @bug637 @regression', async ({ page }) => {
await page.goto('/inpatientNurse');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-637-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #638: 请修复 Bug #638[分诊排队管理] 智能候选池数据过滤失效,导致跨科室患者数据错误显示
* 自动生成: 2026-06-01 22:58:47
*/
test.describe('🐛 Bug#638', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#638 请修复 Bug #638[分诊排队管理] 智能候选池数据过滤失效,导致跨科室患者数据错误显示 @bug638 @regression', async ({ page }) => {
await page.goto('/triageandqueuemanage');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-638-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #639: 请修复 Bug #639[门诊医生站-手术申请] 无法检索到已启用的手术项目(如:足跟缺损修复术)
* 自动生成: 2026-06-01 22:55:32
*/
test.describe('🐛 Bug#639', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#639 请修复 Bug #639[门诊医生站-手术申请] 无法检索到已启用的手术项目(如:足跟缺损修复术) @bug639 @regression', async ({ page }) => {
await page.goto('/doctorstation');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-639-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #641: 请修复 Bug #641[住院护士站-医嘱校对] 停嘱医嘱状态同步错误(显示为“已提交”)且缺少停嘱详情列
* 自动生成: 2026-06-02 00:17:03
*/
test.describe('🐛 Bug#641', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#641 请修复 Bug #641[住院护士站-医嘱校对] 停嘱医嘱状态同步错误(显示为“已提交”)且缺少停嘱详情列 @bug641 @regression', async ({ page }) => {
await page.goto('/inpatientNurse');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-641-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #642: 请修复 Bug #642[住院医生站-临床医嘱] 开立医嘱时检索下拉框对齐方式不合理(弹出位置偏移)
* 自动生成: 2026-06-01 23:15:11
*/
test.describe('🐛 Bug#642', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#642 请修复 Bug #642[住院医生站-临床医嘱] 开立医嘱时检索下拉框对齐方式不合理(弹出位置偏移) @bug642 @regression', async ({ page }) => {
await page.goto('/inpatientDoctor');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-642-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #643: 请修复 Bug #643[门诊手术安排-术中医嘱] 删除已生成的临时医嘱提示成功,但点击刷新后医嘱重新出现
* 自动生成: 2026-06-01 23:48:57
*/
test.describe('🐛 Bug#643', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#643 请修复 Bug #643[门诊手术安排-术中医嘱] 删除已生成的临时医嘱提示成功,但点击刷新后医嘱重新出现 @bug643 @regression', async ({ page }) => {
await page.goto('/operatingroom');
await page.waitForLoadState('networkidle');
// 检查页面正常加载(非登录页)
await expect(page).not.toHaveURL(/.*login.*/);
// 检查无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 页面基本可交互
const body = page.locator('body');
await expect(body).toBeVisible();
// 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-643-result.png',
fullPage: true
});
// 无 JS 错误
expect(jsErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,39 @@
import { test } from '@playwright/test';
test('debug console', async ({ page }) => {
const errors: string[] = [];
const requests: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error' || msg.type() === 'warn') {
errors.push(`[${msg.type()}] ${msg.text()}`);
}
});
page.on('response', async resp => {
if (resp.status() >= 400) {
requests.push(`${resp.status()} ${resp.url()}`);
}
});
await page.goto('http://localhost:81/');
const loginResp = await page.request.post('http://localhost:18082/openhis/login', {
data: { username: 'doctor1', password: '123456', tenantId: '1', code: '', uuid: '' }
});
const { token } = await loginResp.json();
await page.context().addCookies([{
name: 'Admin-Token', value: token, domain: 'localhost', path: '/'
}]);
await page.goto('http://localhost:81/clinicManagement/doctorStation');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(8000);
console.log('=== Console errors/warnings ===');
errors.forEach(e => console.log(e));
console.log('=== Failed requests ===');
requests.forEach(r => console.log(r));
console.log('=== App innerHTML length ===');
const len = await page.evaluate(() => document.querySelector('#app')?.innerHTML.length || 0);
console.log(len);
});

View File

@@ -0,0 +1,55 @@
import { test, expect } from '@playwright/test';
test('debug login', async ({ page }) => {
// 1. 先访问页面获取 cookie
await page.goto('http://localhost:81/');
await page.waitForLoadState('networkidle');
// 2. 调用登录 API
const loginResp = await page.request.post('http://localhost:18082/openhis/login', {
data: {
username: 'doctor1',
password: '123456',
tenantId: '1',
code: '',
uuid: ''
}
});
const loginData = await loginResp.json();
console.log('Login response:', JSON.stringify(loginData));
// 3. 设置 token
const token = loginData.token;
await page.evaluate((t) => {
localStorage.setItem('Admin-Token', t);
}, token);
// 4. 检查 token 是否设置成功
const savedToken = await page.evaluate(() => localStorage.getItem('Admin-Token'));
console.log('Saved token:', savedToken ? savedToken.substring(0, 20) + '...' : 'null');
// 5. 导航到门诊医生站
await page.goto('http://localhost:81/clinicManagement/doctorStation');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(5000);
// 6. 检查页面内容
const title = await page.title();
console.log('Page title:', title);
console.log('Page URL:', page.url());
// 7. 获取页面 HTML 结构
const bodyHTML = await page.evaluate(() => document.body.innerHTML.substring(0, 2000));
console.log('Body HTML (first 2000 chars):', bodyHTML);
// 8. 检查是否有错误
const errors = await page.evaluate(() => {
const errorElements = document.querySelectorAll('.el-message--error, .error, [class*="error"]');
return Array.from(errorElements).map(e => e.textContent?.trim()).filter(Boolean);
});
console.log('Errors found:', errors);
// 截图
await page.screenshot({ path: '/tmp/debug-login.png' });
console.log('Screenshot saved to /tmp/debug-login.png');
});

View File

@@ -0,0 +1,41 @@
import { test, expect } from '@playwright/test';
test('debug page load', async ({ page }) => {
// 登录
await page.goto('http://localhost:81/');
const loginResp = await page.request.post('http://localhost:18082/openhis/login', {
data: { username: 'doctor1', password: '123456', tenantId: '1', code: '', uuid: '' }
});
const loginData = await loginResp.json();
await page.context().addCookies([{
name: 'Admin-Token',
value: loginData.token,
domain: 'localhost',
path: '/'
}]);
// 导航到门诊医生站
await page.goto('http://localhost:81/clinicManagement/doctorStation');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(8000); // 等待更长时间
// 获取页面 HTML
const html = await page.content();
console.log('=== HTML 长度:', html.length);
console.log('=== 前 3000 字符 ===');
console.log(html.substring(0, 3000));
console.log('=== 检查 Vue app ===');
const appExists = await page.evaluate(() => !!document.querySelector('#app'));
console.log('App exists:', appExists);
const appChildren = await page.evaluate(() => document.querySelector('#app')?.children.length || 0);
console.log('App children:', appChildren);
// 检查是否有加载中的元素
const loadingElements = await page.evaluate(() => {
const els = document.querySelectorAll('.loading, .el-loading, [class*="loading"]');
return els.length;
});
console.log('Loading elements:', loadingElements);
await page.screenshot({ path: '/tmp/debug-page.png', fullPage: true });
});