feat(login): 添加租户名称获取功能并优化前端布局

- 在登录控制器中注入租户服务并获取租户名称信息
- 添加租户名称到登录响应结果中
- 更新样式变量定义侧边栏宽度和Logo高度
- 重构公告面板组件统一公告通知显示逻辑
- 简化公告类型图标和样式映射关系
- 更新侧边栏为垂直菜单布局并添加折叠功能
- 优化Logo组件显示租户名称和系统标题
- 调整导航栏布局结构和响应式样式
- 重构主应用容器样式和标签页显示逻辑
This commit is contained in:
2025-12-31 10:28:52 +08:00
parent 10e738edd9
commit 4d4828ea71
54 changed files with 3510 additions and 754 deletions

View File

@@ -13,3 +13,4 @@ onMounted(() => {
})
})
</script>

View File

@@ -0,0 +1,80 @@
import request from '@/utils/request'
/**
* 分页查询手术列表
* @param queryParams 查询参数
* @returns {AxiosPromise}
*/
export function getSurgeryPage(queryParams) {
return request({
url: '/clinical-manage/surgery/surgery-page',
method: 'get',
params: queryParams
})
}
/**
* 根据ID查询手术详情
* @param id 手术ID
* @returns {AxiosPromise}
*/
export function getSurgeryDetail(id) {
return request({
url: '/clinical-manage/surgery/surgery-detail',
method: 'get',
params: { id }
})
}
/**
* 新增手术信息
* @param data 手术信息
* @returns {AxiosPromise}
*/
export function addSurgery(data) {
return request({
url: '/clinical-manage/surgery/surgery',
method: 'post',
data: data
})
}
/**
* 修改手术信息
* @param data 手术信息
* @returns {AxiosPromise}
*/
export function updateSurgery(data) {
return request({
url: '/clinical-manage/surgery/surgery',
method: 'put',
data: data
})
}
/**
* 删除手术信息
* @param id 手术ID
* @returns {AxiosPromise}
*/
export function deleteSurgery(id) {
return request({
url: '/clinical-manage/surgery/surgery',
method: 'delete',
params: { id }
})
}
/**
* 更新手术状态
* @param id 手术ID
* @param statusEnum 状态
* @returns {AxiosPromise}
*/
export function updateSurgeryStatus(id, statusEnum) {
return request({
url: '/clinical-manage/surgery/surgery-status',
method: 'put',
params: { id, statusEnum }
})
}

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M704 192H320c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V256c0-35.2-28.8-64-64-64z m-64 576H384V320h256v448z" fill="currentColor"/>
<path d="M416 384h192v128H416z m0 192h128v64H416z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M768 192H256c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h512c35.2 0 64-28.8 64-64V256c0-35.2-28.8-64-64-64z m-64 576H320V320h384v448z" fill="currentColor"/>
<path d="M384 384h256v64H384z m0 128h192v64H384z m0 128h128v64H384z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M640 128H384c-35.2 0-64 28.8-64 64v640c0 35.2 28.8 64 64 64h256c35.2 0 64-28.8 64-64V192c0-35.2-28.8-64-64-64z m-64 640H448V256h128v512z" fill="currentColor"/>
<path d="M480 320h64v128h-64z m-64 192h192v64H416z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M832 192H192c-35.2 0-64 28.8-64 64v512c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64V256c0-35.2-28.8-64-64-64z m-64 512H256V320h512v384z" fill="currentColor"/>
<path d="M320 384h128v128H320z m256 0h128v128H576z m-256 192h128v64H320z m256 0h128v64H576z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 64c-106.04 0-192 85.96-192 192 0 106.04 85.96 192 192 192 106.04 0 192-85.96 192-192 0-106.04-85.96-192-192-192z m0 320c-70.58 0-128-57.42-128-128 0-70.58 57.42-128 128-128 70.58 0 128 57.42 128 128 0 70.58-57.42 128-128 128z" fill="currentColor"/>
<path d="M736 640h-64c-17.68 0-32 14.32-32 32v256c0 17.68 14.32 32 32 32h64c17.68 0 32-14.32 32-32v-256c0-17.68-14.32-32-32-32z m-192 0h-64c-17.68 0-32 14.32-32 32v256c0 17.68 14.32 32 32 32h64c17.68 0 32-14.32 32-32v-256c0-17.68-14.32-32-32-32z m-192 0h-64c-17.68 0-32 14.32-32 32v256c0 17.68 14.32 32 32 32h64c17.68 0 32-14.32 32-32v-256c0-17.68-14.32-32-32-32z m416-128c-17.68 0-32-14.32-32-32v-64H288v64c0 17.68-14.32 32-32 32h-64c-17.68 0-32-14.32-32-32V352c0-17.68 14.32-32 32-32h544c17.68 0 32 14.32 32 32v128c0 17.68-14.32 32-32 32z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 916 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 64C262.4 64 64 262.4 64 512s198.4 448 448 448 448-198.4 448-448S761.6 64 512 64z m0 768c-176.8 0-320-143.2-320-320s143.2-320 320-320 320 143.2 320 320-143.2 320-320 320z" fill="currentColor"/>
<path d="M480 352h64v192h-64z m0 256h64v64h-64z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 64L128 256v256c0 224 144 416 384 448 240-32 384-224 384-448V256L512 64z m0 832c-176-32-288-192-288-384V288l288-160 288 160v224c0 192-112 352-288 384z" fill="currentColor"/>
<path d="M480 320h64v256h-64z m0 320h64v64h-64z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 347 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M832 256H192c-35.2 0-64 28.8-64 64v512c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64V320c0-35.2-28.8-64-64-64z m-64 512H256V384h512v384z" fill="currentColor"/>
<path d="M320 416h128v64H320z m256 0h128v64H576z m-256 128h128v64H320z m256 0h128v64H576z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M768 192H256c-35.2 0-64 28.8-64 64v512c0 35.2 28.8 64 64 64h512c35.2 0 64-28.8 64-64V256c0-35.2-28.8-64-64-64z m-64 512H320V320h384v384z" fill="currentColor"/>
<path d="M384 416h256v64H384z m0 128h192v64H384z m0 128h128v64H384z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M896 160H768v-64c0-35.2-28.8-64-64-64s-64 28.8-64 64v64H384v-64c0-35.2-28.8-64-64-64s-64 28.8-64 64v64H128c-35.2 0-64 28.8-64 64v640c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64z m0 640H128V288h768v512z" fill="currentColor"/>
<path d="M640 448h-128c-17.6 0-32 14.4-32 32v128c0 17.6 14.4 32 32 32h128c17.6 0 32-14.4 32-32V480c0-17.6-14.4-32-32-32z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 494 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 64c-88.4 0-160 71.6-160 160 0 88.4 71.6 160 160 160 88.4 0 160-71.6 160-160 0-88.4-71.6-160-160-160z m0 256c-52.94 0-96-43.06-96-96 0-52.94 43.06-96 96-96 52.94 0 96 43.06 96 96 0 52.94-43.06 96-96 96z" fill="currentColor"/>
<path d="M800 640h-96v-64c0-17.68-14.32-32-32-32h-320c-17.68 0-32 14.32-32 32v64h-96c-17.68 0-32 14.32-32 32v256c0 17.68 14.32 32 32 32h576c17.68 0 32-14.32 32-32v-256c0-17.68-14.32-32-32-32z m-224 0h-128v-64h128v64z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 128c-70.69 0-128 57.31-128 128 0 70.69 57.31 128 128 70.69 0 128-57.31 128-128 0-70.69-57.31-128-128-128z m0 192c-35.34 0-64-28.66-64-64 0-35.34 28.66-64 64-64 35.34 0 64 28.66 64 64 0 35.34-28.66 64-64 64z" fill="currentColor"/>
<path d="M832 832c0-88.36-28.7-172.6-80.4-242.4C702 521 608 480 512 480s-190 41-239.6 109.6C221 659.4 192 743.64 192 832v64h640v-64z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 489 B

View File

@@ -0,0 +1,5 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M736 288H288c-17.68 0-32 14.32-32 32v416c0 17.68 14.32 32 32 32h448c17.68 0 32-14.32 32-32v-416c0-17.68-14.32-32-32-32z m-64 384H352V384h320v288z" fill="currentColor"/>
<path d="M480 192h64v224h-64z m-128 0h64v224h-64z m256 0h64v224h-64z" fill="currentColor"/>
<path d="M544 64h-64c-17.68 0-32 14.32-32 32v64h-64c-17.68 0-32 14.32-32 32v64h320v-64c0-17.68-14.32-32-32-32h-64v-64c0-17.68-14.32-32-32-32z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M704 128H320c-35.2 0-64 28.8-64 64v640c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V192c0-35.2-28.8-64-64-64z m-64 640H384V256h256v512z" fill="currentColor"/>
<path d="M416 320h192v64H416z m0 128h192v64H416z m0 128h128v64H416z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M704 64H320c-35.2 0-64 28.8-64 64v768c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V128c0-35.2-28.8-64-64-64z m-64 768H384V192h256v640z" fill="currentColor"/>
<path d="M416 256h192v64H416z m0 128h192v64H416z m0 128h128v64H416z m0 128h128v64H416z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M736 256H288c-35.2 0-64 28.8-64 64v416c0 35.2 28.8 64 64 64h448c35.2 0 32-28.8 32-64V320c0-35.2-3.2-64-32-64z m-64 384H352V384h320v256z" fill="currentColor"/>
<path d="M384 416h256v64H384z m0 128h192v64H384z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M896 192H128c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V256c0-35.2-28.8-64-64-64z m-64 576H192V320h640v448z" fill="currentColor"/>
<path d="M320 416h128v64h-128z m256 0h128v64h-128z m-256 128h128v64h-128z m256 0h128v64h-128z m-256 128h128v64h-128z m256 0h128v64h-128z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -42,7 +42,12 @@ $--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;
$base-sidebar-width: 100%;
// 侧边栏宽度(垂直菜单)
$sideBarWidth: 200px;
$base-sidebar-width: $sideBarWidth;
// Logo高度
$logoHeight: 50px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass

View File

@@ -1,49 +1,40 @@
<template>
<el-drawer v-model="noticeVisible" title="公告" direction="rtl" size="400px" destroy-on-close>
<el-tabs v-model="activeTab">
<el-tab-pane label="公告" name="notice">
<el-empty v-if="noticeList.length === 0" description="暂无公告" />
<div v-else class="notice-list">
<div v-for="item in noticeList" :key="item.noticeId" class="notice-item" :class="{ 'is-read': isRead(item.noticeId) }" @click="viewDetail(item)">
<div class="notice-title">
<span v-if="!isRead(item.noticeId)" class="unread-dot"></span>
{{ item.noticeTitle }}
</div>
<div class="notice-info">
<span class="notice-type">
<dict-tag :options="sys_notice_type" :value="item.noticeType" />
</span>
<span class="notice-time">{{ parseTime(item.createTime, '{y}-{m}-{d}') }}</span>
</div>
</div>
<el-drawer v-model="noticeVisible" title="公告/通知" direction="rtl" size="400px" destroy-on-close>
<el-empty v-if="noticeList.length === 0" description="暂无公告/通知" />
<div v-else class="notice-list">
<div v-for="item in noticeList" :key="item.noticeId" class="notice-item" :class="{ 'is-read': isRead(item.noticeId), 'unread': !isRead(item.noticeId) }" @click="viewDetail(item)">
<div class="notice-title">
<span v-if="!isRead(item.noticeId)" class="unread-dot"></span>
{{ item.noticeTitle }}
</div>
</el-tab-pane>
<el-tab-pane label="通知" name="notification">
<el-empty v-if="notificationList.length === 0" description="暂无通知" />
<div v-else class="notice-list">
<div v-for="item in notificationList" :key="item.noticeId" class="notice-item" :class="{ 'is-read': isRead(item.noticeId) }" @click="viewDetail(item)">
<div class="notice-title">
<span v-if="!isRead(item.noticeId)" class="unread-dot"></span>
{{ item.noticeTitle }}
</div>
<div class="notice-info">
<span class="notice-type">
<dict-tag :options="sys_notice_type" :value="item.noticeType" />
</span>
<span class="notice-time">{{ parseTime(item.createTime, '{y}-{m}-{d}') }}</span>
</div>
</div>
<div class="notice-info">
<span class="notice-type">
<el-tag :type="getNoticeTypeTagType(item.noticeType)" size="small">
{{ getNoticeTypeText(item.noticeType) }}
</el-tag>
</span>
<span class="notice-priority" v-if="item.priority">
<el-tag :type="getPriorityTagType(item.priority)" size="small" effect="plain">
{{ getPriorityText(item.priority) }}
</el-tag>
</span>
<span class="notice-time">{{ parseTime(item.createTime, '{y}-{m}-{d}') }}</span>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
<!-- 公告/通知详情对话框 -->
<el-dialog v-model="detailVisible" :title="currentNotice.noticeTitle" width="800px" append-to-body>
<div class="notice-detail">
<div class="detail-header">
<span class="detail-type">
<dict-tag :options="sys_notice_type" :value="currentNotice.noticeType" />
</span>
<div class="detail-type">
<el-tag :type="getNoticeTypeTagType(currentNotice.noticeType)" size="small">
{{ getNoticeTypeText(currentNotice.noticeType) }}
</el-tag>
<el-tag :type="getPriorityTagType(currentNotice.priority)" size="small" effect="plain" style="margin-left: 8px;">
{{ getPriorityText(currentNotice.priority) }}
</el-tag>
</div>
<span class="detail-time">{{ parseTime(currentNotice.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</div>
<div class="detail-content" v-html="currentNotice.noticeContent"></div>
@@ -57,19 +48,13 @@
<script setup>
import {ref} from 'vue'
import {getPublicNoticeList, getReadNoticeIds, getUserNotices, markAsRead} from '@/api/system/notice'
import useUserStore from '@/store/modules/user'
import {getReadNoticeIds, getUserNotices, markAsRead} from '@/api/system/notice'
const { proxy } = getCurrentInstance()
const { sys_notice_type } = proxy.useDict('sys_notice_type')
const emit = defineEmits(['updateUnreadCount'])
const userStore = useUserStore()
const noticeVisible = ref(false)
const detailVisible = ref(false)
const activeTab = ref('notice')
const noticeList = ref([])
const notificationList = ref([])
const currentNotice = ref({})
const readNoticeIds = ref(new Set())
@@ -101,42 +86,82 @@ function loadReadNoticeIds() {
})
}
// 排序:未读的排前面,已读的排后面,同类型按时间
// 排序:未读的排前面,已读的排后面,同状态按优先级和时间
function sortNoticeList(list) {
return list.sort((a, b) => {
const aRead = isRead(a.noticeId)
const bRead = isRead(b.noticeId)
// 未读排在前面
if (aRead !== bRead) {
return aRead ? 1 : -1
}
// 同类型按创建时间倒序(最新的在前
// 同状态按优先级排序1高 2中 3低
const priorityA = a.priority || '3'
const priorityB = b.priority || '3'
if (priorityA !== priorityB) {
return priorityA.localeCompare(priorityB)
}
// 同优先级按创建时间倒序(最新的在前)
return new Date(b.createTime) - new Date(a.createTime)
})
}
// 加载公告和通知
// 加载公告和通知(统一从一个接口获取)
function loadNotices() {
// 加载公告列表
getPublicNoticeList({ pageNum: 1, pageSize: 10 }).then(response => {
let list = response.rows || response.data || []
noticeList.value = sortNoticeList(list)
})
// 加载通知列表
getUserNotices().then(response => {
let list = response.data || []
notificationList.value = sortNoticeList(list)
noticeList.value = sortNoticeList(list)
})
}
// 获取公告类型标签类型
// noticeType: 1=通知, 2=公告
function getNoticeTypeTagType(type) {
const typeMap = {
'1': 'primary', // 通知
'2': 'success' // 公告
}
return typeMap[type] || 'info'
}
// 获取公告类型文本
function getNoticeTypeText(type) {
const textMap = {
'1': '通知',
'2': '公告'
}
return textMap[type] || '公告'
}
// 获取优先级标签类型
// priority: 1=高, 2=中, 3=低
function getPriorityTagType(priority) {
const typeMap = {
'1': 'danger', // 高优先级 - 红色
'2': 'warning', // 中优先级 - 橙色
'3': 'info' // 低优先级 - 灰色
}
return typeMap[priority] || 'info'
}
// 获取优先级文本
function getPriorityText(priority) {
const textMap = {
'1': '高',
'2': '中',
'3': '低'
}
return textMap[priority] || '中'
}
// 查看详情
function viewDetail(item) {
currentNotice.value = item
detailVisible.value = true
// 标记为已读
if (!readNoticeIds.value.has(item.noticeId)) {
markAsRead(item.noticeId).then(() => {
@@ -192,6 +217,10 @@ defineExpose({
color: #909399;
}
}
&.unread {
background-color: #fffbe6;
}
}
.notice-title {
@@ -219,6 +248,12 @@ defineExpose({
align-items: center;
font-size: 12px;
color: #909399;
gap: 8px;
.notice-type,
.notice-priority {
flex-shrink: 0;
}
}
.detail-header {
@@ -228,6 +263,11 @@ defineExpose({
padding-bottom: 16px;
border-bottom: 1px solid #EBEEF5;
margin-bottom: 16px;
.detail-type {
display: flex;
align-items: center;
}
}
.detail-time {

View File

@@ -111,12 +111,11 @@ const unreadCount = computed(() => {
})
// 获取公告类型图标
// noticeType: 1=通知, 2=公告
const getNoticeTypeIcon = (type) => {
const iconMap = {
'1': Bell, // 通知
'2': Warning, // 紧急
'3': InfoFilled, // 信息
'4': CircleCheck // 成功
'2': InfoFilled // 公告
}
return iconMap[type] || InfoFilled
}
@@ -152,36 +151,33 @@ const getPriorityTagType = (priority) => {
}
// 获取公告类型样式类
// noticeType: 1=通知, 2=公告
const getNoticeTypeClass = (type) => {
const classMap = {
'1': 'type-notice',
'2': 'type-urgent',
'3': 'type-info',
'4': 'type-success'
'2': 'type-announcement'
}
return classMap[type] || 'type-info'
return classMap[type] || 'type-announcement'
}
// 获取公告类型标签类型
// noticeType: 1=通知, 2=公告
const getNoticeTypeTagType = (type) => {
const typeMap = {
'1': '',
'2': 'danger',
'3': 'info',
'4': 'success'
'1': 'primary',
'2': 'success'
}
return typeMap[type] || ''
return typeMap[type] || 'info'
}
// 获取公告类型文本
// noticeType: 1=通知, 2=公告
const getNoticeTypeText = (type) => {
const textMap = {
'1': '通知',
'2': '紧急',
'3': '信息',
'4': '成功'
'2': '公告'
}
return textMap[type] || '信息'
return textMap[type] || '公告'
}
// 格式化相对时间
@@ -390,21 +386,11 @@ defineExpose({
background: #e6f7ff;
color: #1890ff;
}
&.type-urgent {
background: #fff1f0;
color: #ff4d4f;
}
&.type-info {
&.type-announcement {
background: #f6ffed;
color: #52c41a;
}
&.type-success {
background: #f9f0ff;
color: #722ed1;
}
}
.notice-item-content {

View File

@@ -127,12 +127,11 @@ const hasUnread = computed(() => {
})
// 获取公告类型图标
// noticeType: 1=通知, 2=公告
const getNoticeTypeIcon = (type) => {
const iconMap = {
'1': Bell, // 通知
'2': Warning, // 紧急
'3': InfoFilled, // 信息
'4': CircleCheck // 成功
'2': InfoFilled // 公告
}
return iconMap[type] || InfoFilled
}
@@ -178,36 +177,33 @@ const getPriorityIcon = (priority) => {
}
// 获取公告类型样式类
// noticeType: 1=通知, 2=公告
const getNoticeTypeClass = (type) => {
const classMap = {
'1': 'type-notice',
'2': 'type-urgent',
'3': 'type-info',
'4': 'type-success'
'2': 'type-announcement'
}
return classMap[type] || 'type-info'
return classMap[type] || 'type-announcement'
}
// 获取公告类型标签类型
// noticeType: 1=通知, 2=公告
const getNoticeTypeTagType = (type) => {
const typeMap = {
'1': '',
'2': 'danger',
'3': 'info',
'4': 'success'
'1': 'primary',
'2': 'success'
}
return typeMap[type] || ''
return typeMap[type] || 'info'
}
// 获取公告类型文本
// noticeType: 1=通知, 2=公告
const getNoticeTypeText = (type) => {
const textMap = {
'1': '通知',
'2': '紧急',
'3': '信息',
'4': '成功'
'2': '公告'
}
return textMap[type] || '信息'
return textMap[type] || '公告'
}
// 格式化日期
@@ -414,21 +410,11 @@ defineExpose({
background: #e6f7ff;
color: #1890ff;
}
&.type-urgent {
background: #fff1f0;
color: #ff4d4f;
}
&.type-info {
&.type-announcement {
background: #f6ffed;
color: #52c41a;
}
&.type-success {
background: #f9f0ff;
color: #722ed1;
}
}
.notice-item-content {

View File

@@ -20,26 +20,17 @@ const tagsViewStore = useTagsViewStore()
<style lang="scss" scoped>
.app-main {
/* 50= navbar 50 */
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
flex: 1;
overflow: auto;
}
.fixed-header + .app-main {
padding-top: 50px;
.fixed-header ~ .app-main {
padding-top: 34px;
}
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - 84px);
}
.fixed-header + .app-main {
padding-top: 84px;
}
.hasTagsView .app-main {
padding-top: 0;
}
</style>

View File

@@ -1,17 +1,29 @@
<template>
<div class="navbar">
<div class="right-menu">
<template v-if="appStore.device !== 'mobile'">
<header-search id="header-search" class="right-menu-item" />
</template>
<!-- 公告和通知按钮 -->
<el-tooltip content="公告/通知" placement="bottom">
<div class="right-menu-item notice-btn" @click="openNoticePanel">
<el-badge :value="unreadCount" :hidden="unreadCount === 0" class="notice-badge">
<el-icon><Bell /></el-icon>
</el-badge>
<div class="left-menu">
<div class="hamburger-container">
<div class="hamburger" @click="toggleSideBar">
<el-icon :size="20">
<component :is="sidebar.opened ? 'Fold' : 'Expand'" />
</el-icon>
</div>
</el-tooltip>
</div>
<!-- 搜索和公告通知 -->
<div class="left-actions">
<template v-if="appStore.device !== 'mobile'">
<header-search id="header-search" class="left-action-item" />
</template>
<!-- 公告和通知按钮 -->
<el-tooltip content="公告/通知" placement="bottom">
<div class="left-action-item notice-btn" @click="openNoticePanel">
<el-badge :value="unreadCount" :hidden="unreadCount === 0" class="notice-badge">
<el-icon><Bell /></el-icon>
</el-badge>
</div>
</el-tooltip>
</div>
</div>
<div class="right-menu">
<div class="avatar-container">
<div class="avatar-wrapper">
<el-dropdown
@@ -40,9 +52,6 @@
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<!-- <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
<span>布局设置</span>
</el-dropdown-item> -->
<el-dropdown-item divided command="logout">
<span>退出登录</span>
</el-dropdown-item>
@@ -86,8 +95,8 @@
</el-select>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submit"> </el-button>
<el-button @click="showDialog = false"> </el-button>
<el-button type="primary" @click="submit">确定</el-button>
<el-button @click="showDialog = false">取消</el-button>
</div>
</template>
</el-dialog>
@@ -98,9 +107,9 @@
</template>
<script setup>
import {onMounted, ref} from 'vue';
import {onMounted, ref, computed} from 'vue';
import {ElMessageBox} from 'element-plus';
import {Bell} from '@element-plus/icons-vue';
import {Fold, Expand, Bell} from '@element-plus/icons-vue';
import HeaderSearch from '@/components/HeaderSearch';
import NoticePanel from '@/components/NoticePanel';
import useAppStore from '@/store/modules/app';
@@ -108,16 +117,19 @@ import useUserStore from '@/store/modules/user';
import useSettingsStore from '@/store/modules/settings';
import {getOrg, switchOrg} from '@/api/login';
import {getUnreadCount} from '@/api/system/notice';
import {useRouter} from 'vue-router';
const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const router = useRouter();
const orgOptions = ref([]);
const showDialog = ref(false);
const orgId = ref('');
const noticePanelRef = ref(null);
const unreadCount = ref(0);
const sidebar = computed(() => appStore.sidebar);
// 加载未读数量
function loadUnreadCount() {
getUnreadCount().then(res => {
@@ -132,6 +144,11 @@ function updateUnreadCount() {
loadUnreadCount();
}
// 切换侧边栏
function toggleSideBar() {
appStore.toggleSideBar();
}
function loadOrgList() {
getOrg().then((res) => {
orgOptions.value = res.data;
@@ -220,16 +237,78 @@ function openNoticePanel() {
<style lang='scss' scoped>
.navbar {
height: 50px;
overflow: visible;
width: 100%;
overflow: hidden;
position: relative;
background-color: transparent;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 10px;
flex-shrink: 0;
min-width: 200px;
z-index: 1002;
justify-content: space-between;
padding: 0 15px;
.left-menu {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 8px;
.hamburger-container {
display: flex;
align-items: center;
.hamburger {
cursor: pointer;
padding: 0 12px;
height: 50px;
display: flex;
align-items: center;
transition: background 0.3s;
color: #5a5e66;
&:hover {
background-color: #f6f6f6;
color: var(--el-color-primary);
}
}
}
.left-actions {
display: flex;
align-items: center;
gap: 4px;
.left-action-item {
display: flex;
align-items: center;
padding: 0 10px;
height: 50px;
font-size: 16px;
color: #606266;
cursor: pointer;
transition: background 0.3s;
&:hover {
background-color: #f6f6f6;
}
.notice-badge {
:deep(.el-badge__content) {
top: -5px;
right: -5px;
}
}
.el-icon {
font-size: 20px;
&:hover {
color: #409eff;
}
}
}
}
}
.right-menu {
display: flex;
@@ -241,49 +320,6 @@ function openNoticePanel() {
outline: none;
}
.right-menu-item {
display: flex;
align-items: center;
padding: 0 6px;
height: 100%;
font-size: 16px;
color: #606266;
flex-shrink: 0;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(255,255,255,0.1);
}
}
}
.notice-btn {
cursor: pointer;
padding: 0 10px;
height: 100%;
display: flex;
align-items: center;
.notice-badge {
:deep(.el-badge__content) {
top: -5px;
right: -5px;
}
}
.el-icon {
font-size: 20px;
color: #606266;
&:hover {
color: #409eff;
}
}
}
.avatar-container {
margin-right: 0;
margin-left: 5px;
@@ -313,8 +349,8 @@ function openNoticePanel() {
.user-avatar {
cursor: pointer;
width: 30px;
height: 30px;
width: 32px;
height: 32px;
border-radius: 6px;
flex-shrink: 0;
}
@@ -364,8 +400,21 @@ function openNoticePanel() {
}
@media (max-width: 768px) {
min-width: 150px;
padding-right: 5px;
padding: 0 10px;
.left-menu {
.hamburger-container {
.hamburger {
padding: 0 8px;
}
}
.left-actions {
.left-action-item {
padding: 0 6px;
}
}
}
.right-menu {
.avatar-container {
@@ -388,8 +437,8 @@ function openNoticePanel() {
}
.user-avatar {
width: 24px;
height: 24px;
width: 28px;
height: 28px;
}
}
}
@@ -397,8 +446,21 @@ function openNoticePanel() {
}
@media (max-width: 480px) {
min-width: 120px;
padding-right: 5px;
padding: 0 8px;
.left-menu {
.hamburger-container {
.hamburger {
padding: 0 6px;
}
}
.left-actions {
.left-action-item {
padding: 0 4px;
}
}
}
.right-menu {
.avatar-container {
@@ -415,6 +477,11 @@ function openNoticePanel() {
max-width: 80px;
font-size: 11px;
}
.user-avatar {
width: 24px;
height: 24px;
}
}
}
}

View File

@@ -7,36 +7,25 @@
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground,
}"
>
<!-- <el-image
:src="sideTheme === 'theme-dark' ? Logo : Logo"
class="sidebar-logo"
fit="scale-down"
/> -->
<!-- <transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<el-image
v-if="logo"
:src="sideTheme === 'theme-dark' ? logoNew : logoBlack"
class="sidebar-logo"
fit="scale-down"
/>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<el-image
v-if="logo"
:src="sideTheme === 'theme-dark' ? logoNew : logoBlack"
class="sidebar-logo"
fit="scale-down"
/>
</router-link>
</transition> -->
<router-link class="sidebar-logo-link" to="/index">
<el-image
:src="logoImage"
class="sidebar-logo"
fit="contain"
/>
<div v-if="!collapse" class="logo-text" :style="{ color: textColor }">
<h1 class="sidebar-title">{{ title }}</h1>
<p v-if="displayName" class="hospital-name">{{ displayName }}</p>
</div>
</router-link>
</div>
</template>
<script setup>
import variables from '@/assets/styles/variables.module.scss';
import useSettingsStore from '@/store/modules/settings';
import {computed, ref} from 'vue';
import useUserStore from '@/store/modules/user';
import {computed} from 'vue';
defineProps({
collapse: {
@@ -45,64 +34,99 @@ defineProps({
},
});
const title = import.meta.env.VITE_APP_TITLE;
const title = import.meta.env.VITE_APP_TITLE || '医院管理系统';
const settingsStore = useSettingsStore();
const userStore = useUserStore();
const sideTheme = computed(() => settingsStore.sideTheme);
const logo = ref(true);
const displayName = computed(() => userStore.tenantName || userStore.hospitalName || userStore.orgName || '');
const textColor = computed(() => {
return sideTheme.value === 'theme-dark' ? '#fff' : '#303133';
});
const logoImage = computed(() => {
return new URL('@/assets/logo/LOGO.jpg', import.meta.url).href;
});
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
.sidebar-logo-container {
position: relative;
width: auto;
min-width: 120px;
max-width: 160px;
width: 100%;
height: 50px;
line-height: 50px;
background: transparent;
text-align: center;
overflow: hidden;
padding: 0 8px;
margin-left: 5px;
display: flex;
align-items: center;
justify-content: center;
& .sidebar-logo-link {
.sidebar-logo-link {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px;
gap: 8px;
text-decoration: none;
cursor: pointer;
& .sidebar-logo {
width: auto;
max-width: 120px;
height: 28px;
vertical-align: middle;
&:hover {
opacity: 0.8;
}
& .sidebar-title {
display: inline-block;
.sidebar-logo {
width: 32px;
height: 32px;
flex-shrink: 0;
object-fit: contain;
}
.logo-text {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 2px;
min-width: 0;
pointer-events: none;
}
.sidebar-logo {
pointer-events: none;
}
.sidebar-title {
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-size: 15px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
.hospital-name {
margin: 0;
font-size: 12px;
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
opacity: 0.85;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
.sidebar-logo-link {
padding: 0;
justify-content: center;
}
.logo-text {
display: none;
}
}
}

View File

@@ -1,50 +1,49 @@
<template>
<div
:class="{ 'has-logo': showLogo }"
class="sidebar-wrapper"
:class="[
'sidebar-wrapper',
{ 'has-logo': showLogo },
{ 'is-collapse': isCollapse }
]"
:style="{
backgroundColor:
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground,
}"
>
<!-- <logo v-if="showLogo" :collapse="isCollapse" /> -->
<div class="menu-container">
<div class="menu-scrollbar">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground
"
:text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:unique-opened="true"
:active-text-color="theme"
:collapse-transition="false"
mode="horizontal"
>
<sidebar-item
v-for="(route, index) in sidebarRouters"
:key="route.path + index"
:item="route"
:base-path="route.path"
/>
</el-menu>
</div>
</div>
<navbar @setLayout="setLayout" class="navbar-container" />
<settings ref="settingRef" />
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar class="sidebar-scrollbar">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground
"
:text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:unique-opened="true"
:active-text-color="theme"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item
v-for="(route, index) in sidebarRouters"
:key="route.path + index"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup>
import Logo from './Logo';
import SidebarItem from './SidebarItem';
import variables from '@/assets/styles/variables.module.scss';
import useAppStore from '@/store/modules/app';
import useSettingsStore from '@/store/modules/settings';
import usePermissionStore from '@/store/modules/permission';
import {computed, ref} from 'vue';
import {computed} from 'vue';
import {useRoute} from 'vue-router';
import {Navbar, Settings} from '@/layout/components';
const route = useRoute();
const appStore = useAppStore();
@@ -65,134 +64,122 @@ const activeMenu = computed(() => {
}
return path;
});
const settingRef = ref(null);
function setLayout() {
settingRef.value.openSetting();
}
</script>
<style lang="scss" scoped>
@import '@/assets/styles/variables.module.scss';
.sidebar-wrapper {
width: $sideBarWidth;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
height: 50px;
padding: 0;
width: 100%;
flex-direction: column;
overflow: hidden;
position: relative;
}
.menu-container {
flex: 1;
height: 50px;
min-width: 0;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
}
.menu-scrollbar {
height: 50px;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
display: flex;
align-items: center;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar:vertical {
display: none;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.el-menu--horizontal {
display: flex !important;
align-items: center !important;
border-bottom: none !important;
background-color: transparent !important;
min-width: auto;
flex-wrap: nowrap;
height: 50px;
& > .el-menu-item,
& > .el-sub-menu {
height: 50px;
line-height: 50px;
color: #fff;
padding: 0 15px !important;
font-size: 14px;
min-width: 120px !important;
flex-shrink: 0;
white-space: nowrap;
display: flex;
align-items: center;
}
:deep(.svg-icon) {
margin-right: 8px !important;
font-size: 14px;
}
:deep(.el-sub-menu__title) {
padding-right: 25px !important;
display: flex;
align-items: center;
height: 50px;
}
:deep(.el-sub-menu__icon-arrow) {
right: 8px !important;
}
:deep(.menu-title) {
font-size: 13px;
}
}
.navbar-container {
transition: width 0.28s;
flex-shrink: 0;
height: 50px;
z-index: 1002;
}
/* 响应式处理 */
@media (max-width: 768px) {
.menu-container {
flex: 1;
min-width: 0;
&.is-collapse {
width: 54px;
}
.el-menu--horizontal {
& > .el-menu-item,
& > .el-sub-menu {
padding: 0 12px !important;
min-width: 80px !important;
font-size: 12px;
.mobile & {
position: fixed;
z-index: 1001;
height: 100%;
top: 0;
left: 0;
transform: translateX(0);
transition: transform 0.28s;
}
.is-collapse.mobile & {
transform: translateX(-100%);
}
}
.has-logo {
.sidebar-scrollbar {
height: calc(100% - #{$logoHeight});
}
}
.sidebar-scrollbar {
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
.sidebar-scrollbar::-webkit-scrollbar {
width: 6px;
}
.sidebar-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.sidebar-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
:deep(.el-menu-item),
:deep(.el-sub-menu__title) {
height: 50px;
line-height: 50px;
.svg-icon {
margin-right: 8px;
}
}
:deep(.el-menu-item) {
&.is-active {
background-color: var(--current-color) !important;
}
}
:deep(.el-sub-menu) {
.el-menu-item {
min-width: $sideBarWidth !important;
}
&.is-active {
> .el-sub-menu__title {
color: var(--current-color) !important;
}
}
}
@media (max-width: 480px) {
.el-menu--horizontal {
& > .el-menu-item,
& > .el-sub-menu {
padding: 0 8px !important;
min-width: 60px !important;
font-size: 11px;
:deep(.el-menu--collapse) {
.el-menu-item,
.el-sub-menu__title {
padding: 0 20px !important;
text-align: center;
span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
.svg-icon {
margin-right: 0 !important;
}
.el-sub-menu {
&.is-opened {
> .el-sub-menu__title .el-icon-arrow-right {
display: none;
}
}
}
}

View File

@@ -1,21 +1,29 @@
<template>
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
<div class="app-wrapper">
<!-- 遮罩层 -->
<div
v-if="device === 'mobile' && sidebar.opened"
class="drawer-bg"
@click="handleClickOutside"
/>
<div class="top-container">
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
<!-- <navbar @setLayout="setLayout" class="navbar-container" /> -->
</div>
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }">
<tags-view v-if="needTagsView" />
<!-- 左侧侧边栏 -->
<sidebar v-if="!sidebar.hide" />
<!-- 右侧主容器 -->
<div class="main-wrapper">
<!-- 顶部导航栏 -->
<navbar @setLayout="setLayout" />
<!-- 内容区 -->
<div :class="{ 'hasTagsView': needTagsView }" class="content-wrapper">
<!-- 标签栏 -->
<div v-if="needTagsView" :class="{ 'fixed-header': fixedHeader }">
<tags-view />
</div>
<!-- 主内容 -->
<app-main />
</div>
<app-main />
<settings ref="settingRef" />
</div>
<!-- 设置组件 -->
<settings ref="settingRef" />
<!-- 公告弹窗组件 -->
<notice-popup ref="noticePopupRef" />
</div>
@@ -24,7 +32,7 @@
<script setup>
import {useWindowSize} from '@vueuse/core';
import Sidebar from './components/Sidebar/index.vue';
import {AppMain, Settings, TagsView} from './components';
import {AppMain, Settings, TagsView, Navbar} from './components';
import NoticePopup from '@/components/NoticePopup/index.vue';
import useAppStore from '@/store/modules/app';
@@ -38,14 +46,7 @@ const device = computed(() => useAppStore().device);
const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
openSidebar: sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation,
mobile: device.value === 'mobile',
}));
const { width, height } = useWindowSize();
const { width } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
watchEffect(() => {
@@ -83,66 +84,67 @@ defineExpose({
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
height: 100%;
display: flex;
overflow: hidden;
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.top-container {
display: flex;
align-items: center;
width: 100%;
height: 50px;
background-color: $base-menu-background;
position: fixed;
top: 0;
left: 0;
z-index: 1001;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.35);
overflow: visible;
z-index: 999;
}
.sidebar-container {
.main-wrapper {
flex: 1;
height: 50px;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.navbar-container {
min-width: 280px;
height: 50px;
width: 100%;
flex-shrink: 0;
background: #fff;
}
.content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
padding-top: 0;
}
.fixed-header {
position: fixed;
top: 50px;
right: 0;
left: 0;
z-index: 9;
width: 100%;
transition: width 0.28s;
padding: 0 15px;
background: #fff;
}
.mobile .fixed-header {
width: 100%;
.sidebarHide {
.sidebar-container {
display: none;
}
}
.main-container {
padding-top: 50px;
.mobile {
.main-wrapper {
margin-left: 0;
}
}
</style>

View File

@@ -19,6 +19,7 @@ const useUserStore = defineStore(
roles: [],
permissions: [],
tenantId: '',
tenantName: '', // 租户名称
hospitalName:''
}),
actions: {
@@ -63,6 +64,7 @@ const useUserStore = defineStore(
this.fixmedinsCode = res.optionJson.fixmedinsCode
this.avatar = avatar
this.hospitalName = res.optionJson.hospitalName
this.tenantName = res.tenantName || ''
resolve(res)
}).catch(error => {

View File

@@ -1,287 +1,154 @@
<template>
<div class="container">
<div class="awaitingBtn">
<el-button @click="awaitingMedicineBtn">
效期预警
<span>{{ total }}</span>
</el-button>
<!-- <el-select v-model="selectValue" @change="handelChange" @keyup.enter="handelEnter">
<el-option label="测试1" value="1"/>
<el-option label="测试2" value="2 "/>
</el-select> -->
<!-- 首页内容为空只显示欢迎信息 -->
<div class="welcome-content">
<div class="welcome-card">
<h1 class="welcome-title">欢迎使用</h1>
<p class="welcome-subtitle">医院管理系统</p>
<div class="quick-actions">
<div class="action-item" @click="handleExpiryWarning">
<el-icon :size="32" color="#e6a23c">
<Warning />
</el-icon>
<span>效期预警</span>
</div>
</div>
</div>
</div>
<!-- <div class="logo">
<img src="/src/assets/images/jlau.jpg" />
</div> -->
</div>
</template>
<script setup name="Index">
import {getproductReturnPage} from './medicationmanagement/statisticalManagement/statisticalManagent';
import {useStore} from '@/store/store';
import {Warning} from '@element-plus/icons-vue';
import {useRouter} from 'vue-router';
const store = useStore();
const router = useRouter();
const version = ref('3.8.7');
const total = ref(0);
const selectValue = ref('');
function awaitingMedicineBtn() {
store.setRemainingDays(180);
console.log(store.remainingDays);
function handleExpiryWarning() {
store.setRemainingDays(180);
router.push({
path: '/medicationmanagement/statisticalManagement/statisticalManagement',
});
}
function goTarget(url) {
window.open(url, '__blank');
}
function handelEnter() {
console.log('enter');
}
// function handelChange(val) {
// console.log(val);
// }
function getExpirationWarningCount() {
getproductReturnPage({ pageNo: 1, pageSize: 10, remainingDays: 180 }).then((res) => {
total.value = res.data.total || 0;
});
}
getExpirationWarningCount();
</script>
<style scoped lang="scss">
.dashboard-container {
.container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.welcome-content {
width: 100%;
max-width: 1200px;
padding: 40px 20px;
display: flex;
justify-content: center;
align-items: center;
}
.welcome-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 60px 80px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
text-align: center;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.welcome-title {
font-size: 42px;
font-weight: 600;
color: #303133;
margin: 0 0 20px 0;
letter-spacing: 2px;
}
.welcome-subtitle {
font-size: 24px;
color: #909399;
margin: 0 0 50px 0;
font-weight: 300;
}
.quick-actions {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 40px;
}
.action-item {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f7fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
padding: 0;
}
.awaitingBtn {
.el-button {
border: 1px #166773 solid;
span {
color: red;
margin-left: 5px;
}
}
.el-button:hover {
border: 1px #166773 solid;
color: #166773;
span {
color: red;
margin-left: 5px;
}
}
position: absolute;
top: 20px;
left: 20px;
}
.section-title {
font-size: 24px;
margin-bottom: 20px;
color: #333;
font-weight: 600;
}
.card {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.card-title {
font-size: 14px;
color: #606266;
margin-bottom: 12px;
}
.card-value {
font-size: 32px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.card-stats {
display: flex;
align-items: center;
font-size: 12px;
color: #67c23a;
}
.card-stats.down {
color: #f56c6c;
}
.stats-icon {
margin-right: 4px;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 30px;
height: 300px;
canvas {
width: 100% !important;
height: 100% !important;
}
}
.bottom-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.notification-container,
.todo-container {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.notification-title,
.todo-title {
font-size: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
}
.notification-list {
list-style: none;
padding: 0;
margin: 0;
}
.notification-item {
padding: 12px 0;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
margin-bottom: 20px;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
}
.todo-count {
color: #606266;
font-size: 14px;
}
.table-container {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ebeef5;
}
th {
background-color: #f5f7fa;
font-weight: 600;
color: #303133;
}
tr:last-child td {
border-bottom: none;
}
.btn {
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
padding: 30px;
background: #fff;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
border: none;
outline: none;
}
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 160px;
.btn-primary {
background-color: #1890ff;
color: #fff;
}
&:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
.btn-primary:hover {
background-color: #40a9ff;
}
span {
color: #409eff;
}
}
.btn-outline {
background-color: transparent;
border: 1px solid #dcdfe6;
color: #606266;
}
.btn-outline:hover {
border-color: #c6e2ff;
color: #40a9ff;
background-color: #ecf5ff;
span {
margin-top: 12px;
font-size: 16px;
color: #303133;
font-weight: 500;
transition: color 0.3s;
}
}
@media (max-width: 768px) {
.bottom-content {
grid-template-columns: 1fr;
.welcome-card {
padding: 40px 30px;
margin: 20px;
}
.dashboard-grid {
grid-template-columns: 1fr;
.welcome-title {
font-size: 32px;
}
.welcome-subtitle {
font-size: 20px;
}
.quick-actions {
flex-direction: column;
gap: 15px;
margin-top: 30px;
}
.action-item {
min-width: 100%;
padding: 20px;
}
}
</style>
</style>

View File

@@ -0,0 +1,759 @@
<template>
<div class="app-container">
<!-- 查询表单 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" class="query-form">
<el-form-item label="手术编号" prop="surgeryNo">
<el-input
v-model="queryParams.surgeryNo"
placeholder="请输入手术编号"
clearable
@keyup.enter="handleQuery"
style="width: 200px"
/>
</el-form-item>
<el-form-item label="手术名称" prop="surgeryName">
<el-input
v-model="queryParams.surgeryName"
placeholder="请输入手术名称"
clearable
@keyup.enter="handleQuery"
style="width: 200px"
/>
</el-form-item>
<el-form-item label="患者姓名" prop="patientName">
<el-input
v-model="queryParams.patientName"
placeholder="请输入患者姓名"
clearable
@keyup.enter="handleQuery"
style="width: 200px"
/>
</el-form-item>
<el-form-item label="手术状态" prop="statusEnum">
<el-select v-model="queryParams.statusEnum" placeholder="请选择手术状态" clearable style="width: 200px">
<el-option
v-for="item in surgeryStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="手术类型" prop="surgeryTypeEnum">
<el-select v-model="queryParams.surgeryTypeEnum" placeholder="请选择手术类型" clearable style="width: 200px">
<el-option
v-for="item in surgeryTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="计划时间" prop="plannedTime">
<el-date-picker
v-model="queryParams.plannedTime"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item class="search-buttons">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd"> 新增手术 </el-button>
</el-col>
<el-col :span="1.5">
<el-button type="" plain icon="Refresh" @click="getPageList">刷新</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="surgeryList" row-key="id">
<el-table-column label="手术编号" align="center" prop="surgeryNo" width="150" />
<el-table-column label="患者姓名" align="center" prop="patientName" width="100" />
<el-table-column label="性别" align="center" prop="patientGender" width="60" />
<el-table-column label="年龄" align="center" prop="patientAge" width="60" />
<el-table-column label="手术名称" align="center" prop="surgeryName" min-width="150" show-overflow-tooltip />
<el-table-column label="手术类型" align="center" prop="surgeryTypeEnum_dictText" width="100" />
<el-table-column label="手术等级" align="center" prop="surgeryLevel_dictText" width="100" />
<el-table-column label="手术状态" align="center" prop="statusEnum_dictText" width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.statusEnum)">
{{ scope.row.statusEnum_dictText }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="计划时间" align="center" prop="plannedTime" width="160" />
<el-table-column label="主刀医生" align="center" prop="mainSurgeonName" width="100" />
<el-table-column label="麻醉医生" align="center" prop="anesthetistName" width="100" />
<el-table-column label="手术室" align="center" prop="operatingRoomName" width="120" />
<el-table-column label="执行科室" align="center" prop="orgName" width="120" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">编辑</el-button>
<el-button link type="primary" @click="handleStart(scope.row)" v-if="scope.row.statusEnum === 1">开始</el-button>
<el-button link type="primary" @click="handleComplete(scope.row)" v-if="scope.row.statusEnum === 2">完成</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getPageList"
/>
</div>
<!-- 新增或修改手术对话框 -->
<el-dialog :title="title" v-model="open" width="800px" @close="cancel" append-to-body :close-on-click-modal="false">
<el-form ref="surgeryRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="id" prop="id" v-show="false">
<el-input v-model="form.id" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="患者" prop="patientId">
<el-select v-model="form.patientId" placeholder="请选择患者" filterable style="width: 100%" :disabled="form.id">
<el-option
v-for="item in patientList"
:key="item.id"
:label="item.name + ' (' + item.busNo + ')'"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="就诊流水号" prop="encounterId">
<el-input v-model="form.encounterNo" placeholder="请选择就诊" readonly>
<template #append>
<el-button icon="Search" @click="selectEncounter" />
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术名称" prop="surgeryName">
<el-input v-model="form.surgeryName" placeholder="请输入手术名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手术编码" prop="surgeryCode">
<el-input v-model="form.surgeryCode" placeholder="请输入手术编码" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术类型" prop="surgeryTypeEnum">
<el-select v-model="form.surgeryTypeEnum" placeholder="请选择手术类型" style="width: 100%">
<el-option
v-for="item in surgeryTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手术等级" prop="surgeryLevel">
<el-select v-model="form.surgeryLevel" placeholder="请选择手术等级" style="width: 100%">
<el-option
v-for="item in surgeryLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="计划手术时间" prop="plannedTime">
<el-date-picker
v-model="form.plannedTime"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手术部位" prop="bodySite">
<el-input v-model="form.bodySite" placeholder="请输入手术部位" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">手术团队</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="主刀医生" prop="mainSurgeonId">
<el-select v-model="form.mainSurgeonId" placeholder="请选择主刀医生" filterable style="width: 100%">
<el-option
v-for="item in doctorList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="麻醉医生" prop="anesthetistId">
<el-select v-model="form.anesthetistId" placeholder="请选择麻醉医生" filterable style="width: 100%">
<el-option
v-for="item in doctorList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="助手1" prop="assistant1Id">
<el-select v-model="form.assistant1Id" placeholder="请选择助手1" filterable clearable style="width: 100%">
<el-option
v-for="item in doctorList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="助手2" prop="assistant2Id">
<el-select v-model="form.assistant2Id" placeholder="请选择助手2" filterable clearable style="width: 100%">
<el-option
v-for="item in doctorList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="巡回护士" prop="scrubNurseId">
<el-select v-model="form.scrubNurseId" placeholder="请选择巡回护士" filterable clearable style="width: 100%">
<el-option
v-for="item in nurseList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="麻醉方式" prop="anesthesiaTypeEnum">
<el-select v-model="form.anesthesiaTypeEnum" placeholder="请选择麻醉方式" style="width: 100%">
<el-option
v-for="item in anesthesiaTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术室" prop="operatingRoomId">
<el-input v-model="form.operatingRoomName" placeholder="请输入手术室">
<template #append>
<el-button icon="Search" />
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="执行科室" prop="orgId">
<el-tree-select
v-model="form.orgId"
:data="orgList"
:props="{ value: 'id', label: 'name', children: 'children' }"
placeholder="请选择执行科室"
check-strictly
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">诊断信息</el-divider>
<el-form-item label="术前诊断" prop="preoperativeDiagnosis">
<el-input v-model="form.preoperativeDiagnosis" type="textarea" placeholder="请输入术前诊断" :rows="3" />
</el-form-item>
<el-form-item label="术后诊断" prop="postoperativeDiagnosis">
<el-input v-model="form.postoperativeDiagnosis" type="textarea" placeholder="请输入术后诊断" :rows="3" />
</el-form-item>
<el-form-item label="手术经过描述" prop="surgeryDescription">
<el-input v-model="form.surgeryDescription" type="textarea" placeholder="请输入手术经过描述" :rows="5" />
</el-form-item>
<el-form-item label="术后医嘱" prop="postoperativeAdvice">
<el-input v-model="form.postoperativeAdvice" type="textarea" placeholder="请输入术后医嘱" :rows="3" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="手术费用" prop="surgeryFee">
<el-input-number v-model="form.surgeryFee" :precision="2" :min="0" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="麻醉费用" prop="anesthesiaFee">
<el-input-number v-model="form.anesthesiaFee" :precision="2" :min="0" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="总费用" prop="totalFee">
<el-input-number v-model="form.totalFee" :precision="2" :min="0" style="width: 100%" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="切口等级" prop="incisionLevel">
<el-select v-model="form.incisionLevel" placeholder="请选择切口等级" style="width: 100%">
<el-option
v-for="item in incisionLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="愈合等级" prop="healingLevel">
<el-select v-model="form.healingLevel" placeholder="请选择愈合等级" style="width: 100%">
<el-option
v-for="item in healingLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="并发症描述" prop="complications">
<el-input v-model="form.complications" type="textarea" placeholder="请输入并发症描述" :rows="3" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注信息" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<!-- 查看手术详情对话框 -->
<el-dialog title="手术详情" v-model="viewOpen" width="900px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="手术编号">{{ viewData.surgeryNo }}</el-descriptions-item>
<el-descriptions-item label="手术状态">
<el-tag :type="getStatusType(viewData.statusEnum)">{{ viewData.statusEnum_dictText }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="患者姓名">{{ viewData.patientName }}</el-descriptions-item>
<el-descriptions-item label="患者信息">{{ viewData.patientGender }} / {{ viewData.patientAge }}</el-descriptions-item>
<el-descriptions-item label="就诊流水号">{{ viewData.encounterNo }}</el-descriptions-item>
<el-descriptions-item label="手术名称">{{ viewData.surgeryName }}</el-descriptions-item>
<el-descriptions-item label="手术编码">{{ viewData.surgeryCode }}</el-descriptions-item>
<el-descriptions-item label="手术类型">{{ viewData.surgeryTypeEnum_dictText }}</el-descriptions-item>
<el-descriptions-item label="手术等级">{{ viewData.surgeryLevel_dictText }}</el-descriptions-item>
<el-descriptions-item label="麻醉方式">{{ viewData.anesthesiaTypeEnum_dictText }}</el-descriptions-item>
<el-descriptions-item label="计划时间">{{ viewData.plannedTime }}</el-descriptions-item>
<el-descriptions-item label="实际开始时间">{{ viewData.actualStartTime }}</el-descriptions-item>
<el-descriptions-item label="实际结束时间">{{ viewData.actualEndTime }}</el-descriptions-item>
<el-descriptions-item label="手术部位">{{ viewData.bodySite }}</el-descriptions-item>
<el-descriptions-item label="主刀医生">{{ viewData.mainSurgeonName }}</el-descriptions-item>
<el-descriptions-item label="麻醉医生">{{ viewData.anesthetistName }}</el-descriptions-item>
<el-descriptions-item label="助手1">{{ viewData.assistant1Name }}</el-descriptions-item>
<el-descriptions-item label="助手2">{{ viewData.assistant2Name }}</el-descriptions-item>
<el-descriptions-item label="手术室">{{ viewData.operatingRoomName }}</el-descriptions-item>
<el-descriptions-item label="执行科室">{{ viewData.orgName }}</el-descriptions-item>
<el-descriptions-item label="术前诊断" :span="2">{{ viewData.preoperativeDiagnosis }}</el-descriptions-item>
<el-descriptions-item label="术后诊断" :span="2">{{ viewData.postoperativeDiagnosis }}</el-descriptions-item>
<el-descriptions-item label="手术费用" :span="2">¥{{ viewData.surgeryFee }}</el-descriptions-item>
<el-descriptions-item label="麻醉费用" :span="2">¥{{ viewData.anesthesiaFee }}</el-descriptions-item>
<el-descriptions-item label="总费用" :span="2">¥{{ viewData.totalFee }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ viewData.remark }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup name="SurgeryManage">
import { getSurgeryPage, addSurgery, updateSurgery, deleteSurgery, getSurgeryDetail, updateSurgeryStatus } from '@/api/surgerymanage'
const { proxy } = getCurrentInstance()
const loading = ref(true)
const showSearch = ref(true)
const surgeryList = ref([])
const queryParams = ref({
pageNo: 1,
pageSize: 10,
surgeryNo: undefined,
surgeryName: undefined,
patientName: undefined,
statusEnum: undefined,
surgeryTypeEnum: undefined,
plannedTime: undefined
})
const open = ref(false)
const viewOpen = ref(false)
const form = ref({
id: undefined,
patientId: undefined,
encounterId: undefined,
encounterNo: undefined,
surgeryName: undefined,
surgeryCode: undefined,
surgeryTypeEnum: undefined,
surgeryLevel: undefined,
plannedTime: undefined,
mainSurgeonId: undefined,
anesthetistId: undefined,
assistant1Id: undefined,
assistant2Id: undefined,
scrubNurseId: undefined,
anesthesiaTypeEnum: undefined,
bodySite: undefined,
operatingRoomId: undefined,
operatingRoomName: undefined,
orgId: undefined,
preoperativeDiagnosis: undefined,
postoperativeDiagnosis: undefined,
surgeryDescription: undefined,
postoperativeAdvice: undefined,
surgeryFee: undefined,
anesthesiaFee: undefined,
totalFee: undefined,
incisionLevel: undefined,
healingLevel: undefined,
complications: undefined,
remark: undefined
})
const surgeryRef = ref()
const viewData = ref({})
const total = ref(0)
const title = ref('')
// 字典选项
const surgeryStatusOptions = ref([
{ value: 0, label: '待排期' },
{ value: 1, label: '已排期' },
{ value: 2, label: '手术中' },
{ value: 3, label: '已完成' },
{ value: 4, label: '已取消' },
{ value: 5, label: '暂停' }
])
const surgeryTypeOptions = ref([
{ value: 1, label: '门诊手术' },
{ value: 2, label: '住院手术' },
{ value: 3, label: '急诊手术' },
{ value: 4, label: '择期手术' }
])
const surgeryLevelOptions = ref([
{ value: 1, label: '一级手术' },
{ value: 2, label: '二级手术' },
{ value: 3, label: '三级手术' },
{ value: 4, label: '四级手术' },
{ value: 5, label: '特级手术' }
])
const anesthesiaTypeOptions = ref([
{ value: 0, label: '无麻醉' },
{ value: 1, label: '局部麻醉' },
{ value: 2, label: '区域麻醉' },
{ value: 3, label: '全身麻醉' },
{ value: 4, label: '脊椎麻醉' },
{ value: 5, label: '硬膜外麻醉' },
{ value: 6, label: '表面麻醉' }
])
const incisionLevelOptions = ref([
{ value: 1, label: 'I级切口' },
{ value: 2, label: 'II级切口' },
{ value: 3, label: 'III级切口' },
{ value: 4, label: 'IV级切口' }
])
const healingLevelOptions = ref([
{ value: 1, label: '甲级愈合' },
{ value: 2, label: '乙级愈合' },
{ value: 3, label: '丙级愈合' }
])
// 下拉列表数据
const patientList = ref([])
const doctorList = ref([])
const nurseList = ref([])
const orgList = ref([])
const rules = ref({
patientId: [{ required: true, message: '请选择患者', trigger: 'change' }],
surgeryName: [{ required: true, message: '请输入手术名称', trigger: 'blur' }],
surgeryTypeEnum: [{ required: true, message: '请选择手术类型', trigger: 'change' }],
surgeryLevel: [{ required: true, message: '请选择手术等级', trigger: 'change' }],
plannedTime: [{ required: true, message: '请选择计划手术时间', trigger: 'change' }],
mainSurgeonId: [{ required: true, message: '请选择主刀医生', trigger: 'change' }],
anesthesiaTypeEnum: [{ required: true, message: '请选择麻醉方式', trigger: 'change' }],
bodySite: [{ required: true, message: '请输入手术部位', trigger: 'blur' }]
})
// 监听费用变化自动计算总费用
watch([() => form.value.surgeryFee, () => form.value.anesthesiaFee], ([newSurgeryFee, newAnesthesiaFee]) => {
const surgeryFee = Number(newSurgeryFee) || 0
const anesthesiaFee = Number(newAnesthesiaFee) || 0
form.value.totalFee = surgeryFee + anesthesiaFee
})
getPageList()
function getList() {
loading.value = true
const params = { ...queryParams.value }
// 处理时间范围
if (params.plannedTime && params.plannedTime.length === 2) {
params.plannedTimeStart = params.plannedTime[0]
params.plannedTimeEnd = params.plannedTime[1]
delete params.plannedTime
}
getSurgeryPage(params).then((res) => {
surgeryList.value = res.data.records
total.value = res.data.total
}).catch(error => {
console.error('获取手术列表失败:', error)
proxy.$modal.msgError('获取手术列表失败,请稍后重试')
surgeryList.value = []
total.value = 0
}).finally(() => {
loading.value = false
})
}
function getPageList() {
queryParams.value.pageNo = 1
getList()
}
function handleQuery() {
queryParams.value.pageNo = 1
getList()
}
function resetQuery() {
proxy.resetForm('queryRef')
queryParams.value = {
pageNo: 1,
pageSize: 10,
surgeryNo: undefined,
surgeryName: undefined,
patientName: undefined,
statusEnum: undefined,
surgeryTypeEnum: undefined,
plannedTime: undefined
}
getList()
}
function handleAdd() {
title.value = '新增手术'
open.value = true
reset()
}
function handleEdit(row) {
title.value = '编辑手术'
open.value = true
getSurgeryDetail(row.id).then(res => {
if (res.code === 200) {
Object.assign(form.value, res.data)
}
}).catch(error => {
console.error('获取手术信息失败:', error)
proxy.$modal.msgError('获取手术信息失败')
})
}
function handleView(row) {
viewOpen.value = true
getSurgeryDetail(row.id).then(res => {
if (res.code === 200) {
viewData.value = res.data
}
}).catch(error => {
console.error('获取手术信息失败:', error)
proxy.$modal.msgError('获取手术信息失败')
})
}
function cancel() {
open.value = false
reset()
}
function reset() {
form.value = {
id: undefined,
patientId: undefined,
encounterId: undefined,
encounterNo: undefined,
surgeryName: undefined,
surgeryCode: undefined,
surgeryTypeEnum: undefined,
surgeryLevel: undefined,
plannedTime: undefined,
mainSurgeonId: undefined,
anesthetistId: undefined,
assistant1Id: undefined,
assistant2Id: undefined,
scrubNurseId: undefined,
anesthesiaTypeEnum: undefined,
bodySite: undefined,
operatingRoomId: undefined,
operatingRoomName: undefined,
orgId: undefined,
preoperativeDiagnosis: undefined,
postoperativeDiagnosis: undefined,
surgeryDescription: undefined,
postoperativeAdvice: undefined,
surgeryFee: undefined,
anesthesiaFee: undefined,
totalFee: undefined,
incisionLevel: undefined,
healingLevel: undefined,
complications: undefined,
remark: undefined
}
if (surgeryRef.value) {
surgeryRef.value.resetFields()
}
}
function submitForm() {
proxy.$refs['surgeryRef'].validate((valid) => {
if (valid) {
if (form.value.id == undefined) {
addSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('新增成功')
open.value = false
getPageList()
}).catch(error => {
console.error('新增手术失败:', error)
proxy.$modal.msgError('新增手术失败,请稍后重试')
})
} else {
updateSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('修改成功')
open.value = false
getPageList()
}).catch(error => {
console.error('更新手术失败:', error)
proxy.$modal.msgError('更新手术失败,请稍后重试')
})
}
}
})
}
function handleDelete(row) {
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
return deleteSurgery(row.id)
}).then(() => {
getPageList()
proxy.$modal.msgSuccess('删除成功')
}).catch(error => {
console.error('删除手术失败:', error)
})
}
function handleStart(row) {
proxy.$modal.confirm('是否确认开始手术"' + row.surgeryName + '"?').then(() => {
return updateSurgeryStatus(row.id, 2)
}).then(() => {
getPageList()
proxy.$modal.msgSuccess('手术已开始')
}).catch(error => {
console.error('开始手术失败:', error)
})
}
function handleComplete(row) {
proxy.$modal.confirm('是否确认完成手术"' + row.surgeryName + '"?').then(() => {
return updateSurgeryStatus(row.id, 3)
}).then(() => {
getPageList()
proxy.$modal.msgSuccess('手术已完成')
}).catch(error => {
console.error('完成手术失败:', error)
})
}
function selectEncounter() {
// TODO: 实现选择就诊的功能
proxy.$modal.msgInfo('请选择就诊记录')
}
function getStatusType(status) {
const typeMap = {
0: 'info',
1: 'warning',
2: 'primary',
3: 'success',
4: 'danger',
5: 'info'
}
return typeMap[status] || 'info'
}
</script>