Browse Source

✨ feat: 新增公告管理板块并接入弹窗公告

将弹窗公告从网站管理拆分到独立公告管理板块,新增横幅公告管理页面,并在默认布局接入首页弹窗读取与已读上报。

Made-with: Cursor
Pchen0 1 month ago
parent
commit
4f3e293c2c

+ 88 - 1
src/api/public.js

@@ -2,7 +2,14 @@ import request from '../utils/request'
 
 
 const api = {
 const api = {
   AppVersion: '/Public/GetAppVersion',
   AppVersion: '/Public/GetAppVersion',
-  GetNotice: '/Public/GetNotice'
+  GetNotice: '/Public/GetNotice',
+  PopupUnread: '/Popup/Unread',
+  PopupMarkRead: '/Popup/MarkRead',
+  AdminPopupList: '/Admin/Popup/List',
+  AdminPopup: '/Admin/Popup',
+  AdminPopupReadList: '/Admin/Popup/ReadList',
+  AdminNoticeList: '/Admin/Notice/List',
+  AdminNotice: '/Admin/Notice'
 }
 }
 
 
 export function getAppVersion() {
 export function getAppVersion() {
@@ -19,3 +26,83 @@ export function GetNotice(parameter) {
     params: parameter
     params: parameter
   })
   })
 }
 }
+
+export function getUnreadPopup(parameter) {
+  return request({
+    url: api.PopupUnread,
+    method: 'get',
+    params: parameter
+  })
+}
+
+export function markPopupRead(parameter) {
+  return request({
+    url: api.PopupMarkRead,
+    method: 'post',
+    data: parameter
+  })
+}
+
+export function getAdminPopupList(parameter) {
+  return request({
+    url: api.AdminPopupList,
+    method: 'get',
+    params: parameter
+  })
+}
+
+export function createAdminPopup(parameter) {
+  return request({
+    url: api.AdminPopup,
+    method: 'post',
+    data: parameter
+  })
+}
+
+export function updateAdminPopup(parameter) {
+  return request({
+    url: api.AdminPopup,
+    method: 'put',
+    data: parameter
+  })
+}
+
+export function deleteAdminPopup(parameter) {
+  return request({
+    url: api.AdminPopup,
+    method: 'delete',
+    data: parameter
+  })
+}
+
+export function getAdminPopupReadList(parameter) {
+  return request({
+    url: api.AdminPopupReadList,
+    method: 'get',
+    params: parameter
+  })
+}
+
+export function getAdminNoticeList(parameter) {
+  return request({
+    url: api.AdminNoticeList,
+    method: 'get',
+    params: parameter
+  })
+}
+
+export function saveAdminNotice(parameter) {
+  return request({
+    url: api.AdminNotice,
+    method: 'post',
+    data: parameter
+  })
+}
+
+export function deleteAdminNotice(parameter) {
+  return request({
+    url: api.AdminNotice,
+    method: 'delete',
+    data: parameter
+  })
+}

+ 131 - 0
src/layout/default-layout.vue

@@ -18,12 +18,25 @@
     </a-avatar>
     </a-avatar>
 
 
     <AIChat :visible="chat"/> -->
     <AIChat :visible="chat"/> -->
+
+    <a-modal
+      v-model:visible="popupVisible"
+      :title="popupData.title || '公告'"
+      :footer="false"
+      width="760px"
+      modal-class="home-popup-modal"
+      @cancel="handlePopupClose"
+    >
+      <div class="popup-html" v-html="popupData.content_html"></div>
+    </a-modal>
   </a-layout>
   </a-layout>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 import { onMounted, onBeforeUnmount, ref, onUnmounted } from 'vue'
 import { onMounted, onBeforeUnmount, ref, onUnmounted } from 'vue'
 import { eventBus } from '@/utils/eventBus'
 import { eventBus } from '@/utils/eventBus'
+import { Notification } from '@arco-design/web-vue'
+import { getUnreadPopup, markPopupRead } from '@/api/public'
 
 
 import PageLayout from './page-layout.vue'
 import PageLayout from './page-layout.vue'
 import Menu from '@/components/Menu/index.vue'
 import Menu from '@/components/Menu/index.vue'
@@ -32,6 +45,8 @@ import Footer from '@/components/Footer/index.vue'
 // import AIChat from '@/components/AIChat/index.vue'
 // import AIChat from '@/components/AIChat/index.vue'
 
 
 const chat = ref(false)
 const chat = ref(false)
+const popupVisible = ref(false)
+const popupData = ref({})
 
 
 // 按钮拖动
 // 按钮拖动
 let isDragging = false
 let isDragging = false
@@ -72,6 +87,7 @@ onMounted(() => {
     button.addEventListener('mousedown', handleMouseDown)
     button.addEventListener('mousedown', handleMouseDown)
   }
   }
   eventBus.on('closeAI', () => { chat.value = false })
   eventBus.on('closeAI', () => { chat.value = false })
+  getPopup()
 })
 })
 
 
 onBeforeUnmount(() => {
 onBeforeUnmount(() => {
@@ -84,6 +100,30 @@ onUnmounted(() => {
   eventBus.off('closeAI', () => { chat.value = false })
   eventBus.off('closeAI', () => { chat.value = false })
 })
 })
 
 
+const getPopup = async () => {
+  try {
+    const res = await getUnreadPopup({ limit: 1 })
+    if (!res || res.code !== 0) return
+    const list = res.data || []
+    if (!list.length) return
+    popupData.value = list[0]
+    popupVisible.value = true
+  } catch (error) {
+    Notification.error({
+      title: '获取首页公告失败',
+      content: error.message || '请稍后再试'
+    })
+  }
+}
+
+const handlePopupClose = async () => {
+  popupVisible.value = false
+  if (!popupData.value?.id) return
+  try {
+    await markPopupRead({ popup_id: popupData.value.id })
+  } catch (_) {}
+}
+
 </script>
 </script>
 
 
 <style scoped lang="less">
 <style scoped lang="less">
@@ -158,6 +198,27 @@ onUnmounted(() => {
   overflow-y: auto;
   overflow-y: auto;
   background-color: var(--color-fill-2);
   background-color: var(--color-fill-2);
   transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
   transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
+
+  &::-webkit-scrollbar {
+    width: 10px;
+    height: 10px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    border: 2px solid transparent;
+    border-radius: 8px;
+    background-clip: padding-box;
+    background-color: rgba(22, 93, 255, 0.28);
+    transition: background-color 0.2s ease;
+  }
+
+  &::-webkit-scrollbar-thumb:hover {
+    background-color: rgba(22, 93, 255, 0.46);
+  }
 }
 }
 
 
 .aiButton {
 .aiButton {
@@ -175,4 +236,74 @@ onUnmounted(() => {
     transform: scale(1.15);
     transform: scale(1.15);
   }
   }
 }
 }
+
+.popup-html {
+  margin-top: 0;
+  padding-right: 4px;
+  max-height: min(56vh, 520px);
+  overflow-y: auto;
+  line-height: 1.7;
+  color: var(--color-text-1);
+
+  &::-webkit-scrollbar {
+    width: 8px;
+    height: 6px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    border-radius: 6px;
+    background-color: rgba(134, 144, 156, 0.35);
+  }
+
+  &::-webkit-scrollbar-thumb:horizontal {
+    border-radius: 999px;
+  }
+
+  &::-webkit-scrollbar-thumb:hover {
+    background-color: rgba(134, 144, 156, 0.55);
+  }
+}
+
+:deep(.home-popup-modal) {
+  border-radius: 14px;
+  box-shadow: 0 14px 40px rgba(15, 35, 95, 0.16);
+  overflow: hidden;
+
+  .arco-modal-header {
+    margin-bottom: 0;
+    padding: 18px 24px 14px;
+    border-bottom: 1px solid var(--color-border-2);
+    background: linear-gradient(180deg, rgba(22, 93, 255, 0.06) 0%, rgba(22, 93, 255, 0) 100%);
+  }
+
+  .arco-modal-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: var(--color-text-1);
+  }
+
+  .arco-modal-body {
+    padding: 18px 24px 22px;
+  }
+
+  .arco-modal-close-btn {
+    top: 14px;
+    right: 14px;
+    width: 28px;
+    height: 28px;
+    border-radius: 8px;
+    color: var(--color-text-2);
+    background-color: rgba(240, 243, 245, 0.9);
+    transition: all 0.2s ease;
+  }
+
+  .arco-modal-close-btn:hover {
+    color: var(--color-text-1);
+    background-color: rgba(229, 233, 237, 1);
+  }
+}
 </style>
 </style>

+ 174 - 0
src/pages/admin/notice/index.vue

@@ -0,0 +1,174 @@
+<template>
+  <div class="container">
+    <Breadcrumb :items="['公告管理', '横幅公告管理']" />
+    <a-card title="横幅公告管理">
+      <a-space style="margin-bottom: 12px;">
+        <a-input v-model="query.keyword" placeholder="按标识或内容搜索" allow-clear style="width: 280px" />
+        <a-button type="primary" @click="search">搜索</a-button>
+        <a-button @click="openCreate">新增公告</a-button>
+      </a-space>
+
+      <a-table
+        :data="data"
+        :loading="loading"
+        :pagination="{
+          showPageSize: true,
+          showJumper: true,
+          showTotal: true,
+          pageSize: pagination.pagesize,
+          current: pagination.current,
+          total: pagination.total
+        }"
+        @page-change="onPageChange"
+        @page-size-change="onPageSizeChange"
+      >
+        <template #columns>
+          <a-table-column title="公告标识" data-index="key" :width="220" />
+          <a-table-column title="内容" data-index="content" ellipsis tooltip />
+          <a-table-column title="操作" :width="180">
+            <template #cell="{ record }">
+              <a-space>
+                <a-button size="mini" @click="openEdit(record)">编辑</a-button>
+                <a-button status="danger" size="mini" @click="remove(record)">删除</a-button>
+              </a-space>
+            </template>
+          </a-table-column>
+        </template>
+      </a-table>
+    </a-card>
+
+    <a-modal v-model:visible="editorVisible" :title="form.mode === 'create' ? '新增横幅公告' : '编辑横幅公告'" @before-ok="submitEditor">
+      <a-form :model="form" layout="vertical">
+        <a-form-item label="公告标识(key)">
+          <a-input v-model="form.key" :disabled="form.mode === 'edit'" allow-clear />
+        </a-form-item>
+        <a-form-item label="公告内容">
+          <a-textarea v-model="form.content" :max-length="{ length: 1000 }" show-word-limit allow-clear />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
+</template>
+
+<script setup>
+import { reactive, ref, onMounted } from 'vue'
+import { Modal, Notification, Message } from '@arco-design/web-vue'
+import { getAdminNoticeList, saveAdminNotice, deleteAdminNotice } from '@/api/public'
+
+const loading = ref(false)
+const data = ref([])
+const query = reactive({ keyword: '' })
+const pagination = reactive({ total: 0, current: 1, pagesize: 20 })
+
+const editorVisible = ref(false)
+const form = reactive({
+  mode: 'create',
+  key: '',
+  content: ''
+})
+
+const getList = async () => {
+  try {
+    loading.value = true
+    const res = await getAdminNoticeList({
+      keyword: query.keyword,
+      pagesize: pagination.pagesize,
+      current: pagination.current
+    })
+    if (!res || res.code !== 0) {
+      return Notification.error({
+        title: '获取横幅公告失败',
+        content: res?.msg ?? '请稍后再试'
+      })
+    }
+    data.value = Array.isArray(res.data) ? res.data : []
+    pagination.total = res.pagination?.total || 0
+  } catch (error) {
+    Notification.error({
+      title: '获取横幅公告失败',
+      content: error.message || '请稍后再试'
+    })
+  } finally {
+    loading.value = false
+  }
+}
+
+const search = () => {
+  pagination.current = 1
+  getList()
+}
+
+const openCreate = () => {
+  form.mode = 'create'
+  form.key = ''
+  form.content = ''
+  editorVisible.value = true
+}
+
+const openEdit = (record) => {
+  form.mode = 'edit'
+  form.key = record.key
+  form.content = record.content
+  editorVisible.value = true
+}
+
+const submitEditor = async () => {
+  if (!form.key || !form.content) {
+    Message.error('请填写完整公告信息')
+    return false
+  }
+  const res = await saveAdminNotice({
+    key: form.key,
+    content: form.content
+  })
+  if (!res || res.code !== 0) {
+    Notification.error({
+      title: '保存横幅公告失败',
+      content: res?.msg ?? '请稍后再试'
+    })
+    return false
+  }
+  Message.success('保存成功')
+  getList()
+  return true
+}
+
+const remove = (record) => {
+  Modal.confirm({
+    title: '删除横幅公告',
+    content: `确定删除公告标识「${record.key}」吗?`,
+    onOk: async () => {
+      const res = await deleteAdminNotice({ key: record.key })
+      if (!res || res.code !== 0) {
+        return Notification.error({
+          title: '删除横幅公告失败',
+          content: res?.msg ?? '请稍后再试'
+        })
+      }
+      Message.success('删除成功')
+      getList()
+    }
+  })
+}
+
+const onPageChange = (page) => {
+  pagination.current = page
+  getList()
+}
+
+const onPageSizeChange = (size) => {
+  pagination.pagesize = size
+  pagination.current = 1
+  getList()
+}
+
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped>
+.container {
+  padding: 0 20px 20px 20px;
+}
+</style>

+ 285 - 0
src/pages/admin/popup/index.vue

@@ -0,0 +1,285 @@
+<template>
+  <div class="container">
+    <Breadcrumb :items="['公告管理', '首页弹窗公告']" />
+    <a-card title="首页弹窗公告">
+      <a-space style="margin-bottom: 12px;">
+        <a-button type="primary" @click="openCreate">新增公告</a-button>
+      </a-space>
+
+      <a-table
+        :data="data"
+        :loading="loading"
+        :pagination="{
+          showPageSize: true,
+          showJumper: true,
+          showTotal: true,
+          pageSize: pagination.pagesize,
+          current: pagination.current,
+          total: pagination.total
+        }"
+        @page-change="onPageChange"
+        @page-size-change="onPageSizeChange"
+      >
+        <template #columns>
+          <a-table-column title="ID" data-index="id" :width="80" />
+          <a-table-column title="标题" data-index="title" :width="220" ellipsis tooltip />
+          <a-table-column title="优先级" data-index="priority" :width="100" />
+          <a-table-column title="状态" :width="100">
+            <template #cell="{ record }">
+              <a-tag :color="record.is_active ? 'green' : 'red'">{{ record.is_active ? '启用' : '停用' }}</a-tag>
+            </template>
+          </a-table-column>
+          <a-table-column title="连续展示" :width="110">
+            <template #cell="{ record }">
+              <a-tag :color="record.repeat_show ? 'arcoblue' : 'gray'">{{ record.repeat_show ? '开启' : '关闭' }}</a-tag>
+            </template>
+          </a-table-column>
+          <a-table-column title="已读人数" data-index="read_count" :width="100" />
+          <a-table-column title="生效时间" :width="180">
+            <template #cell="{ record }">{{ record.start_at || '-' }}</template>
+          </a-table-column>
+          <a-table-column title="失效时间" :width="180">
+            <template #cell="{ record }">{{ record.end_at || '-' }}</template>
+          </a-table-column>
+          <a-table-column title="操作" :width="260">
+            <template #cell="{ record }">
+              <a-space>
+                <a-button size="mini" @click="openEdit(record)">编辑</a-button>
+                <a-button size="mini" @click="openReadList(record)">已读列表</a-button>
+                <a-button status="danger" size="mini" @click="remove(record)">删除</a-button>
+              </a-space>
+            </template>
+          </a-table-column>
+        </template>
+      </a-table>
+    </a-card>
+
+    <a-modal v-model:visible="editorVisible" :title="form.id ? '编辑公告' : '新增公告'" width="980px" @before-ok="submitEditor">
+      <a-form :model="form" layout="vertical">
+        <a-form-item label="标题">
+          <a-input v-model="form.title" allow-clear />
+        </a-form-item>
+        <a-form-item label="优先级">
+          <a-input-number v-model="form.priority" mode="button" />
+        </a-form-item>
+        <a-form-item label="状态">
+          <a-switch v-model="form.is_active" :checked-value="1" :unchecked-value="0" />
+        </a-form-item>
+        <a-form-item label="连续展示(已读后仍展示)">
+          <a-switch v-model="form.repeat_show" :checked-value="1" :unchecked-value="0" />
+        </a-form-item>
+        <a-form-item label="生效时间">
+          <a-date-picker v-model="form.start_at" show-time format="YYYY-MM-DD HH:mm:ss" value-format="x" allow-clear />
+        </a-form-item>
+        <a-form-item label="失效时间">
+          <a-date-picker v-model="form.end_at" show-time format="YYYY-MM-DD HH:mm:ss" value-format="x" allow-clear />
+        </a-form-item>
+        <a-form-item label="公告内容(富文本)">
+          <WangEditor v-model="form.content_html" />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+
+    <a-modal v-model:visible="readVisible" title="已读用户" :footer="false" width="860px" draggable>
+      <a-table
+        :data="readData"
+        :columns="readColumns"
+        :loading="readLoading"
+        :pagination="false"
+        :scroll="{ y: 420 }"
+      >
+        <template #avatar="{ record }">
+          <a-avatar :size="28">
+            <img :src="record.avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
+          </a-avatar>
+          {{ record.username || '-' }}
+        </template>
+        <template #read_at="{ record }">{{ formatTime(record.read_at) }}</template>
+        <template #empty>
+          <a-empty description="暂无已读记录" />
+        </template>
+      </a-table>
+    </a-modal>
+  </div>
+</template>
+
+<script setup>
+import { reactive, ref, onMounted } from 'vue'
+import { Modal, Notification, Message } from '@arco-design/web-vue'
+import WangEditor from '@/components/Editor/WangEditor.vue'
+import {
+  getAdminPopupList,
+  createAdminPopup,
+  updateAdminPopup,
+  deleteAdminPopup,
+  getAdminPopupReadList
+} from '@/api/public'
+
+const loading = ref(false)
+const data = ref([])
+const pagination = reactive({ total: 0, current: 1, pagesize: 20 })
+
+const editorVisible = ref(false)
+const form = reactive({
+  id: null,
+  title: '',
+  content_html: '',
+  priority: 0,
+  is_active: 1,
+  repeat_show: 0,
+  start_at: '',
+  end_at: ''
+})
+
+const readVisible = ref(false)
+const readLoading = ref(false)
+const readData = ref([])
+const readColumns = [
+  { title: '用户', slotName: 'avatar', width: 140 },
+  { title: 'UUID', dataIndex: 'user_uuid', width: 220, ellipsis: true, tooltip: true },
+  { title: '已读时间', slotName: 'read_at', width: 180 }
+]
+
+const formatTime = (time) => new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
+
+const getList = async () => {
+  try {
+    loading.value = true
+    const res = await getAdminPopupList({
+      pagesize: pagination.pagesize,
+      current: pagination.current
+    })
+    if (!res || res.code !== 0) {
+      return Notification.error({
+        title: '获取公告列表失败',
+        content: res?.msg ?? '请稍后再试'
+      })
+    }
+    data.value = res.data || []
+    pagination.total = res.pagination?.total || 0
+  } catch (error) {
+    Notification.error({
+      title: '获取公告列表失败',
+      content: error.message || '请稍后再试'
+    })
+  } finally {
+    loading.value = false
+  }
+}
+
+const openCreate = () => {
+  form.id = null
+  form.title = ''
+  form.content_html = ''
+  form.priority = 0
+  form.is_active = 1
+  form.repeat_show = 0
+  form.start_at = ''
+  form.end_at = ''
+  editorVisible.value = true
+}
+
+const openEdit = (record) => {
+  form.id = record.id
+  form.title = record.title
+  form.content_html = record.content_html
+  form.priority = record.priority
+  form.is_active = record.is_active
+  form.repeat_show = record.repeat_show ? 1 : 0
+  form.start_at = record.start_at ? new Date(record.start_at).getTime() : ''
+  form.end_at = record.end_at ? new Date(record.end_at).getTime() : ''
+  editorVisible.value = true
+}
+
+const submitEditor = async () => {
+  if (!form.title || !form.content_html) {
+    Message.error('请填写标题和内容')
+    return false
+  }
+  const payload = {
+    id: form.id,
+    title: form.title,
+    content_html: form.content_html,
+    priority: form.priority,
+    is_active: form.is_active,
+    repeat_show: form.repeat_show,
+    start_at: form.start_at,
+    end_at: form.end_at
+  }
+  const res = form.id ? await updateAdminPopup(payload) : await createAdminPopup(payload)
+  if (!res || res.code !== 0) {
+    Notification.error({
+      title: form.id ? '更新公告失败' : '创建公告失败',
+      content: res?.msg ?? '请稍后再试'
+    })
+    return false
+  }
+  Message.success(form.id ? '更新成功' : '创建成功')
+  getList()
+  return true
+}
+
+const remove = (record) => {
+  Modal.confirm({
+    title: '删除公告',
+    content: `确定删除公告「${record.title}」吗?`,
+    onOk: async () => {
+      const res = await deleteAdminPopup({ id: record.id })
+      if (!res || res.code !== 0) {
+        return Notification.error({
+          title: '删除公告失败',
+          content: res?.msg ?? '请稍后再试'
+        })
+      }
+      Message.success('删除成功')
+      getList()
+    }
+  })
+}
+
+const openReadList = async (record) => {
+  readVisible.value = true
+  readLoading.value = true
+  readData.value = []
+  try {
+    const res = await getAdminPopupReadList({
+      popup_id: record.id,
+      pagesize: 200,
+      current: 1
+    })
+    if (!res || res.code !== 0)
+      return Notification.error({
+        title: '获取已读列表失败',
+        content: res?.msg ?? '请稍后再试'
+      })
+    readData.value = Array.isArray(res.data) ? res.data : []
+  } catch (error) {
+    Notification.error({
+      title: '获取已读列表失败',
+      content: error.message || '请稍后再试'
+    })
+  } finally {
+    readLoading.value = false
+  }
+}
+
+const onPageChange = (page) => {
+  pagination.current = page
+  getList()
+}
+const onPageSizeChange = (size) => {
+  pagination.pagesize = size
+  pagination.current = 1
+  getList()
+}
+
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped>
+.container {
+  padding: 0 20px 20px 20px;
+}
+</style>

+ 48 - 0
src/router/index.js

@@ -306,6 +306,15 @@ const routes = [
                     permission: ['admin', 'service']
                     permission: ['admin', 'service']
                 }
                 }
             },
             },
+            {
+                path: 'lepaoBindAudit',
+                name: 'admin.lepaoBindAudit',
+                component: () => import('../pages/admin/lepaoBindAudit/index.vue'),
+                meta: {
+                    title: '绑定记录',
+                    permission: ['admin', 'service']
+                }
+            },
             {
             {
                 path: 'lepaoCountLedger',
                 path: 'lepaoCountLedger',
                 name: 'admin.lepaoCountLedger',
                 name: 'admin.lepaoCountLedger',
@@ -325,6 +334,14 @@ const routes = [
                     permission: ['admin', 'service']
                     permission: ['admin', 'service']
                 }
                 }
             },
             },
+            {
+                path: 'popup',
+                redirect: '/noticeManage/popup',
+                meta: {
+                    hideInMenu: true,
+                    permission: ['admin', 'service']
+                }
+            },
             {
             {
                 path: 'service/orderList',
                 path: 'service/orderList',
                 name: 'admin.service.orderList',
                 name: 'admin.service.orderList',
@@ -373,6 +390,37 @@ const routes = [
             }
             }
         ]
         ]
     },
     },
+    {
+        path: '/noticeManage',
+        name: 'noticeManage',
+        redirect: '/noticeManage/notice',
+        component: DEFAULT_LAYOUT,
+        meta: {
+            title: '公告管理',
+            icon: 'icon-notification',
+            permission: ['admin', 'service']
+        },
+        children: [
+            {
+                path: 'notice',
+                name: 'noticeManage.notice',
+                component: () => import('../pages/admin/notice/index.vue'),
+                meta: {
+                    title: '横幅公告',
+                    permission: ['admin', 'service']
+                }
+            },
+            {
+                path: 'popup',
+                name: 'noticeManage.popup',
+                component: () => import('../pages/admin/popup/index.vue'),
+                meta: {
+                    title: '弹窗公告',
+                    permission: ['admin', 'service']
+                }
+            }
+        ]
+    },
     {
     {
         path: '/goodsManage',
         path: '/goodsManage',
         name: 'goodsManage',
         name: 'goodsManage',