Files
his/openhis-ui-vue3/src/components/NoticePanel.vue
zhangfei 9c3e603b94 Fix Bug #443: 手术计费:点击签发耗材时异常报错
当手术计费弹窗中点击"签发"耗材时,因耗材的locationId(发放库房)为空导致后端异常。
在DoctorStationAdviceAppServiceImpl.handDevice方法中,当locationId为null时,使用登录用户的科室ID作为默认值,
与NurseBillingAppService中的处理方式保持一致。
2026-05-08 09:14:18 +08:00

290 lines
7.0 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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>
<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>
</div>
</div>
<!-- 公告/通知详情对话框 -->
<el-dialog v-model="detailVisible" :title="currentNotice.noticeTitle" width="800px" append-to-body>
<div class="notice-detail">
<div class="detail-header">
<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>
</div>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</el-drawer>
</template>
<script setup>
import {ref} from 'vue'
import {getReadNoticeIds, getUserNotices, markAsRead} from '@/api/system/notice'
const emit = defineEmits(['updateUnreadCount'])
const noticeVisible = ref(false)
const detailVisible = ref(false)
const noticeList = ref([])
const currentNotice = ref({})
const readNoticeIds = ref(new Set())
// 打开公告/通知面板
function open() {
noticeVisible.value = true
loadNotices()
loadReadNoticeIds()
}
// 加载已读公告ID列表
function loadReadNoticeIds() {
getReadNoticeIds().then(response => {
const ids = response.data || []
readNoticeIds.value = new Set(ids)
// 同步到 localStorage
localStorage.setItem('readNoticeIds', JSON.stringify(ids))
}).catch(() => {
// 接口调用失败时从 localStorage 读取
const readIds = localStorage.getItem('readNoticeIds')
if (readIds) {
try {
const ids = JSON.parse(readIds)
readNoticeIds.value = new Set(ids)
} catch (e) {
console.error('解析已读ID失败', e)
}
}
})
}
// 排序:未读的排前面,已读的排后面,同状态按优先级和时间排序
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() {
getUserNotices().then(response => {
let list = response.data || []
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(() => {
readNoticeIds.value.add(item.noticeId)
// 保存到 localStorage
saveReadNoticeIds()
emit('updateUnreadCount')
})
}
}
// 保存已读公告ID列表
function saveReadNoticeIds() {
const ids = Array.from(readNoticeIds.value)
localStorage.setItem('readNoticeIds', JSON.stringify(ids))
}
// 检查是否已读
function isRead(noticeId) {
return readNoticeIds.value.has(noticeId)
}
// 暴露方法给父组件
defineExpose({
open,
isRead,
readNoticeIds
})
</script>
<style lang="scss" scoped>
.notice-list {
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.notice-item {
padding: 12px 0;
border-bottom: 1px solid #EBEEF5;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #F5F7FA;
}
&:last-child {
border-bottom: none;
}
&.is-read {
.notice-title {
color: #909399;
}
}
&.unread {
background-color: #fffbe6;
}
}
.notice-title {
font-size: 14px;
color: #303133;
margin-bottom: 8px;
line-height: 1.4;
display: flex;
align-items: center;
.unread-dot {
display: inline-block;
width: 6px;
height: 6px;
background-color: #f56c6c;
border-radius: 50%;
margin-right: 8px;
flex-shrink: 0;
}
}
.notice-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #909399;
gap: 8px;
.notice-type,
.notice-priority {
flex-shrink: 0;
}
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid #EBEEF5;
margin-bottom: 16px;
.detail-type {
display: flex;
align-items: center;
}
}
.detail-time {
font-size: 12px;
color: #909399;
}
.detail-content {
font-size: 14px;
line-height: 1.8;
color: #303133;
max-height: 500px;
overflow-y: auto;
}
:deep(.el-drawer__body) {
padding: 0 20px;
}
</style>