zhangjie 3 年 前
コミット
84a75d0bed

+ 0 - 15
src/assets/back.svg

@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="14px" height="12px" viewBox="0 0 14 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
-    <title>返回</title>
-    <desc>Created with Sketch.</desc>
-    <defs></defs>
-    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g id="9-管理端-公告详情" transform="translate(-1327.000000, -120.000000)">
-            <g id="返回" transform="translate(1327.000000, 120.000000)">
-                <polyline id="Path-2" stroke="#CCCCCC" stroke-width="2" transform="translate(4.500000, 6.000000) scale(-1, 1) translate(-4.500000, -6.000000) " points="2 1 7 6 2 11"></polyline>
-                <rect id="Rectangle-8" fill="#CCCCCC" x="2" y="5" width="12" height="2"></rect>
-            </g>
-        </g>
-    </g>
-</svg>

ファイルの差分が大きいため隠しています
+ 0 - 9
src/assets/sms-read.svg


+ 0 - 14
src/assets/sms-unread.svg

@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="16px" height="12px" viewBox="0 0 16 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
-    <title>sms-未读</title>
-    <desc>Created with Sketch.</desc>
-    <defs></defs>
-    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g id="8-考生端-公告列表" transform="translate(-260.000000, -235.000000)" fill="#FDAD08">
-            <g id="sms-未读" transform="translate(260.000000, 235.000000)">
-                <path d="M10.2612161,6.01241042 L15.9078877,0.460631922 C15.7791453,0.191589577 15.5045861,0.00576547231 15.1865006,0.00576547231 L0.799296703,0.00576547231 C0.481230769,0.00576547231 0.206652015,0.191589577 0.07798779,0.460631922 L5.7245812,6.01241042 C6.97732845,6.80249511 9.00846886,6.80249511 10.2612161,6.01241042 Z M0,1.563557 L0,10.4731661 L4.69352869,6.00699674 L0,1.563557 Z M11.2922686,6.00699674 L15.9857973,10.4731661 L15.9857973,1.563557 L11.2922686,6.00699674 Z M10.5570891,6.70301629 C9.14094261,7.60211726 6.8448547,7.60211726 5.42872772,6.70301629 L5.21726984,6.50284691 L0.0903540904,11.569544 C0.223746032,11.8252964 0.490998779,11.9999609 0.799296703,11.9999609 L15.1865006,11.9999609 C15.4948181,11.9999609 15.7621294,11.8252964 15.8954432,11.569544 L10.768547,6.50284691 L10.5570891,6.70301629 Z" id="Fill-1"></path>
-            </g>
-        </g>
-    </g>
-</svg>

+ 12 - 0
src/components/MainLayout/MainLayout.vue

@@ -9,6 +9,7 @@ import {
 import { useRoute, useRouter } from "vue-router";
 import QM_LOGO from "./qm-logo.png";
 import VueQrcode from "@chenfengyuan/vue-qrcode";
+import SiteMessageNotificationVue from "@/features/SiteMessage/SiteMessageNotification.vue";
 
 const router = useRouter();
 const route = useRoute();
@@ -103,6 +104,7 @@ const toLogout = () => {
     act: "点击",
     stk: "退出登录",
   });
+  void router.push({ name: "UserLogin" });
 };
 
 onMounted(async () => {
@@ -212,6 +214,15 @@ watch(
             @click="swithMenu(menu)"
           >
             <span>{{ menu.name }}</span>
+            <n-badge
+              v-if="
+                menu.routeName === 'SiteMessage' &&
+                store.siteMessage.unreadCount
+              "
+              class="qm-ml-10"
+              :value="store.siteMessage.unreadCount"
+              :max="99"
+            />
           </div>
         </div>
       </div>
@@ -239,6 +250,7 @@ watch(
       </div>
     </div>
   </div>
+  <SiteMessageNotificationVue />
 </template>
 
 <style scoped>

+ 12 - 23
src/features/SiteMessage/SiteMessage.vue

@@ -1,11 +1,12 @@
 <script lang="ts" setup>
 import { SiteMessage } from "@/types/student-client";
-import { messageListApi, updateNotieReadStatusApi } from "@/api/siteMessage";
+import { updateNotieReadStatusApi } from "@/api/siteMessage";
 import { store } from "@/store/store";
-import { DataTableColumns, useMessage } from "naive-ui";
+import { DataTableColumns, NIcon, useMessage } from "naive-ui";
 import { h, computed, onMounted } from "vue";
 import { useRouter } from "vue-router";
 import { RowKey } from "naive-ui/lib/data-table/src/interface";
+import { MailOpen, MailUnread } from "@vicons/ionicons5";
 
 const message = useMessage();
 const router = useRouter();
@@ -19,12 +20,11 @@ const columns: DataTableColumns<SiteMessage> = [
     key: "title",
     render(row) {
       return h("p", { class: "qm-link-text", onClick: () => toDetail(row) }, [
-        h("i", {
-          class: [
-            "icon",
-            "qm-mr-10",
-            row.hasRead ? "icon-sms-read" : "icon-sms-unread",
-          ],
+        h(NIcon, {
+          component: row.hasRead ? MailOpen : MailUnread,
+          size: 18,
+          class: "qm-mr-10 qm-vertical-mid",
+          color: row.hasRead ? "#999" : "#2d8cf0",
         }),
         h("span", {}, row.title),
       ]);
@@ -46,18 +46,6 @@ const pagination = $ref({
   },
 });
 
-const getList = async () => {
-  const res = await messageListApi(store.user.id, store.siteMessagesTimeStamp);
-  const messages = res.data.map((item) => {
-    let nitem = { ...item };
-    if (item.hasRecalled) {
-      nitem.title = "发送者已撤回消息:" + nitem.title;
-      nitem.content = "该消息已经被发送者撤回。";
-    }
-    return nitem;
-  });
-  store.siteMessage.messages = messages;
-};
 const selectedChange = (ids: RowKey[]) => {
   selectedMessageIds = ids;
 };
@@ -73,11 +61,11 @@ const toMarkRead = async () => {
   await updateNotieReadStatusApi(selectedMessageIds.join());
   message.success("修改成功!");
   store.updateSiteMessagesTimeStamp();
-  void getList();
+  void store.fetchSiteMessage();
 };
 
 onMounted(() => {
-  void getList();
+  void store.fetchSiteMessage();
 });
 </script>
 
@@ -85,7 +73,8 @@ onMounted(() => {
   <div class="box-justify qm-mb-20">
     <h3 class="qm-big-text">公告通知</h3>
     <n-button @click="toMarkRead"
-      ><i class="icon icon-message-read qm-mr-10"></i> 标记为已读</n-button
+      ><n-icon class="qm-mr-5" :component="MailOpen" :size="16"></n-icon
+      >标记为已读</n-button
     >
   </div>
   <div class="part-box">

+ 47 - 7
src/features/SiteMessage/SiteMessageDetail.vue

@@ -2,7 +2,9 @@
 import { SiteMessage } from "@/types/student-client";
 import { store } from "@/store/store";
 import { useRoute } from "vue-router";
-import { onMounted } from "vue";
+import { onMounted, watch } from "vue";
+import { ArrowBack } from "@vicons/ionicons5";
+import { updateNotieReadStatusApi } from "@/api/siteMessage";
 
 let notice: SiteMessage | undefined = $ref();
 const route = useRoute();
@@ -11,27 +13,65 @@ const toBack = () => {
   window.history.go(-1);
 };
 
-onMounted(() => {
-  const curNoticeId = Number(route.params.noticeId);
+const markMessageRead = async (noticeId: string) => {
+  if (!noticeId) return;
+  await updateNotieReadStatusApi(noticeId);
+  store.updateSiteMessagesTimeStamp();
+  void store.fetchSiteMessage();
+};
+
+const initData = () => {
+  const paramNoticeId = route.params.noticeId as string;
+  const curNoticeId = Number(paramNoticeId);
   const curNotice = store.siteMessage.messages.find(
     (item) => item.id === curNoticeId
   );
   notice = curNotice;
+
+  if (notice?.hasRead) return;
+
+  void markMessageRead(paramNoticeId);
+};
+
+onMounted(() => {
+  initData();
 });
+
+watch(
+  () => route.params,
+  () => {
+    initData();
+  }
+);
 </script>
 
 <template>
   <div class="box-justify qm-mb-20">
     <div></div>
-    <n-button @click="toBack"
-      ><i class="icon icon-back qm-mr-10"></i> 返回列表</n-button
+    <n-button @click="toBack">
+      <n-icon class="qm-mr-5" :component="ArrowBack" :size="16"></n-icon>
+      返回列表</n-button
     >
   </div>
   <div v-if="notice" class="part-box">
-    <div class="message-title tw-text-center">
-      <h3 class="text-4xl">{{ notice.title }}</h3>
+    <div class="message-title">
+      <h3>{{ notice.title }}</h3>
       <p class="tips-info">发布时间: {{ notice.publishTime }}</p>
     </div>
     <div class="message-desc" v-html="notice.content"></div>
   </div>
 </template>
+
+<style scoped>
+.message-title {
+  text-align: center;
+}
+.message-title > h3 {
+  font-size: 22px;
+  line-height: 1;
+  padding: 10px;
+}
+.message-desc {
+  padding-top: 30px;
+}
+</style>

+ 117 - 0
src/features/SiteMessage/SiteMessageNotification.vue

@@ -0,0 +1,117 @@
+<script setup lang="ts">
+import { store } from "@/store/store";
+import { watch, onMounted, computed } from "vue";
+import { useRouter } from "vue-router";
+import { ChevronForward } from "@vicons/ionicons5";
+
+const router = useRouter();
+
+const firstUnreadMessage = computed(() => store.siteMessage.firstUnreadMessage);
+const showPopover = computed(() => !!firstUnreadMessage.value);
+
+const updateSiteMessage = () => {
+  const { messages, ignoreMessageIds } = store.siteMessage;
+  if (!messages) return;
+  const unreadMessages = messages
+    .filter((item) => !item.hasRead && !ignoreMessageIds.includes(item.id))
+    .reverse();
+  store.siteMessage.unreadCount = unreadMessages.length;
+  store.siteMessage.firstUnreadMessage = unreadMessages[0] || null;
+};
+
+const toView = () => {
+  if (!firstUnreadMessage.value) return;
+  void router.push({
+    name: "SiteMessageDetail",
+    params: {
+      noticeId: firstUnreadMessage.value.id,
+    },
+  });
+};
+
+const toIgnore = () => {
+  if (!firstUnreadMessage.value) return;
+
+  if (store.siteMessage.ignoreMessageIds.includes(firstUnreadMessage.value.id))
+    return;
+
+  store.siteMessage.ignoreMessageIds.push(firstUnreadMessage.value.id);
+  updateSiteMessage();
+};
+
+watch(
+  () => store.siteMessage.messages,
+  () => {
+    console.log("site message change");
+
+    updateSiteMessage();
+  },
+  {
+    deep: true,
+    immediate: true,
+  }
+);
+
+onMounted(() => {
+  void store.fetchSiteMessage();
+});
+</script>
+
+<template>
+  <n-popover
+    trigger="manual"
+    :show="showPopover"
+    :showArrow="false"
+    placement="top-end"
+  >
+    <template #trigger>
+      <div class="site-message-notification"></div>
+    </template>
+    <div v-if="firstUnreadMessage" class="site-message-main">
+      <h2 class="site-message-title">
+        {{ firstUnreadMessage.title }}
+      </h2>
+      <div class="site-message-body" v-html="firstUnreadMessage.content"></div>
+      <div class="site-message-action box-justify">
+        <n-button text color="#328358" @click="toView">
+          详情
+          <n-icon :component="ChevronForward" :size="16"></n-icon>
+        </n-button>
+        <n-button text color="#999" @click="toIgnore">忽略</n-button>
+      </div>
+    </div>
+  </n-popover>
+</template>
+
+<style scoped>
+.site-message-notification {
+  position: fixed;
+  bottom: 20px;
+  right: 15px;
+  width: 280px;
+  height: 0;
+  z-index: 999;
+}
+.site-message-main {
+  width: 280px;
+  margin: -8px -14px;
+  border-radius: var(--app-border-radius);
+  overflow: hidden;
+}
+.site-message-title {
+  padding: 10px;
+  line-height: 1;
+  font-size: var(--app-font-size);
+  color: var(--app-color-white);
+  background-color: var(--app-color-success);
+  text-align: center;
+}
+.site-message-body {
+  padding: 20px;
+  height: 140px;
+  overflow: hidden;
+}
+.site-message-action {
+  padding: 10px 15px;
+}
+</style>

+ 26 - 0
src/store/store.ts

@@ -1,5 +1,6 @@
 import { defineStore } from "pinia";
 import { Store } from "@/types/student-client";
+import { messageListApi } from "@/api/siteMessage";
 
 const initStore: Store = {
   user: {} as Store["user"],
@@ -66,6 +67,31 @@ export const useStore = defineStore("ecs", {
     updateSiteMessagesTimeStamp() {
       store.siteMessagesTimeStamp = Date.now();
     },
+    async fetchSiteMessage() {
+      const res = await messageListApi(
+        store.user.id,
+        store.siteMessagesTimeStamp
+      );
+      const messages = res.data.map((item) => {
+        const nitem = { ...item };
+        if (item.hasRecalled) {
+          nitem.title = "发送者已撤回消息:" + nitem.title;
+          nitem.content = "该消息已经被发送者撤回。";
+        }
+        return nitem;
+      });
+      const prevSiteMessage = Object.assign(
+        {},
+        {
+          messages: [],
+          firstUnreadMessage: null,
+          ignoreMessageIds: [],
+          unreadCount: 0,
+        },
+        store.siteMessage
+      );
+      store.siteMessage = Object.assign({}, prevSiteMessage, { messages });
+    },
   },
 });
 

+ 1 - 0
src/styles/cssvar.css

@@ -12,6 +12,7 @@
   --app-color-primary-light: #679cd4;
   --app-color-success: #13bb8a;
   --app-color-success-light: #46b379;
+  --app-color-success-light: #328358;
   --app-color-warning: #f0a020;
   --app-color-warning-light: #f3b34d;
   --app-color-error: #ed4014;

+ 11 - 1
src/styles/global.css

@@ -1,7 +1,6 @@
 @import "./tailwind.css";
 @import "./nprogress.css";
 @import "./cssvar.css";
-@import "./icon.css";
 
 body {
   font-size: var(--app-font-size);
@@ -114,6 +113,12 @@ body {
   color: var(--app-color-primary);
 }
 
+.qm-ml-5 {
+  margin-left: 5px;
+}
+.qm-ml-10 {
+  margin-left: 10px;
+}
 .qm-mr-5 {
   margin-right: 5px;
 }
@@ -129,6 +134,9 @@ body {
 .qm-mb-20 {
   margin-bottom: 20px;
 }
+.qm-vertical-mid {
+  vertical-align: middle;
+}
 
 .main-text-color {
   color: var(--app-color-text-main);
@@ -164,3 +172,5 @@ body {
   align-items: center;
   justify-content: space-between;
 }
+
+/* naive ui custome */

+ 0 - 24
src/styles/icon.css

@@ -1,24 +0,0 @@
-/* icon */
-.icon {
-  display: inline-block;
-  vertical-align: middle;
-  width: 16px;
-  height: 16px;
-  background-repeat: no-repeat;
-  background-size: 100% 100%;
-}
-.icon-sms-read {
-  width: 16px;
-  height: 14px;
-  background-image: url(../assets/sms-read.svg);
-}
-.icon-sms-unread {
-  width: 16px;
-  height: 14px;
-  background-image: url(../assets/sms-unread.svg);
-}
-.icon-back {
-  width: 16px;
-  height: 14px;
-  background-image: url(../assets/back.svg);
-}

+ 1 - 1
src/types/student-client.d.ts

@@ -107,7 +107,7 @@ export type Store = {
   siteMessage: {
     messages: SiteMessage[];
     /** 第一个未读消息 popup用 computed */
-    firstUnreadMessage: SiteMessage;
+    firstUnreadMessage: SiteMessage | null;
     /** 忽略的站内消息,不弹popup */
     ignoreMessageIds: number[];
     /** 未读消息总数 */

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません