浏览代码

feature(公告): 通知公告

ZZH-wl 2 年之前
父节点
当前提交
5a95907c61

+ 2 - 0
package.json

@@ -14,6 +14,8 @@
   },
   "dependencies": {
     "@riophae/vue-treeselect": "^0.4.0",
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "^1.0.2",
     "await-to-js": "^3.0.0",
     "axios": "^0.24.0",
     "core-js": "^3.19.3",

+ 116 - 0
src/App.vue

@@ -5,7 +5,123 @@
 </template>
 
 <script>
+  import { mapGetters } from 'vuex'
+  import messageApi from '@/api/system/message'
+
   export default {
     name: 'App',
+    data() {
+      return {
+        url: 'ws://127.0.0.1:8899/ws?uid=',
+        ws: null,
+        interValId: '',
+        timerId: '',
+      }
+    },
+    created() {
+      this.connectWebsocket()
+    },
+    destroyed() {
+      clearInterval(this.interValId)
+    },
+    computed: {
+      ...mapGetters({
+        id: 'user/id',
+        username: 'user/username',
+      }),
+    },
+    methods: {
+      connectWebsocket() {
+        if (this.id) {
+          this.ws = new WebSocket(this.url + this.id)
+        } else {
+          this.ws = null
+          console.info('uid为空')
+        }
+        try {
+          if (this.ws) {
+            // ws连接成功
+            this.ws.onopen = function () {
+              console.info('WebSocket Server [' + this.url + '] 连接成功!')
+            }
+            // ws连接关闭
+            this.ws.onclose = this.onClose
+            // ws连接错误
+            this.ws.onerror = this.onError
+            // ws数据返回处理
+            this.ws.onmessage = this.onMessage
+          }
+        } catch (e) {
+          console.error(e)
+        }
+
+        this.interValId = setInterval(this.heartAndReconnect, 10000)
+      },
+      // 心跳且重新
+      heartAndReconnect() {
+        if (this.ws) {
+          let data = {
+            type: 'heartbeat',
+          }
+          this.ws.send(JSON.stringify(data))
+        } else {
+          clearInterval(this.interValId)
+          this.connectWebsocket()
+        }
+      },
+      // 关闭
+      onClose() {
+        if (this.ws) {
+          this.ws.close()
+          this.ws = null
+        }
+        console.info('WebSocket Server [' + this.url + '] 连接关闭!')
+      },
+      // 处理链接错误
+      onError() {
+        if (this.ws) {
+          this.ws.close()
+          this.ws = null
+        }
+        console.info('WebSocket Server [' + this.url + '] 连接错误!')
+      },
+      onMessage(result) {
+        console.info(' > ' + result.data)
+        this.showMessage(result.data)
+      },
+      // 打开消息弹窗
+      showMessage(item) {
+        let data = JSON.parse(item)
+        if (data.type === 'SendMessage' && data.content.body) {
+          this.$baseEventBus.$emit('receivedMessage')
+          let msg = JSON.parse(item).content.body
+          const h = this.$createElement
+          let tag = {
+            style: {
+              cursor: 'pointer',
+            },
+            on: {
+              click: () => {
+                // this.handleItem(data.content.body)
+              },
+            },
+          }
+          this.$notify({
+            title: msg.msgTitle,
+            position: 'top-right',
+            type: 'warning',
+            duration: 5000,
+            dangerouslyUseHTMLString: true,
+            message: h('div', {}, [h('div', tag, msg.msgContent)]),
+          })
+        }
+      },
+      // 执行路由跳转
+      handleItem(item) {
+        messageApi.getEntityById({ id: item.id }).then(() => {
+          this.$baseEventBus.$emit('receivedMessage')
+        })
+      },
+    },
   }
 </script>

+ 8 - 0
src/api/notice.js

@@ -0,0 +1,8 @@
+import request from '@/utils/request'
+
+export function getList() {
+  return request({
+    url: '/notice/getList',
+    method: 'get',
+  })
+}

+ 21 - 0
src/api/system/message.js

@@ -0,0 +1,21 @@
+import micro_request from '@/utils/micro_request'
+
+const basePath = process.env.VUE_APP_AdminPath
+export default {
+  // 获取列表
+  getList(query) {
+    return micro_request.postRequest(basePath, 'Message', 'GetList', query)
+  },
+  getEntityById(query) {
+    return micro_request.postRequest(basePath, 'Message', 'GetEntityById', query)
+  },
+  doAdd(query) {
+    return micro_request.postRequest(basePath, 'Message', 'Create', query)
+  },
+  doEdit(query) {
+    return micro_request.postRequest(basePath, 'Message', 'UpdateById', query)
+  },
+  doDelete(query) {
+    return micro_request.postRequest(basePath, 'Message', 'DeleteByIds', query)
+  },
+}

+ 1 - 1
src/utils/request.js

@@ -85,7 +85,7 @@ const handleData = async ({ data, status = 0, statusText }) => {
 const instance = axios.create({
   // baseURL,
   // eslint-disable-next-line no-undef
-  baseURL: $GlobalConfig.baseUrl,
+  baseURL: '/vab-mock-server',
   timeout: requestTimeout,
   headers: {
     'Content-Type': contentType,

+ 3 - 5
src/vab/components/VabAvatar/index.vue

@@ -3,11 +3,8 @@
     <span class="avatar-dropdown">
       <el-avatar class="user-avatar" :src="avatar" />
       <div class="user-name">
-        <span class="hidden-xs-only">{{ username }}</span>
-        <vab-icon
-          class="vab-dropdown"
-          :class="{ 'vab-dropdown-active': active }"
-          icon="arrow-down-s-line" />
+        <span class="hidden-xs-only">{{ nickName }}</span>
+        <vab-icon class="vab-dropdown" :class="{ 'vab-dropdown-active': active }" icon="arrow-down-s-line" />
       </div>
     </span>
     <template #dropdown>
@@ -37,6 +34,7 @@
       ...mapGetters({
         avatar: 'user/avatar',
         username: 'user/username',
+        nickName: 'user/nickName',
       }),
     },
     methods: {

+ 5 - 7
src/vab/components/VabNav/index.vue

@@ -28,8 +28,9 @@
       <el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="20">
         <div class="right-panel">
           <vab-error-log />
-          <vab-full-screen />
-          <vab-language />
+          <!--          <vab-full-screen />-->
+          <!--          <vab-language />-->
+          <!--          <vab-notice />-->
           <vab-refresh />
           <vab-avatar />
         </div>
@@ -62,9 +63,7 @@
         routes: 'routes/routes',
       }),
       handleRoutes() {
-        return this.routes.filter(
-          (route) => route.meta && route.meta.hidden !== true
-        )
+        return this.routes.filter((route) => route.meta && route.meta.hidden !== true)
       },
       handleActiveMenu() {
         return this.routes.find((route) => route.name === this.extra.first)
@@ -89,8 +88,7 @@
     methods: {
       translateTitle,
       handleTabClick(handler) {
-        if (handler !== true && openFirstMenu)
-          this.$router.push(this.handleActiveMenu)
+        if (handler !== true && openFirstMenu) this.$router.push(this.handleActiveMenu)
       },
     },
   }

+ 137 - 0
src/vab/components/VabNotice/index.vue

@@ -0,0 +1,137 @@
+<template>
+  <el-badge v-if="theme.showNotice" :value="badge">
+    <el-popover placement="bottom" trigger="hover" width="300">
+      <template #reference>
+        <vab-icon icon="notification-line" />
+      </template>
+      <el-tabs v-model="activeName" @tab-click="handleClick">
+        <el-tab-pane :label="translateTitle('通知')" name="notice">
+          <div class="notice-list">
+            <el-scrollbar>
+              <ul>
+                <li v-for="(item, index) in list" :key="index">
+                  <el-avatar :size="45" :src="item.image" />
+                  <span v-html="item.notice" />
+                </li>
+              </ul>
+            </el-scrollbar>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane :label="translateTitle('邮件')" name="email">
+          <div class="notice-list">
+            <el-scrollbar>
+              <ul>
+                <li v-for="(item, index) in list" :key="index">
+                  <el-avatar :size="45" :src="item.image" />
+                  <span>{{ item.email }}</span>
+                </li>
+              </ul>
+            </el-scrollbar>
+          </div>
+        </el-tab-pane>
+      </el-tabs>
+      <div class="notice-clear" @click="handleClearNotice">
+        <el-button type="text">
+          <vab-icon icon="close-circle-line" />
+          <span>{{ translateTitle('清空消息') }}</span>
+        </el-button>
+      </div>
+    </el-popover>
+  </el-badge>
+</template>
+
+<script>
+  import { mapGetters } from 'vuex'
+  import { translateTitle } from '@/utils/i18n'
+  import { getList } from '@/api/notice'
+
+  export default {
+    name: 'VabNotice',
+    data() {
+      return {
+        list: [],
+        badge: null,
+        activeName: 'notice',
+      }
+    },
+    computed: {
+      ...mapGetters({
+        theme: 'settings/theme',
+      }),
+    },
+    created() {
+      this.$nextTick(() => {
+        if (this.theme.showNotice) this.fetchData()
+      })
+    },
+    methods: {
+      translateTitle,
+      handleClick() {
+        this.fetchData()
+      },
+      handleClearNotice() {
+        this.badge = null
+        this.list = []
+        this.$baseMessage('清空消息成功', 'success', 'vab-hey-message-success')
+      },
+      async fetchData() {
+        const {
+          data: { list, total },
+        } = await getList()
+        this.list = list
+        this.badge = total === 0 ? null : total
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  :deep() {
+    .el-tabs__active-bar {
+      min-width: 28px;
+    }
+  }
+
+  .notice-list {
+    height: 29vh;
+
+    ul {
+      padding: 0 15px 0 0;
+      margin: 0;
+
+      li {
+        display: flex;
+        align-items: center;
+        padding: 10px 0 10px 0;
+
+        :deep() {
+          .el-avatar {
+            flex-shrink: 0;
+            width: 50px;
+            height: 50px;
+            border-radius: 50%;
+          }
+        }
+
+        span {
+          margin-left: 10px;
+        }
+      }
+    }
+  }
+
+  .notice-clear {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 10px 0 0 0;
+    font-size: 14px;
+    text-align: center;
+    cursor: pointer;
+    border-top: 1px solid #e8eaec;
+
+    i {
+      margin-right: 3px;
+    }
+  }
+</style>

+ 80 - 4
src/views/index/index.vue

@@ -179,14 +179,14 @@
           </div>
         </div>
         <ul>
-          <li v-for="(activity, index) in activities" :key="index" class="notice-list">
+          <li v-for="(message, index) in messageList" :key="index" class="notice-list" @click="getNoticeInfo(message)">
             <p>
-              <span>【{{ activity.type }}】</span>
-              {{ activity.content }}
+              <span>【{{ msgTypeFormat(message) }}】</span>
+              {{ message.msgTitle }}
             </p>
             <p class="time">
               <vab-icon icon="time-line" />
-              {{ activity.timestamp }}
+              {{ parseTime(message.createdTime, '{y}-{m}-{d} {h}:{i}') }}
             </p>
           </li>
         </ul>
@@ -197,6 +197,8 @@
 
 <script>
   import * as echarts from 'echarts'
+  import messageApi from '@/api/system/message'
+
   export default {
     name: 'Index',
     data() {
@@ -262,12 +264,20 @@
         chartFunnel: {},
         forecast: '1',
         date: '2023-01-10',
+        messageList: [],
+        msgTypeOptions: [],
       }
     },
     mounted() {
       this.initBar()
       this.initFunnel()
       window.addEventListener('resize', this.handleResize)
+      this.$baseEventBus.$on('receivedMessage', () => {
+        console.log('---------------通知更新公告----------------')
+        this.handleNoticeList()
+      })
+      this.getOptions()
+      this.handleNoticeList()
     },
     methods: {
       initBar() {
@@ -372,6 +382,30 @@
           this.chartFunnel.resize()
         })
       },
+      getOptions() {
+        this.getDicts('sys_msg_type').then((response) => {
+          this.msgTypeOptions = response.data.values || []
+        })
+      },
+      msgTypeFormat(row) {
+        return this.selectDictLabel(this.msgTypeOptions, row.msgType)
+      },
+      async handleNoticeList() {
+        const { data } = await messageApi.getList({ msgType: '10' })
+        this.messageList = data.list
+      },
+      getNoticeInfo(msg) {
+        messageApi.getEntityById({ id: msg.id }).then((res) => {
+          this.$notify({
+            title: res.msgTitle,
+            position: 'top-right',
+            type: 'info',
+            duration: 1000 * 60 * 5,
+            dangerouslyUseHTMLString: true,
+            message: msg.msgContent,
+          })
+        })
+      },
     },
   }
 </script>
@@ -382,10 +416,12 @@
     display: flex;
     background: #f6f8f9;
     height: calc(100vh - 60px - 12px * 2 - 40px);
+
     .el-card {
       margin-bottom: 12px;
       border-radius: 4px;
     }
+
     .card-title {
       font-size: 16px;
       font-weight: 700;
@@ -393,32 +429,39 @@
       display: flex;
       justify-content: space-between;
       align-items: center;
+
       i {
         font-weight: normal;
         cursor: pointer;
+
         &:hover {
           color: #1d66dc;
         }
       }
     }
+
     .left {
       flex: 1;
       margin-right: 10px;
       overflow-x: hidden;
       overflow-y: auto;
+
       .board {
         height: 60%;
         display: flex;
         flex-direction: column;
+
         ::v-deep .el-card__body {
           flex: 1;
           display: flex;
           flex-direction: column;
           justify-content: space-around;
         }
+
         ul {
           height: 60px;
           display: flex;
+
           li {
             flex: 1;
             height: 60px;
@@ -427,9 +470,11 @@
             padding: 0 20px;
             user-select: none;
             cursor: pointer;
+
             + li {
               border-left: 1px solid #eee;
             }
+
             i {
               font-size: 32px;
               border-radius: 4px;
@@ -437,18 +482,22 @@
               height: 40px;
               color: #1d66dc;
               background: #ecf5ff;
+
               &.ri-money-cny-circle-line {
                 color: #e6a23c;
                 background: #fdf6ec;
               }
+
               &.ri-exchange-cny-fill {
                 color: #f56c6c;
                 background: #fef0f0;
               }
             }
+
             .text {
               padding-left: 10px;
               color: #9499a0;
+
               .num {
                 font-size: 22px;
                 font-weight: bold;
@@ -458,6 +507,7 @@
           }
         }
       }
+
       .chart-row {
         // height: calc(40% - 12px);
         // .el-card {
@@ -474,19 +524,24 @@
         // }
       }
     }
+
     .right {
       width: 450px;
+
       .detail {
         height: 64px;
         border: 1px solid rgb(235, 237, 240);
         border-radius: 4px;
+
         &.active {
           border: 1px solid #d7e8f4;
           background: #f7fbfe;
+
           p:last-child {
             font-weight: bold;
           }
         }
+
         p {
           height: 32px;
           line-height: 32px;
@@ -495,39 +550,47 @@
           overflow: hidden;
           text-overflow: ellipsis;
         }
+
         .time {
           display: flex;
           justify-content: space-between;
           align-items: center;
         }
       }
+
       .calendar {
         height: 60%;
         border-radius: 4px;
         display: flex;
         flex-direction: column;
+
         ::v-deep .el-card__body {
           flex: 1;
           overflow: hidden;
         }
+
         .week {
           height: 48px;
           display: flex;
           align-items: center;
+
           ul {
             flex: 1;
             display: flex;
             height: 48px;
+
             li {
               flex: 1;
               display: flex;
               flex-direction: column;
               align-items: center;
               justify-content: center;
+
               &.active span:last-child {
                 background: rgb(34, 76, 218);
                 color: #fff;
               }
+
               span {
                 flex: 1;
                 height: 24px;
@@ -535,15 +598,18 @@
                 text-align: center;
                 line-height: 24px;
               }
+
               span:first-child {
                 color: rgb(150, 151, 153);
               }
+
               span:last-child {
                 color: rgb(50, 50, 51);
                 border-radius: 4px;
               }
             }
           }
+
           i {
             height: 24px;
             width: 24px;
@@ -553,36 +619,43 @@
             border-radius: 4px;
             cursor: pointer;
             transition: 0.3s all;
+
             &:hover {
               color: #fff;
               background: rgb(100, 101, 102);
             }
           }
         }
+
         .el-timeline {
           margin-top: 20px;
           padding-top: 4px;
           height: calc(100% - 72px);
           overflow: auto;
+
           .el-timeline-item {
             padding-bottom: 10px;
           }
         }
       }
+
       .notice {
         height: calc(40% - 12px);
         display: flex;
         flex-direction: column;
+
         ::v-deep .el-card__body {
           flex: 1;
           overflow: auto;
         }
+
         .notice-list {
           display: flex;
           justify-content: space-between;
           height: 40px;
           line-height: 40px;
           border-bottom: 1px solid #ebeef5;
+
           p:first-child {
             flex: 1;
             min-width: 0;
@@ -590,6 +663,7 @@
             text-overflow: ellipsis;
             white-space: nowrap;
           }
+
           span {
             font-weight: bold;
           }
@@ -597,9 +671,11 @@
       }
     }
   }
+
   ::v-deep .el-timeline-item__tail {
     border-left: 2px dashed #e4e7ed;
   }
+
   #funnel,
   #bar {
     height: 300px;

+ 65 - 23
src/views/proj/business/components/DetailsContract.vue

@@ -21,9 +21,18 @@
         :prop="item.prop"
         show-overflow-tooltip>
         <template #default="{ row }">
-          <span v-if="item.prop === 'contractAmount'">
+          <span v-if="item.prop === 'contractType'">
+            {{ selectDictLabel(contractTypeOptions, row.contractType) }}
+          </span>
+          <span v-else-if="item.prop === 'signatoryType'">
+            {{ selectDictLabel(signatoryTypeOptions, row.signatoryType) }}
+          </span>
+          <span v-else-if="item.prop === 'contractAmount'">
             {{ formatPrice(row.contractAmount) }}
           </span>
+          <span v-else-if="item.prop === 'collectedAmount'">
+            {{ formatPrice(row.collectedAmount) }}
+          </span>
           <span v-else-if="item.label === '合同时间'">
             {{ parseTime(row.contractStartTime, '{y}-{m}-{d}') }}~{{ parseTime(row.contractEndTime, '{y}-{m}-{d}') }}
           </span>
@@ -60,74 +69,107 @@
             disableCheck: false,
           },
           {
-            label: '合同名称',
-            width: '280px',
-            prop: 'contractName',
+            label: '合同类型',
+            width: '100px',
+            prop: 'contractType',
             sortable: false,
             disableCheck: false,
           },
+          // {
+          //   label: '审批状态',
+          //   width: '100px',
+          //   prop: 'approStatus',
+          //   sortable: false,
+          //   disableCheck: false,
+          // },
           {
-            label: '合同类型',
+            label: '所在省',
             width: '100px',
-            prop: 'contractType',
+            prop: 'custProvince',
             sortable: false,
             disableCheck: false,
           },
           {
-            label: '合同金额',
+            label: '所在市',
+            width: '100px',
+            prop: 'custCity',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '客户名称',
+            width: '280px',
+            prop: 'custName',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '签订单位类型',
             width: '120px',
-            prop: 'contractAmount',
+            prop: 'signatoryType',
             sortable: false,
             disableCheck: false,
           },
           {
-            label: '合同时间',
-            width: '180px',
+            label: '合同签订单位',
+            width: '120px',
             prop: 'distributorName',
             sortable: false,
             disableCheck: false,
           },
           {
-            label: '审批状态',
-            width: '100px',
-            prop: 'approStatus',
+            label: '合同签订时间',
+            width: '120px',
+            prop: 'createdTime',
             sortable: false,
             disableCheck: false,
           },
           {
-            label: '销售工程师',
-            width: '120px',
-            prop: 'inchargeName',
+            label: '合同有效时间',
+            width: '200px',
+            prop: 'contractStartTime',
             sortable: false,
             disableCheck: false,
           },
           {
-            label: '公司签约人',
+            label: '合同金额',
             width: '120px',
-            prop: 'signatoryName',
+            prop: 'contractAmount',
             sortable: false,
             disableCheck: false,
           },
           {
-            label: '客户签约人',
+            label: '回款金额',
             width: '120px',
-            prop: 'custSignatoryName',
+            prop: 'collectedAmount',
             sortable: false,
             disableCheck: false,
           },
           {
-            label: '经销商/代理商',
+            label: '销售工程师',
             width: '120px',
-            prop: 'distributorName',
+            prop: 'inchargeName',
             sortable: false,
             disableCheck: false,
           },
         ],
         list: [],
+        contractTypeOptions: [],
+        signatoryTypeOptions: [],
       }
     },
-    mounted() {},
+    mounted() {
+      this.getOptions()
+    },
     methods: {
+      getOptions() {
+        Promise.all([this.getDicts('contract_type'), this.getDicts('contract_signatory_type')])
+          .then(([contract, signatory]) => {
+            this.contractTypeOptions = contract.data.values || []
+            this.signatoryTypeOptions = signatory.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
       open(busId) {
         this.busId = busId
         this.fetchData()

+ 186 - 0
src/views/system/notice/components/NoticeEdit.vue

@@ -0,0 +1,186 @@
+<template>
+  <el-dialog :title="title" :visible.sync="dialogFormVisible" @close="close">
+    <el-form ref="form" label-width="80px" :model="form" :rules="rules">
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="消息标题" prop="msgTitle">
+            <el-input v-model="form.msgTitle" placeholder="请输入消息标题" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="消息类别" prop="msgType">
+            <el-select v-model="form.msgType" placeholder="请选择消息类别">
+              <el-option v-for="dict in msgTypeOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="接收用户" prop="recvUser">
+            <el-input v-model="form.recvUser" readonly @focus="handleSelectSale" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="状态" prop="msgStatus">
+            <el-radio-group v-model="form.msgStatus">
+              <el-radio v-for="dict in msgStatusOptions" :key="dict.key" :label="dict.key">
+                {{ dict.value }}
+              </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="内容">
+            <div style="border: 1px solid #ccc">
+              <Toolbar
+                :default-config="toolbarConfig"
+                :editor="editor"
+                :mode="mode"
+                style="border-bottom: 1px solid #ccc" />
+              <Editor
+                v-model="form.msgContent"
+                :default-config="editorConfig"
+                style="height: 250px; overflow-y: hidden"
+                @onCreated="onCreated" />
+            </div>
+            <!--            <el-input-->
+            <!--              v-model="form.msgContent"-->
+            <!--              maxlength="300"-->
+            <!--              placeholder="请输入内容"-->
+            <!--              rows="5"-->
+            <!--              show-word-limit-->
+            <!--              type="textarea" />-->
+          </el-form-item>
+        </el-col>
+        <!--        <el-col :span="24">-->
+        <!--          <el-form-item label="备注">-->
+        <!--            <el-input-->
+        <!--              v-model="form.remark"-->
+        <!--              maxlength="300"-->
+        <!--              placeholder="请输入备注"-->
+        <!--              rows="3"-->
+        <!--              show-word-limit-->
+        <!--              type="textarea" />-->
+        <!--          </el-form-item>-->
+        <!--        </el-col>-->
+      </el-row>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="close">取 消</el-button>
+      <el-button type="primary" @click="save">确 定</el-button>
+    </div>
+
+    <!-- 选择销售工程师弹窗 -->
+    <select-user ref="selectSales" multiple @save="selectSales" />
+  </el-dialog>
+</template>
+
+<script>
+  import messageApi from '@/api/system/message'
+  import SelectUser from '@/components/select/SelectUser.vue'
+  import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+
+  export default {
+    name: 'NoticeEdit',
+    components: {
+      SelectUser,
+      Editor,
+      Toolbar,
+    },
+    data() {
+      return {
+        form: {
+          recvUserIds: undefined,
+          recvUser: undefined,
+          msgStatus: '10',
+        },
+        rules: {
+          msgTitle: [{ required: true, message: '不能为空', trigger: 'blur' }],
+          msgType: [{ required: true, message: '不能为空', trigger: 'blur' }],
+          msgStatus: [{ required: true, message: '不能为空', trigger: 'blur' }],
+          msgContent: [{ required: true, message: '不能为空', trigger: 'blur' }],
+        },
+        title: '',
+        dialogFormVisible: false,
+        msgTypeOptions: [],
+        msgStatusOptions: [],
+
+        // wangeditor
+        editor: null,
+        toolbarConfig: {},
+        editorConfig: { placeholder: '请输入内容...' },
+        mode: 'default', // or 'simple'
+      }
+    },
+    created() {},
+    beforeDestroy() {
+      const editor = this.editor
+      if (editor == null) return
+      editor.destroy() // 组件销毁时,及时销毁编辑器
+    },
+    methods: {
+      onCreated(editor) {
+        this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
+      },
+      handleSelectSale() {
+        this.$refs.selectSales.open()
+      },
+      selectSales(val) {
+        if (val && val.length > 0) {
+          this.form.recvUserIds = val.map((item) => item.id).join()
+          this.form.recvUser = val.map((item) => item.nickName).join()
+          console.log(this.form.recvUser)
+        }
+      },
+      getOptions() {
+        this.getDicts('sys_msg_type').then((response) => {
+          this.msgTypeOptions = response.data.values || []
+        })
+        this.getDicts('sys_msg_status').then((response) => {
+          this.msgStatusOptions = response.data.values || []
+        })
+      },
+      showEdit(row) {
+        if (!row) {
+          this.title = '添加'
+        } else {
+          this.title = '编辑'
+          this.form = Object.assign(this.form, row)
+        }
+        this.dialogFormVisible = true
+        if (this.$parent.msgTypeOptions) {
+          this.msgTypeOptions = this.$parent.msgTypeOptions
+        }
+        if (this.$parent.msgStatusOptions) {
+          this.msgStatusOptions = this.$parent.msgStatusOptions
+        }
+        if (!this.msgTypeOptions) {
+          this.getOptions()
+        }
+      },
+      close() {
+        this.$refs['form'].resetFields()
+        this.form = this.$options.data().form
+        this.dialogFormVisible = false
+      },
+      save() {
+        this.$refs['form'].validate(async (valid) => {
+          if (valid) {
+            let data
+            if (this.form.id) {
+              data = await messageApi.doEdit(this.form)
+            } else {
+              data = await messageApi.doAdd(this.form)
+            }
+            this.$baseMessage(data.msg, 'success')
+            this.$emit('fetch-data')
+            this.close()
+          } else {
+            return false
+          }
+        })
+      },
+    },
+  }
+</script>
+<style src="@wangeditor/editor/dist/css/style.css"></style>

+ 225 - 0
src/views/system/notice/index.vue

@@ -0,0 +1,225 @@
+<template>
+  <div class="app-container">
+    <vab-query-form>
+      <vab-query-form-top-panel>
+        <el-form ref="queryForm" :inline="true" label-width="68px" :model="queryForm" size="small">
+          <el-form-item prop="msgTitle">
+            <el-input
+              v-model="queryForm.msgTitle"
+              clearable
+              placeholder="请输入消息标题"
+              @keyup.enter.native="queryData" />
+          </el-form-item>
+          <el-form-item prop="msgType">
+            <el-select v-model="queryForm.msgType" clearable placeholder="消息类别">
+              <el-option v-for="dict in msgTypeOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button icon="el-icon-search" type="primary" @click="queryData">搜索</el-button>
+            <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </vab-query-form-top-panel>
+      <vab-query-form-left-panel :span="12">
+        <el-button icon="el-icon-plus" type="primary" @click="handleEdit">添加</el-button>
+        <el-button icon="el-icon-delete" type="danger" @click="handleDelete">删除</el-button>
+      </vab-query-form-left-panel>
+      <vab-query-form-right-panel :span="12">
+        <table-tool :check-list.sync="checkList" :columns="columns" />
+      </vab-query-form-right-panel>
+    </vab-query-form>
+    <el-table
+      :key="uuid"
+      v-loading="listLoading"
+      :data="list"
+      :height="$baseTableHeight(1)"
+      @selection-change="setSelectRows">
+      <el-table-column align="center" show-overflow-tooltip type="selection" />
+      <el-table-column
+        v-for="(item, index) in finallyColumns"
+        :key="index"
+        align="center"
+        :formatter="item.formatter"
+        :label="item.label"
+        :prop="item.prop"
+        :sortable="item.sortable"
+        :width="item.width" />
+
+      <el-table-column align="center" label="操作" width="85">
+        <template #default="{ row }">
+          <el-button type="text" @click="handleEdit(row)">编辑</el-button>
+          <el-button type="text" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination
+      background
+      :current-page="queryForm.pageNum"
+      :layout="layout"
+      :page-size="queryForm.pageSize"
+      :total="total"
+      @current-change="handleCurrentChange"
+      @size-change="handleSizeChange" />
+
+    <!-- 添加或修改公告对话框 -->
+    <edit ref="edit" @fetch-data="fetchData" />
+  </div>
+</template>
+
+<script>
+  import messageApi from '@/api/system/message'
+  import Edit from './components/NoticeEdit.vue'
+  import TableTool from '@/components/table/TableTool'
+
+  export default {
+    name: 'Notice',
+    components: { Edit, TableTool },
+    data() {
+      return {
+        uuid: '',
+        checkList: [],
+        columns: [
+          {
+            prop: 'id',
+            label: '参数主键',
+            width: 'auto',
+          },
+          {
+            prop: 'msgType',
+            label: '消息类别',
+            width: 'auto',
+            disableCheck: true,
+            formatter: this.msgTypeFormat,
+          },
+          {
+            prop: 'msgTitle',
+            label: '消息标题',
+            width: 'auto',
+            disableCheck: true,
+          },
+          {
+            prop: 'msgStatus',
+            label: '消息状态',
+            width: 'auto',
+            formatter: this.msgStatusFormat,
+          },
+          {
+            prop: 'remark',
+            label: '备注',
+            width: 'auto',
+          },
+          {
+            prop: 'createdName',
+            label: '创建人',
+            width: 'auto',
+          },
+          {
+            prop: 'createdTime',
+            label: '创建时间',
+            width: 'auto',
+          },
+        ],
+        list: [],
+        listLoading: false,
+        layout: 'total, sizes, prev, pager, next, jumper',
+        total: 0,
+        selectRows: '',
+        queryForm: {
+          pageNum: 1,
+          pageSize: 10,
+          configName: '',
+          configKey: '',
+          configType: '',
+        },
+        msgTypeOptions: [],
+        msgStatusOptions: [],
+      }
+    },
+    computed: {
+      finallyColumns() {
+        return this.columns.filter((item) => this.checkList.includes(item.label))
+      },
+    },
+    created() {
+      this.getOptions()
+    },
+    mounted() {
+      this.fetchData()
+    },
+    methods: {
+      getOptions() {
+        this.getDicts('sys_msg_type').then((response) => {
+          this.msgTypeOptions = response.data.values || []
+        })
+        this.getDicts('sys_msg_status').then((response) => {
+          this.msgStatusOptions = response.data.values || []
+        })
+      },
+      // 参数系统内置字典翻译
+      msgTypeFormat(row) {
+        return this.selectDictLabel(this.msgTypeOptions, row.msgType)
+      },
+      msgStatusFormat(row) {
+        return this.selectDictLabel(this.msgStatusOptions, row.msgStatus)
+      },
+      setSelectRows(val) {
+        this.selectRows = val
+      },
+      handleEdit(row) {
+        if (row.id) {
+          this.$refs['edit'].showEdit(row)
+        } else {
+          this.$refs['edit'].showEdit()
+        }
+      },
+      handleDelete(row) {
+        if (row.id) {
+          this.$baseConfirm('你确定要删除当前项吗', null, async () => {
+            const { msg } = await messageApi.doDelete({ ids: [row.id] })
+            this.$baseMessage(msg, 'success')
+            await this.fetchData()
+          })
+        } else {
+          if (this.selectRows.length > 0) {
+            const ids = this.selectRows.map((item) => item.id)
+            this.$baseConfirm('你确定要删除选中项吗', null, async () => {
+              const { msg } = await messageApi.doDelete({ ids })
+              this.$baseMessage(msg, 'success')
+              await this.fetchData()
+            })
+          } else {
+            this.$baseMessage('未选中任何行', 'error')
+            return false
+          }
+        }
+      },
+      handleSizeChange(val) {
+        this.queryForm.pageSize = val
+        this.fetchData()
+      },
+      handleCurrentChange(val) {
+        this.queryForm.pageNum = val
+        this.fetchData()
+      },
+      queryData() {
+        this.queryForm.pageNum = 1
+        this.fetchData()
+      },
+      /** 重置按钮操作 */
+      resetQuery() {
+        this.resetForm('queryForm')
+        this.fetchData()
+      },
+      async fetchData() {
+        this.listLoading = true
+        const { data } = await messageApi.getList(this.queryForm)
+        const { list, total } = data
+        this.list = list
+        this.total = total
+        this.listLoading = false
+      },
+    },
+  }
+</script>