Browse Source

feat[litemall-admin]: 管理后台支持通知中心和通知管理

Junling Bu 6 years ago
parent
commit
0c4786dcd4

+ 1 - 0
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/util/AdminResponseCode.java

@@ -23,5 +23,6 @@ public class AdminResponseCode {
     public static final Integer GROUPON_GOODS_UNKNOWN = 650;
     public static final Integer GROUPON_GOODS_EXISTED = 651;
     public static final Integer GROUPON_GOODS_OFFLINE = 652;
+    public static final Integer NOTICE_UPDATE_NOT_ALLOWED = 660;
 
 }

+ 153 - 0
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/web/AdminNoticeController.java

@@ -0,0 +1,153 @@
+package org.linlinjava.litemall.admin.web;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.apache.shiro.subject.Subject;
+import org.linlinjava.litemall.admin.annotation.RequiresPermissionsDesc;
+import org.linlinjava.litemall.core.util.JacksonUtil;
+import org.linlinjava.litemall.core.util.ResponseUtil;
+import org.linlinjava.litemall.core.validator.Order;
+import org.linlinjava.litemall.core.validator.Sort;
+import org.linlinjava.litemall.db.domain.*;
+import org.linlinjava.litemall.db.service.LitemallAdminService;
+import org.linlinjava.litemall.db.service.LitemallNoticeAdminService;
+import org.linlinjava.litemall.db.service.LitemallNoticeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.constraints.NotNull;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.linlinjava.litemall.admin.util.AdminResponseCode.NOTICE_UPDATE_NOT_ALLOWED;
+
+@RestController
+@RequestMapping("/admin/notice")
+@Validated
+public class AdminNoticeController {
+    private final Log logger = LogFactory.getLog(AdminNoticeController.class);
+
+    @Autowired
+    private LitemallNoticeService noticeService;
+    @Autowired
+    private LitemallAdminService adminService;
+    @Autowired
+    private LitemallNoticeAdminService noticeAdminService;
+
+    @RequiresPermissions("admin:notice:list")
+    @RequiresPermissionsDesc(menu = {"系统管理", "通知管理"}, button = "查询")
+    @GetMapping("/list")
+    public Object list(String title, String content,
+                       @RequestParam(defaultValue = "1") Integer page,
+                       @RequestParam(defaultValue = "10") Integer limit,
+                       @Sort @RequestParam(defaultValue = "add_time") String sort,
+                       @Order @RequestParam(defaultValue = "desc") String order) {
+        List<LitemallNotice> noticeList = noticeService.querySelective(title, content, page, limit, sort, order);
+        return ResponseUtil.okList(noticeList);
+    }
+
+    private Object validate(LitemallNotice notice) {
+        String title = notice.getTitle();
+        if (StringUtils.isEmpty(title)) {
+            return ResponseUtil.badArgument();
+        }
+        return null;
+    }
+
+    private Integer getAdminId(){
+        Subject currentUser = SecurityUtils.getSubject();
+        LitemallAdmin admin = (LitemallAdmin) currentUser.getPrincipal();
+        return admin.getId();
+    }
+
+    @RequiresPermissions("admin:notice:create")
+    @RequiresPermissionsDesc(menu = {"推广管理", "通知管理"}, button = "添加")
+    @PostMapping("/create")
+    public Object create(@RequestBody LitemallNotice notice) {
+        Object error = validate(notice);
+        if (error != null) {
+            return error;
+        }
+        // 1. 添加通知记录
+        notice.setAdminId(getAdminId());
+        noticeService.add(notice);
+        // 2. 添加管理员通知记录
+        List<LitemallAdmin> adminList = adminService.all();
+        LitemallNoticeAdmin noticeAdmin = new LitemallNoticeAdmin();
+        noticeAdmin.setNoticeId(notice.getId());
+        noticeAdmin.setNoticeTitle(notice.getTitle());
+        for(LitemallAdmin admin : adminList){
+            noticeAdmin.setAdminId(admin.getId());
+            noticeAdminService.add(noticeAdmin);
+        }
+        return ResponseUtil.ok(notice);
+    }
+
+    @RequiresPermissions("admin:notice:read")
+    @RequiresPermissionsDesc(menu = {"推广管理", "通知管理"}, button = "详情")
+    @GetMapping("/read")
+    public Object read(@NotNull Integer id) {
+        LitemallNotice notice = noticeService.findById(id);
+        List<LitemallNoticeAdmin> noticeAdminList = noticeAdminService.queryByNoticeId(id);
+        Map<String, Object> data = new HashMap<>(2);
+        data.put("notice", notice);
+        data.put("noticeAdminList", noticeAdminList);
+        return ResponseUtil.ok(data);
+    }
+
+    @RequiresPermissions("admin:notice:update")
+    @RequiresPermissionsDesc(menu = {"推广管理", "通知管理"}, button = "编辑")
+    @PostMapping("/update")
+    public Object update(@RequestBody LitemallNotice notice) {
+        Object error = validate(notice);
+        if (error != null) {
+            return error;
+        }
+        LitemallNotice originalNotice = noticeService.findById(notice.getId());
+        if (originalNotice == null) {
+            return ResponseUtil.badArgument();
+        }
+        // 如果通知已经有人阅读过,则不支持编辑
+        if(noticeAdminService.countReadByNoticeId(notice.getId()) > 0){
+            return ResponseUtil.fail(NOTICE_UPDATE_NOT_ALLOWED, "通知已被阅读,不能重新编辑");
+        }
+        // 1. 更新通知记录
+        notice.setAdminId(getAdminId());
+        noticeService.updateById(notice);
+        // 2. 更新管理员通知记录
+        if(!originalNotice.getTitle().equals(notice.getTitle())){
+            LitemallNoticeAdmin noticeAdmin = new LitemallNoticeAdmin();
+            noticeAdmin.setNoticeTitle(notice.getTitle());
+            noticeAdminService.updateByNoticeId(noticeAdmin, notice.getId());
+        }
+        return ResponseUtil.ok(notice);
+    }
+
+    @RequiresPermissions("admin:notice:delete")
+    @RequiresPermissionsDesc(menu = {"推广管理", "通知管理"}, button = "删除")
+    @PostMapping("/delete")
+    public Object delete(@RequestBody LitemallNotice notice) {
+        // 1. 删除通知管理员记录
+        noticeAdminService.deleteByNoticeId(notice.getId());
+        // 2. 删除通知记录
+        noticeService.deleteById(notice.getId());
+        return ResponseUtil.ok();
+    }
+
+    @RequiresPermissions("admin:notice:batch-delete")
+    @RequiresPermissionsDesc(menu = {"推广管理", "通知管理"}, button = "批量删除")
+    @PostMapping("/batch-delete")
+    public Object batchDelete(@RequestBody String body) {
+        List<Integer> ids = JacksonUtil.parseIntegerList(body, "ids");
+        // 1. 删除通知管理员记录
+        noticeAdminService.deleteByNoticeIds(ids);
+        // 2. 删除通知记录
+        noticeService.deleteByIds(ids);
+        return ResponseUtil.ok();
+    }
+}

+ 104 - 4
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/web/AdminProfileController.java

@@ -1,5 +1,6 @@
 package org.linlinjava.litemall.admin.web;
 
+import io.swagger.models.auth.In;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.shiro.SecurityUtils;
@@ -8,15 +9,25 @@ import org.apache.shiro.subject.Subject;
 import org.linlinjava.litemall.core.util.JacksonUtil;
 import org.linlinjava.litemall.core.util.ResponseUtil;
 import org.linlinjava.litemall.core.util.bcrypt.BCryptPasswordEncoder;
+import org.linlinjava.litemall.core.validator.Order;
+import org.linlinjava.litemall.core.validator.Sort;
 import org.linlinjava.litemall.db.domain.LitemallAdmin;
+import org.linlinjava.litemall.db.domain.LitemallIssue;
+import org.linlinjava.litemall.db.domain.LitemallNotice;
+import org.linlinjava.litemall.db.domain.LitemallNoticeAdmin;
 import org.linlinjava.litemall.db.service.LitemallAdminService;
+import org.linlinjava.litemall.db.service.LitemallNoticeAdminService;
+import org.linlinjava.litemall.db.service.LitemallNoticeService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.StringUtils;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 import static org.linlinjava.litemall.admin.util.AdminResponseCode.ADMIN_INVALID_ACCOUNT;
 
@@ -28,6 +39,10 @@ public class AdminProfileController {
 
     @Autowired
     private LitemallAdminService adminService;
+    @Autowired
+    private LitemallNoticeService noticeService;
+    @Autowired
+    private LitemallNoticeAdminService noticeAdminService;
 
     @RequiresAuthentication
     @PostMapping("/password")
@@ -56,4 +71,89 @@ public class AdminProfileController {
         return ResponseUtil.ok();
     }
 
+    private Integer getAdminId(){
+        Subject currentUser = SecurityUtils.getSubject();
+        LitemallAdmin admin = (LitemallAdmin) currentUser.getPrincipal();
+        return admin.getId();
+    }
+
+    @RequiresAuthentication
+    @GetMapping("/nnotice")
+    public Object nNotice() {
+        int count = noticeAdminService.countUnread(getAdminId());
+        return ResponseUtil.ok(count);
+    }
+
+    @RequiresAuthentication
+    @GetMapping("/lsnotice")
+    public Object lsNotice(String title, String type,
+                            @RequestParam(defaultValue = "1") Integer page,
+                            @RequestParam(defaultValue = "10") Integer limit,
+                            @Sort @RequestParam(defaultValue = "add_time") String sort,
+                            @Order @RequestParam(defaultValue = "desc") String order) {
+        List<LitemallNoticeAdmin> noticeList = noticeAdminService.querySelective(title, type, getAdminId(), page, limit, sort, order);
+        return ResponseUtil.okList(noticeList);
+    }
+
+    @RequiresAuthentication
+    @PostMapping("/catnotice")
+    public Object catNotice(@RequestBody String body) {
+        Integer noticeId = JacksonUtil.parseInteger(body, "noticeId");
+        if(noticeId == null){
+            return ResponseUtil.badArgument();
+        }
+
+        LitemallNoticeAdmin noticeAdmin = noticeAdminService.find(noticeId, getAdminId());
+        if(noticeAdmin == null){
+           return ResponseUtil.badArgumentValue();
+        }
+        // 更新通知记录中的时间
+        noticeAdmin.setReadTime(LocalDateTime.now());
+        noticeAdminService.update(noticeAdmin);
+
+        // 返回通知的相关信息
+        Map<String, Object> data = new HashMap<>();
+        LitemallNotice notice = noticeService.findById(noticeId);
+        data.put("title", notice.getTitle());
+        data.put("content", notice.getContent());
+        data.put("time", notice.getUpdateTime());
+        Integer adminId = notice.getAdminId();
+        if(adminId.equals(0)){
+            data.put("admin", "系统");
+        }
+        else{
+            LitemallAdmin admin = adminService.findById(notice.getAdminId());
+            data.put("admin", admin.getUsername());
+            data.put("avatar", admin.getAvatar());
+        }
+        return ResponseUtil.ok(data);
+    }
+
+    @RequiresAuthentication
+    @PostMapping("/bcatnotice")
+    public Object bcatNotice(@RequestBody String body) {
+        List<Integer> ids = JacksonUtil.parseIntegerList(body, "ids");
+        noticeAdminService.markReadByIds(ids, getAdminId());
+        return ResponseUtil.ok();
+    }
+
+    @RequiresAuthentication
+    @PostMapping("/rmnotice")
+    public Object rmNotice(@RequestBody String body) {
+        Integer id = JacksonUtil.parseInteger(body, "id");
+        if(id == null){
+            return ResponseUtil.badArgument();
+        }
+        noticeAdminService.deleteById(id, getAdminId());
+        return ResponseUtil.ok();
+    }
+
+    @RequiresAuthentication
+    @PostMapping("/brmnotice")
+    public Object brmNotice(@RequestBody String body) {
+        List<Integer> ids = JacksonUtil.parseIntegerList(body, "ids");
+        noticeAdminService.deleteByIds(ids, getAdminId());
+        return ResponseUtil.ok();
+    }
+
 }

+ 49 - 0
litemall-admin/src/api/notice.js

@@ -0,0 +1,49 @@
+import request from '@/utils/request'
+
+export function listNotice(query) {
+  return request({
+    url: '/notice/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function createNotice(data) {
+  return request({
+    url: '/notice/create',
+    method: 'post',
+    data
+  })
+}
+
+export function readNotice(query) {
+  return request({
+    url: '/notice/read',
+    method: 'get',
+    params: query
+  })
+}
+
+export function updateNotice(data) {
+  return request({
+    url: '/notice/update',
+    method: 'post',
+    data
+  })
+}
+
+export function deleteNotice(data) {
+  return request({
+    url: '/notice/delete',
+    method: 'post',
+    data
+  })
+}
+
+export function batchDeleteNotice(data) {
+  return request({
+    url: '/notice/batch-delete',
+    method: 'post',
+    data
+  })
+}

+ 48 - 0
litemall-admin/src/api/profile.js

@@ -7,3 +7,51 @@ export function changePassword(data) {
     data
   })
 }
+
+export function nNotice() {
+  return request({
+    url: '/profile/nnotice',
+    method: 'get'
+  })
+}
+
+export function listNotice(query) {
+  return request({
+    url: '/profile/lsnotice',
+    method: 'get',
+    params: query
+  })
+}
+
+export function catNotice(data) {
+  return request({
+    url: '/profile/catnotice',
+    method: 'post',
+    data
+  })
+}
+
+export function bcatNotice(data) {
+  return request({
+    url: '/profile/bcatnotice',
+    method: 'post',
+    data
+  })
+}
+
+export function rmotice(data) {
+  return request({
+    url: '/profile/rmnotice',
+    method: 'post',
+    data
+  })
+}
+
+export function brmNotice(data) {
+  return request({
+    url: '/profile/brmnotice',
+    method: 'post',
+    data
+  })
+}
+

+ 51 - 0
litemall-admin/src/components/Notice/index.vue

@@ -0,0 +1,51 @@
+<template>
+  <div>
+    <el-badge :is-dot="hasNotice">
+      <i class="el-icon-bell" @click="click" />
+    </el-badge>
+  </div>
+</template>
+
+<script>
+import { nNotice } from '@/api/profile'
+
+export default {
+  data() {
+    return {
+      hasNotice: false,
+      timer: ''
+    }
+  },
+  mounted() {
+    this.timer = setInterval(this.checkNotice, 10 * 1000)
+  },
+  beforeDestroy() {
+    clearInterval(this.timer)
+  },
+  created() {
+    this.checkNotice()
+  },
+  methods: {
+    click() {
+      this.$router.push({ path: '/profile/notice' })
+    },
+    checkNotice() {
+      nNotice().then(response => {
+        this.hasNotice = response.data.data > 0
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+/deep/ .el-badge__content.is-fixed.is-dot {
+  right: 5px;
+  top: 10px;
+}
+
+.el-icon-bell {
+  font-size: 20px;
+  cursor: pointer;
+}
+</style>

+ 3 - 26
litemall-admin/src/components/Screenfull/index.vue

@@ -1,29 +1,6 @@
 <template>
   <div>
-    <svg
-      t="1508738709248"
-      class="screenfull-svg"
-      viewBox="0 0 1024 1024"
-      version="1.1"
-      xmlns="http://www.w3.org/2000/svg"
-      p-id="2069"
-      xmlns:xlink="http://www.w3.org/1999/xlink"
-      width="32"
-      height="32"
-      @click="click">
-      <path
-        d="M333.493443 428.647617 428.322206 333.832158 262.572184 168.045297 366.707916 64.444754 64.09683 64.444754 63.853283 366.570793 167.283957 262.460644Z"
-        p-id="2070"/>
-      <path
-        d="M854.845439 760.133334 688.61037 593.95864 593.805144 688.764889 759.554142 854.56096 655.44604 958.161503 958.055079 958.161503 958.274066 656.035464Z"
-        p-id="2071"/>
-      <path
-        d="M688.535669 428.550403 854.31025 262.801405 957.935352 366.921787 957.935352 64.34754 655.809313 64.081481 759.919463 167.535691 593.70793 333.731874Z"
-        p-id="2072"/>
-      <path
-        d="M333.590658 594.033341 167.8171 759.804852 64.218604 655.67219 64.218604 958.270996 366.342596 958.502263 262.234493 855.071589 428.421466 688.86108Z"
-        p-id="2073"/>
-    </svg>
+    <i class="el-icon-full-screen" @click="click" />
   </div>
 </template>
 
@@ -67,12 +44,12 @@ export default {
 </script>
 
 <style scoped>
-.screenfull-svg {
+.el-icon-full-screen {
   display: inline-block;
   cursor: pointer;
   fill: #5a5e66;;
   width: 20px;
   height: 20px;
-  vertical-align: 10px;
+  font-size: 20px;
 }
 </style>

+ 7 - 4
litemall-admin/src/components/SizeSelect/index.vue

@@ -1,7 +1,7 @@
 <template>
   <el-dropdown trigger="click" @command="handleSetSize">
     <div>
-      <svg-icon class-name="size-icon" icon-class="size" />
+      <i class="el-icon-rank" />
     </div>
     <el-dropdown-menu slot="dropdown">
       <el-dropdown-item :disabled="size==='medium'" command="medium">Medium</el-dropdown-item>
@@ -46,10 +46,13 @@ export default {
 </script>
 
 <style scoped>
-.size-icon {
-  font-size: 20px;
+.el-icon-rank {
+  display: inline-block;
   cursor: pointer;
-  vertical-align: -4px!important;
+  fill: #5a5e66;;
+  width: 20px;
+  height: 20px;
+  font-size: 20px;
 }
 </style>
 

+ 16 - 0
litemall-admin/src/router/index.js

@@ -398,6 +398,16 @@ export const asyncRouterMap = [
         }
       },
       {
+        path: 'notice',
+        component: () => import('@/views/sys/notice'),
+        name: 'notice',
+        meta: {
+          perms: ['GET /admin/notice/list', 'POST /admin/notice/create', 'POST /admin/notice/update', 'POST /admin/notice/delete'],
+          title: '通知管理',
+          noCache: true
+        }
+      },
+      {
         path: 'log',
         component: () => import('@/views/sys/log'),
         name: 'log',
@@ -579,6 +589,12 @@ export const asyncRouterMap = [
         component: () => import('@/views/profile/password'),
         name: 'password',
         meta: { title: '修改密码', noCache: true }
+      },
+      {
+        path: 'notice',
+        component: () => import('@/views/profile/notice'),
+        name: 'notice',
+        meta: { title: '通知中心', noCache: true }
       }
     ],
     hidden: true

+ 11 - 11
litemall-admin/src/views/layout/components/Navbar.vue

@@ -1,26 +1,29 @@
 <template>
   <div class="navbar">
-    <hamburger :toggle-click="toggleSideBar" :is-active="sidebar.opened" class="hamburger-container"/>
+    <hamburger :toggle-click="toggleSideBar" :is-active="sidebar.opened" class="hamburger-container" />
 
-    <breadcrumb class="breadcrumb-container"/>
+    <breadcrumb class="breadcrumb-container" />
 
     <div class="right-menu">
       <template v-if="device!=='mobile'">
 
         <el-tooltip content="全屏" effect="dark" placement="bottom">
-          <screenfull class="screenfull right-menu-item"/>
+          <screenfull class="right-menu-item" />
         </el-tooltip>
 
         <el-tooltip content="布局大小" effect="dark" placement="bottom">
-          <size-select class="international right-menu-item"/>
+          <size-select class="right-menu-item" />
         </el-tooltip>
 
+        <el-tooltip content="通知中心" effect="dark" placement="bottom">
+          <notice class="right-menu-item" />
+        </el-tooltip>
       </template>
 
       <el-dropdown class="avatar-container right-menu-item" trigger="click">
         <div class="avatar-wrapper">
           <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
-          <i class="el-icon-caret-bottom"/>
+          <i class="el-icon-caret-bottom" />
         </div>
         <el-dropdown-menu slot="dropdown">
           <router-link to="/">
@@ -58,13 +61,15 @@ import Breadcrumb from '@/components/Breadcrumb'
 import Hamburger from '@/components/Hamburger'
 import Screenfull from '@/components/Screenfull'
 import SizeSelect from '@/components/SizeSelect'
+import Notice from '@/components/Notice'
 
 export default {
   components: {
     Breadcrumb,
     Hamburger,
     Screenfull,
-    SizeSelect
+    SizeSelect,
+    Notice
   },
   computed: {
     ...mapGetters([
@@ -114,11 +119,6 @@ export default {
     .right-menu-item {
       display: inline-block;
       margin: 0 8px;
-    }
-    .screenfull {
-      height: 20px;
-    }
-    .international{
       vertical-align: top;
     }
     .avatar-container {

+ 196 - 0
litemall-admin/src/views/profile/notice.vue

@@ -0,0 +1,196 @@
+<template>
+  <div class="app-container">
+
+    <!-- 查询和其他操作 -->
+    <div class="filter-container">
+      <el-input v-model="listQuery.title" clearable class="filter-item" style="width: 200px;" placeholder="请输入标题关键字" />
+      <el-select v-model="listQuery.type" class="filter-item" placeholder="请选择通知类型">
+        <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+      </el-select>
+      <el-button class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">查找</el-button>
+    </div>
+
+    <div class="operator-container">
+      <el-button class="filter-item" type="primary" icon="el-icon-edit" @click="handleBatchRead">批量已读</el-button>
+      <el-button class="filter-item" type="danger" icon="el-icon-delete" @click="handleBatchDelete">批量删除</el-button>
+    </div>
+
+    <!-- 查询结果 -->
+    <el-table v-loading="listLoading" :data="list" element-loading-text="正在查询中。。。" border fit highlight-current-row @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" />
+
+      <el-table-column align="center" label="通知标题" prop="noticeTitle" />
+
+      <el-table-column align="center" label="通知时间" prop="addTime" width="180" />
+
+      <el-table-column align="center" label="通知状态" prop="readTime" width="120">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.readTime ? 'success' : 'error' ">{{ scope.row.readTime ? '已读' : '未读' }}</el-tag>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="操作" width="200" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button type="primary" size="mini" @click="handleRead(scope.row)">阅读</el-button>
+          <el-button type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" />
+
+    <el-dialog :title="notice.title" :visible.sync="noticeVisible" center>
+      <el-divider content-position="left">{{ notice.admin }} 于 {{ notice.time }} 通知如下内容:</el-divider>
+      <div v-html="notice.content" />
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="afterRead">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listNotice, catNotice, bcatNotice, rmNotice, brmNotice } from '@/api/profile'
+import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
+import _ from 'lodash'
+
+export default {
+  name: 'AdminNotice',
+  components: { Pagination },
+  data() {
+    return {
+      list: [],
+      total: 0,
+      listLoading: true,
+      listQuery: {
+        page: 1,
+        limit: 20,
+        title: '',
+        type: 'unread',
+        sort: 'add_time',
+        order: 'desc'
+      },
+      options: [{
+        value: 'all',
+        label: '所有'
+      }, {
+        value: 'read',
+        label: '已读'
+      }, {
+        value: 'unread',
+        label: '未读'
+      }],
+      multipleSelection: [],
+      notice: {
+        title: '',
+        source: '',
+        content: '',
+        addTime: ''
+      },
+      noticeVisible: false
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.listLoading = true
+      listNotice(this.listQuery)
+        .then(response => {
+          this.list = response.data.data.list
+          this.total = response.data.data.total
+          this.listLoading = false
+        })
+        .catch(() => {
+          this.list = []
+          this.total = 0
+          this.listLoading = false
+        })
+    },
+    handleFilter() {
+      this.listQuery.page = 1
+      this.getList()
+    },
+    handleDelete(row) {
+      rmNotice(row)
+        .then(response => {
+          this.$notify.success({
+            title: '成功',
+            message: '删除通知成功'
+          })
+          const index = this.list.indexOf(row)
+          this.list.splice(index, 1)
+        })
+        .catch(response => {
+          this.$notify.error({
+            title: '失败',
+            message: response.data.errmsg
+          })
+        })
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val
+    },
+    handleBatchDelete() {
+      if (this.multipleSelection.length === 0) {
+        this.$message.error('请选择至少一条记录')
+        return
+      }
+      const ids = []
+      _.forEach(this.multipleSelection, function(item) {
+        ids.push(item.id)
+      })
+      brmNotice({ ids: ids })
+        .then(response => {
+          this.$notify.success({
+            title: '成功',
+            message: '批量删除通知成功'
+          })
+          this.getList()
+        })
+        .catch(response => {
+          this.$notify.error({
+            title: '失败',
+            message: response.data.errmsg
+          })
+        })
+    },
+    handleRead(row) {
+      catNotice(row)
+        .then(response => {
+          this.notice = response.data.data
+          this.noticeVisible = true
+        })
+    },
+    afterRead() {
+      this.noticeVisible = false
+      this.getList()
+    },
+    handleBatchRead() {
+      if (this.multipleSelection.length === 0) {
+        this.$message.error('请选择至少一条记录')
+        return
+      }
+      const ids = []
+      _.forEach(this.multipleSelection, function(item) {
+        ids.push(item.id)
+      })
+      bcatNotice({ ids: ids })
+        .then(response => {
+          this.$notify.success({
+            title: '成功',
+            message: '批量设置通知已读成功'
+          })
+          this.getList()
+        })
+        .catch(response => {
+          this.$notify.error({
+            title: '失败',
+            message: response.data.errmsg
+          })
+        })
+    }
+  }
+}
+</script>

+ 305 - 0
litemall-admin/src/views/sys/notice.vue

@@ -0,0 +1,305 @@
+<template>
+  <div class="app-container">
+
+    <!-- 查询和其他操作 -->
+    <div class="filter-container">
+      <el-input v-model="listQuery.title" clearable class="filter-item" style="width: 200px;" placeholder="请输入标题关键字" />
+      <el-input v-model="listQuery.content" clearable class="filter-item" style="width: 200px;" placeholder="请输入内容关键字" />
+      <el-button v-permission="['GET /admin/notice/list']" class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">查找</el-button>
+      <el-button :loading="downloadLoading" class="filter-item" type="primary" icon="el-icon-download" @click="handleDownload">导出</el-button>
+    </div>
+
+    <div class="operator-container">
+      <el-button v-permission="['POST /admin/notice/create']" class="filter-item" type="primary" icon="el-icon-edit" @click="handleCreate">添加</el-button>
+      <el-button v-permission="['GET /admin/notice/batch-delete']" class="filter-item" type="danger" icon="el-icon-delete" @click="handleBatchDelete">批量删除</el-button>
+    </div>
+
+    <!-- 查询结果 -->
+    <el-table v-loading="listLoading" :data="list" element-loading-text="正在查询中。。。" border fit highlight-current-row @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" />
+
+      <el-table-column align="center" label="通知标题" prop="title" />
+
+      <el-table-column align="center" label="通知详情" prop="content">
+        <template slot-scope="scope">
+          <el-dialog :visible.sync="contentDialogVisible" title="通知详情">
+            <div v-html="contentDetail" />
+          </el-dialog>
+          <el-button type="primary" size="mini" @click="showContent(scope.row.content)">查看</el-button>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="添加时间" prop="addTime" />
+
+      <el-table-column align="center" label="管理员ID" prop="adminId" />
+
+      <el-table-column align="center" label="操作" min-width="100" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button v-permission="['POST /admin/notice/update']" type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>
+          <el-button v-permission="['POST /admin/notice/delete']" type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" />
+
+    <!-- 添加或修改对话框 -->
+    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
+      <el-form ref="dataForm" :rules="rules" :model="dataForm" status-icon label-position="left" label-width="100px">
+        <el-form-item label="通知标题" prop="title">
+          <el-input v-model="dataForm.title" />
+        </el-form-item>
+        <el-form-item label="通知内容" prop="content">
+          <editor v-model="dataForm.content" :init="editorInit" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">取消</el-button>
+        <el-button v-if="dialogStatus=='create'" type="primary" @click="createData">确定</el-button>
+        <el-button v-else type="primary" @click="updateData">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <el-tooltip placement="top" content="返回顶部">
+      <back-to-top :visibility-height="100" />
+    </el-tooltip>
+
+  </div>
+</template>
+
+<script>
+import { listNotice, createNotice, updateNotice, deleteNotice, batchDeleteNotice } from '@/api/notice'
+import BackToTop from '@/components/BackToTop'
+import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
+import _ from 'lodash'
+import Editor from '@tinymce/tinymce-vue'
+import { createStorage } from '@/api/storage'
+import { getToken } from '@/utils/auth'
+
+export default {
+  name: 'Notice',
+  components: { BackToTop, Pagination, Editor },
+  data() {
+    return {
+      list: [],
+      total: 0,
+      listLoading: true,
+      listQuery: {
+        page: 1,
+        limit: 20,
+        title: undefined,
+        content: undefined,
+        sort: 'add_time',
+        order: 'desc'
+      },
+      multipleSelection: [],
+      contentDetail: '',
+      contentDialogVisible: false,
+      dataForm: {
+        id: undefined,
+        title: undefined,
+        content: undefined
+      },
+      dialogFormVisible: false,
+      dialogStatus: '',
+      textMap: {
+        update: '编辑',
+        create: '创建'
+      },
+      rules: {
+        name: [
+          { required: true, message: '通知标题不能为空', trigger: 'blur' }
+        ]
+      },
+      editorInit: {
+        language: 'zh_CN',
+        height: 200,
+        convert_urls: false,
+        plugins: ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools importcss insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount'],
+        toolbar: ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'],
+        images_upload_handler: function(blobInfo, success, failure) {
+          const formData = new FormData()
+          formData.append('file', blobInfo.blob())
+          createStorage(formData).then(res => {
+            success(res.data.data.url)
+          }).catch(() => {
+            failure('上传失败,请重新上传')
+          })
+        }
+      },
+      downloadLoading: false
+    }
+  },
+  computed: {
+    headers() {
+      return {
+        'X-Litemall-Admin-Token': getToken()
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.listLoading = true
+      listNotice(this.listQuery)
+        .then(response => {
+          this.list = response.data.data.list
+          this.total = response.data.data.total
+          this.listLoading = false
+        })
+        .catch(() => {
+          this.list = []
+          this.total = 0
+          this.listLoading = false
+        })
+    },
+    handleFilter() {
+      this.listQuery.page = 1
+      this.getList()
+    },
+    resetForm() {
+      this.dataForm = {
+        id: undefined,
+        title: undefined,
+        content: undefined
+      }
+    },
+    handleCreate() {
+      this.resetForm()
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['dataForm'].clearValidate()
+      })
+    },
+    createData() {
+      this.$refs['dataForm'].validate(valid => {
+        if (valid) {
+          createNotice(this.dataForm)
+            .then(response => {
+              this.list.unshift(response.data.data)
+              this.dialogFormVisible = false
+              this.$notify.success({
+                title: '成功',
+                message: '创建成功'
+              })
+            })
+            .catch(response => {
+              this.$notify.error({
+                title: '失败',
+                message: response.data.errmsg
+              })
+            })
+        }
+      })
+    },
+    handleUpdate(row) {
+      this.dataForm = Object.assign({}, row)
+      this.dialogStatus = 'update'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['dataForm'].clearValidate()
+      })
+    },
+    updateData() {
+      this.$refs['dataForm'].validate(valid => {
+        if (valid) {
+          updateNotice(this.dataForm)
+            .then(() => {
+              for (const v of this.list) {
+                if (v.id === this.dataForm.id) {
+                  const index = this.list.indexOf(v)
+                  this.list.splice(index, 1, this.dataForm)
+                  break
+                }
+              }
+              this.dialogFormVisible = false
+              this.$notify.success({
+                title: '成功',
+                message: '更新广告成功'
+              })
+            })
+            .catch(response => {
+              this.$notify.error({
+                title: '失败',
+                message: response.data.errmsg
+              })
+            })
+        }
+      })
+    },
+    handleDelete(row) {
+      deleteNotice(row)
+        .then(response => {
+          this.$notify.success({
+            title: '成功',
+            message: '删除通知成功'
+          })
+          const index = this.list.indexOf(row)
+          this.list.splice(index, 1)
+        })
+        .catch(response => {
+          this.$notify.error({
+            title: '失败',
+            message: response.data.errmsg
+          })
+        })
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val
+    },
+    showContent(content) {
+      this.contentDetail = content
+      this.contentDialogVisible = true
+    },
+    handleBatchDelete() {
+      if (this.multipleSelection.length === 0) {
+        this.$message.error('请选择至少一条记录')
+        return
+      }
+      const ids = []
+      _.forEach(this.multipleSelection, function(item) {
+        ids.push(item.id)
+      })
+      batchDeleteNotice({ ids: ids })
+        .then(response => {
+          this.$notify.success({
+            title: '成功',
+            message: '批量删除通知成功'
+          })
+          this.getList()
+        })
+        .catch(response => {
+          this.$notify.error({
+            title: '失败',
+            message: response.data.errmsg
+          })
+        })
+    },
+    handleDownload() {
+      this.downloadLoading = true
+      import('@/vendor/Export2Excel').then(excel => {
+        const tHeader = [
+          '通知ID',
+          '通知标题',
+          '管理员ID',
+          '添加时间',
+          '更新时间'
+        ]
+        const filterVal = [
+          'id',
+          'title',
+          'content',
+          'adminId',
+          'addTime',
+          'updateTime'
+        ]
+        excel.export_json_to_excel2(tHeader, this.list, filterVal, '通知')
+        this.downloadLoading = false
+      })
+    }
+  }
+}
+</script>