Ver Fonte

通知公告新增详细显示

RuoYi há 1 mês atrás
pai
commit
b01d860029

+ 359 - 0
src/layout/components/HeaderNotice/DetailView.vue

@@ -0,0 +1,359 @@
+<template>
+  <el-drawer v-model="visible" title="公告详情" direction="rtl" size="50%" append-to-body :before-close="handleClose" class="notice-detail-drawer">
+    <div v-loading="loading" class="notice-detail-drawer__body">
+      <div v-if="!detail" class="notice-empty">
+        <el-icon><Document /></el-icon>
+        <span>暂无数据</span>
+      </div>
+      <div v-else class="notice-page">
+        <div class="notice-type-wrap">
+          <span v-if="detail.noticeType === '1'" class="notice-type-tag type-notify">
+            <el-icon><Bell /></el-icon> 通知
+          </span>
+          <span v-else-if="detail.noticeType === '2'" class="notice-type-tag type-announce">
+            <el-icon><Message /></el-icon> 公告
+          </span>
+          <span v-else class="notice-type-tag type-notify">
+            <el-icon><Document /></el-icon> 消息
+          </span>
+        </div>
+
+        <h1 class="notice-title">{{ detail.noticeTitle }}</h1>
+
+        <div class="notice-meta">
+          <span class="meta-item">
+            <el-icon><User /></el-icon>
+            <span>{{ detail.createBy || '—' }}</span>
+          </span>
+          <span class="meta-item">
+            <el-icon><Clock /></el-icon>
+            <span>{{ detail.createTime || '—' }}</span>
+          </span>
+          <span class="meta-item">
+            <span :class="['status-dot', isStatusNormal ? 'status-ok' : 'status-off']"></span>
+            <span>{{ isStatusNormal ? '正常' : '已关闭' }}</span>
+          </span>
+        </div>
+
+        <div class="notice-divider">
+          <span class="notice-divider-dot"></span>
+          <span class="notice-divider-dot"></span>
+          <span class="notice-divider-dot"></span>
+        </div>
+
+        <div class="notice-body">
+          <div v-if="hasContent" class="notice-content" v-html="detail.noticeContent" />
+          <div v-else class="notice-empty notice-empty--inner">
+            <el-icon><Document /></el-icon> 暂无内容
+          </div>
+        </div>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script setup>
+import { getNotice } from '@/api/system/notice'
+
+const visible = ref(false)
+const loading = ref(false)
+const detail = ref(null)
+
+const isStatusNormal = computed(() => {
+  const status = detail.value && detail.value.status
+  return status === '0' || status === 0
+})
+
+const hasContent = computed(() => {
+  const content = detail.value && detail.value.noticeContent
+  return content != null && String(content).trim() !== ''
+})
+
+function open(payload) {
+  let id = null
+  let preset = null
+  if (payload != null && typeof payload === 'object') {
+    id = payload.noticeId
+    if (payload.noticeContent != null) {
+      preset = payload
+    }
+  } else {
+    id = payload
+  }
+  visible.value = true
+  if (preset) {
+    detail.value = preset
+    return
+  }
+  if (id == null || id === '') {
+    detail.value = null
+    return
+  }
+  loading.value = true
+  detail.value = null
+  getNotice(id).then(res => {
+    detail.value = res.data
+  }).catch(() => {
+    detail.value = null
+  }).finally(() => {
+    loading.value = false
+  })
+}
+
+function handleClose() {
+  visible.value = false
+  detail.value = null
+  loading.value = false
+}
+
+defineExpose({
+  open
+})
+</script>
+
+<style lang="scss" scoped>
+.notice-page {
+  max-width: 760px;
+  margin: 0 auto;
+  padding: 8px 8px 20px;
+  animation: notice-fade-up 0.28s ease both;
+}
+
+@keyframes notice-fade-up {
+  from {
+    opacity: 0;
+    transform: translateY(14px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.notice-type-tag {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  padding: 3px 12px;
+  border-radius: 2px;
+  font-size: 11px;
+  font-weight: 700;
+  letter-spacing: 1px;
+  text-transform: uppercase;
+  margin-bottom: 14px;
+}
+
+.type-notify {
+  background: #fff8e6;
+  color: #b7791f;
+  border-left: 3px solid #d97706;
+}
+
+.type-announce {
+  background: #e8f5e9;
+  color: #276749;
+  border-left: 3px solid #38a169;
+}
+
+.notice-title {
+  font-size: 22px;
+  font-weight: 700;
+  color: #1a202c;
+  line-height: 1.45;
+  margin: 0 0 16px;
+  letter-spacing: -0.2px;
+}
+
+.notice-meta {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 16px;
+  padding: 12px 0;
+  border-top: 1px solid #e9ecef;
+  border-bottom: 1px solid #e9ecef;
+  margin-bottom: 28px;
+}
+
+.meta-item {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  font-size: 12px;
+  color: #718096;
+}
+
+.meta-item .el-icon {
+  font-size: 12px;
+  color: #a0aec0;
+}
+
+.status-dot {
+  display: inline-block;
+  width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  margin-right: 4px;
+}
+
+.status-ok {
+  background: #38a169;
+}
+
+.status-off {
+  background: #e53e3e;
+}
+
+.notice-divider {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 24px;
+}
+
+.notice-divider::before,
+.notice-divider::after {
+  content: '';
+  flex: 1;
+  height: 1px;
+  background: linear-gradient(to right, transparent, #dee2e6, transparent);
+}
+
+.notice-divider-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #cbd5e0;
+}
+
+.notice-body {
+  background: #fff;
+  border-radius: 6px;
+  padding: 28px 32px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.04);
+  min-height: 120px;
+}
+
+.notice-content {
+  font-size: 14px;
+  line-height: 1.85;
+  color: #2d3748;
+  word-break: break-word;
+}
+
+.notice-content :deep(p) {
+  margin: 0 0 1em;
+}
+
+.notice-content :deep(h1),
+.notice-content :deep(h2),
+.notice-content :deep(h3) {
+  font-weight: 700;
+  color: #1a202c;
+  margin: 1.4em 0 0.6em;
+}
+
+.notice-content :deep(h1) {
+  font-size: 18px;
+}
+
+.notice-content :deep(h2) {
+  font-size: 16px;
+}
+
+.notice-content :deep(h3) {
+  font-size: 14px;
+}
+
+.notice-content :deep(a) {
+  color: #3182ce;
+  text-decoration: underline;
+}
+
+.notice-content :deep(a:hover) {
+  color: #2b6cb0;
+}
+
+.notice-content :deep(img) {
+  max-width: 100%;
+  border-radius: 4px;
+  margin: 8px 0;
+}
+
+.notice-content :deep(ul),
+.notice-content :deep(ol) {
+  padding-left: 20px;
+  margin: 0 0 1em;
+}
+
+.notice-content :deep(li) {
+  margin-bottom: 4px;
+}
+
+.notice-content :deep(blockquote) {
+  border-left: 3px solid #cbd5e0;
+  margin: 1em 0;
+  padding: 6px 16px;
+  color: #718096;
+  background: #f7fafc;
+}
+
+.notice-content :deep(table) {
+  border-collapse: collapse;
+  width: 100%;
+  margin: 1em 0;
+  font-size: 13px;
+}
+
+.notice-content :deep(table th),
+.notice-content :deep(table td) {
+  border: 1px solid #e2e8f0;
+  padding: 7px 12px;
+}
+
+.notice-content :deep(table th) {
+  background: #f7fafc;
+  font-weight: 600;
+}
+
+.notice-empty {
+  text-align: center;
+  padding: 40px 0;
+  color: #a0aec0;
+  font-size: 13px;
+}
+
+.notice-empty .el-icon {
+  font-size: 28px;
+  display: inline-flex;
+  margin-bottom: 10px;
+}
+
+.notice-empty--inner {
+  padding: 32px 0;
+}
+
+.notice-detail-drawer__body {
+  height: 100%;
+  overflow: auto;
+  padding: 10px 16px 22px;
+}
+</style>
+
+<style lang="scss">
+.notice-detail-drawer {
+  .el-drawer__header {
+    margin-bottom: 0;
+    padding: 16px 20px;
+    border-bottom: 1px solid #ebeef5;
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+  }
+  
+  .el-drawer__body {
+    background: #f5f6f8;
+    padding: 0;
+  }
+}
+</style>

+ 5 - 58
src/layout/components/HeaderNotice/index.vue

@@ -33,26 +33,13 @@
     </el-popover>
 
     <!-- 预览弹窗 -->
-    <el-dialog v-model="previewVisible" :title="previewTitle" width="680px" append-to-body class="notice-preview-dialog">
-      <div class="notice-preview-meta">
-        <el-tag size="small" :type="previewNoticeType === '1' ? 'warning' : 'success'">
-          {{ previewNoticeType === '1' ? '通知' : '公告' }}
-        </el-tag>
-        <span class="notice-preview-info">
-          <el-icon><User /></el-icon> {{ previewCreateBy }}
-        </span>
-        <span class="notice-preview-info">
-          <el-icon><Timer /></el-icon> {{ previewCreateTime }}
-        </span>
-      </div>
-      <div class="notice-preview-divider"></div>
-      <div class="notice-preview-content" v-html="previewContent"></div>
-    </el-dialog>
+    <notice-detail-view ref="noticeViewRef" />
   </div>
 </template>
 
 <script setup>
-import { listNoticeTop, markNoticeRead, markNoticeReadAll, getNotice } from '@/api/system/notice'
+import NoticeDetailView from './DetailView'
+import { listNoticeTop, markNoticeRead, markNoticeReadAll } from '@/api/system/notice'
 
 const noticePopover = ref(null)
 const noticeList = ref([])
@@ -60,12 +47,7 @@ const unreadCount = ref(0)
 const noticeLoading = ref(false)
 const noticeVisible = ref(false)
 const noticeLeaveTimer = ref(null)
-const previewVisible = ref(false)
-const previewTitle = ref('')
-const previewContent = ref('')
-const previewNoticeType = ref('')
-const previewCreateBy = ref('')
-const previewCreateTime = ref('')
+const { proxy } = getCurrentInstance()
 
 // 加载顶部公告列表
 function loadNoticeTop() {
@@ -109,15 +91,7 @@ function previewNotice(item) {
     if (idx !== -1) noticeList.value[idx] = { ...item, isRead: true }
     unreadCount.value = Math.max(0, unreadCount.value - 1)
   }
-  getNotice(item.noticeId).then(res => {
-    const notice = res.data
-    previewTitle.value = notice.noticeTitle
-    previewContent.value = notice.noticeContent
-    previewNoticeType.value = notice.noticeType
-    previewCreateBy.value = notice.createBy
-    previewCreateTime.value = notice.createTime
-    previewVisible.value = true
-  })
+  proxy.$refs["noticeViewRef"].open(item.noticeId)
 }
 
 // 全部已读
@@ -208,30 +182,3 @@ function markAllRead() {
   color: #bbb;
 }
 </style>
-
-<style>
-.notice-preview-dialog .el-dialog__body { padding: 0 10px 10px; }
-.notice-preview-dialog .notice-preview-meta {
-  display: flex;
-  align-items: center;
-  gap: 14px;
-  padding: 12px 0;
-  font-size: 12px;
-  color: #888;
-}
-.notice-preview-dialog .notice-preview-info { display: flex; align-items: center; gap: 4px; }
-.notice-preview-dialog .notice-preview-divider {
-  height: 1px;
-  background: linear-gradient(to right, transparent, #e2e8f0, transparent);
-  margin-bottom: 16px;
-}
-.notice-preview-dialog .notice-preview-content {
-  font-size: 14px;
-  line-height: 1.85;
-  color: #2d3748;
-  word-break: break-word;
-}
-.notice-preview-dialog .notice-preview-content img { max-width: 100%; border-radius: 4px; }
-.notice-preview-dialog .notice-preview-content p { margin: 0 0 1em; }
-.notice-preview-dialog .notice-preview-content a { color: var(--el-color-primary); text-decoration: underline; }
-</style>

+ 12 - 6
src/views/system/notice/index.vue

@@ -71,12 +71,11 @@
       <el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
          <el-table-column type="selection" width="55" align="center" />
          <el-table-column label="序号" align="center" prop="noticeId" width="100" />
-         <el-table-column
-            label="公告标题"
-            align="center"
-            prop="noticeTitle"
-            :show-overflow-tooltip="true"
-         />
+         <el-table-column label="公告标题" align="center" :show-overflow-tooltip="true">
+            <template #default="scope">
+               <a class="link-type" style="cursor:pointer" @click="handleViewData(scope.row)">{{ scope.row.noticeTitle }}</a>
+            </template>
+         </el-table-column>
          <el-table-column label="公告类型" align="center" prop="noticeType" width="100">
             <template #default="scope">
                <dict-tag :options="sys_notice_type" :value="scope.row.noticeType" />
@@ -155,10 +154,12 @@
             </div>
          </template>
       </el-dialog>
+      <notice-detail-view ref="noticeViewRef" />
    </div>
 </template>
 
 <script setup name="Notice">
+import NoticeDetailView from "@/layout/components/HeaderNotice/DetailView"
 import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice"
 
 const { proxy } = getCurrentInstance()
@@ -277,6 +278,11 @@ function submitForm() {
   })
 }
 
+/** 查看公告详情 */
+function handleViewData(row) {
+  proxy.$refs["noticeViewRef"].open(row)
+}
+
 /** 删除按钮操作 */
 function handleDelete(row) {
   const noticeIds = row.noticeId || ids.value