Эх сурвалжийг харах

feat: 添加消息通知系统组件和功能

- 新增MessageBox组件用于显示消息通知
- 新增MessageCard组件实现消息卡片样式和动画
- 新增msg.ts存储管理消息状态和类型
- 新增多种消息类型图标
- 在App.vue中集成消息通知组件
- 优化颜色主题匹配功能,提取isDark工具函数
```

这个提交消息:
1. 使用feat类型,因为是新增功能
2. 简明描述了主要新增内容
3. 在body部分列出关键新增组件和功能
4. 使用中文简洁清晰地描述了变更内容
5. 遵循了50字符限制和imperative mood的要求
Sakulin 2 сар өмнө
parent
commit
cb24973b9c

+ 2 - 0
src/App.vue

@@ -5,6 +5,7 @@ import SideMenu from './layouts/BlogMenu.vue';
 import Footer from './layouts/BlogFooter.vue';
 import Footer from './layouts/BlogFooter.vue';
 import { ref } from 'vue';
 import { ref } from 'vue';
 import LoginAlert from '@/layouts/LoginAlert.vue'
 import LoginAlert from '@/layouts/LoginAlert.vue'
+import MessageBox from '@/components/MessageBox.vue'
 
 
 const displayLoginAlert = ref(false);
 const displayLoginAlert = ref(false);
 
 
@@ -29,6 +30,7 @@ const avatarMouseUpHandler = () => {
 <template>
 <template>
   <div class="container">
   <div class="container">
     <div class="aside">
     <div class="aside">
+      <MessageBox/>
       <div class="avatar-box">
       <div class="avatar-box">
         <img src="./assets/headset.jpg" alt="avatar" class="avatar" @mousedown.prevent="avatarMouseDownHandler" @mouseup.prevent="avatarMouseUpHandler"/>
         <img src="./assets/headset.jpg" alt="avatar" class="avatar" @mousedown.prevent="avatarMouseDownHandler" @mouseup.prevent="avatarMouseUpHandler"/>
         <div class="info">
         <div class="info">

+ 38 - 0
src/assets/icons.ts

@@ -85,3 +85,41 @@ export const othersIcon: Icon = {
   </svg>
   </svg>
   `
   `
 }
 }
+
+
+export const successIcon: Icon = {
+  template: `
+  <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
+    <path d="M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667z m0 810.666666c-204.8 0-373.333333-168.533333-373.333333-373.333333S307.2 138.666667 512 138.666667 885.333333 307.2 885.333333 512 716.8 885.333333 512 885.333333z"/>
+    <path d="M674.133333 608c-46.933333 57.6-100.266667 85.333333-162.133333 85.333333s-115.2-27.733333-162.133333-85.333333c-10.666667-12.8-32-14.933333-44.8-4.266667-12.8 10.666667-14.933333 32-4.266667 44.8 59.733333 70.4 130.133333 106.666667 211.2 106.666667s151.466667-36.266667 211.2-106.666667c10.666667-12.8 8.533333-34.133333-4.266667-44.8-12.8-10.666667-34.133333-8.533333-44.8 4.266667zM362.666667 512c23.466667 0 42.666667-19.2 42.666666-42.666667v-64c0-23.466667-19.2-42.666667-42.666666-42.666666s-42.666667 19.2-42.666667 42.666666v64c0 23.466667 19.2 42.666667 42.666667 42.666667zM661.333333 512c23.466667 0 42.666667-19.2 42.666667-42.666667v-64c0-23.466667-19.2-42.666667-42.666667-42.666666s-42.666667 19.2-42.666666 42.666666v64c0 23.466667 19.2 42.666667 42.666666 42.666667z"/>
+  </svg>
+  `
+}
+
+export const failedIcon: Icon = {
+  template: `
+  <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
+    <path d="M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667z m0 810.666666c-204.8 0-373.333333-168.533333-373.333333-373.333333S307.2 138.666667 512 138.666667 885.333333 307.2 885.333333 512 716.8 885.333333 512 885.333333z"/>
+    <path d="M512 597.333333c-81.066667 0-151.466667 36.266667-211.2 106.666667-10.666667 12.8-8.533333 34.133333 4.266667 44.8 12.8 10.666667 34.133333 8.533333 44.8-4.266667 46.933333-57.6 100.266667-85.333333 162.133333-85.333333s115.2 27.733333 162.133333 85.333333c6.4 8.533333 14.933333 10.666667 25.6 10.666667 6.4 0 14.933333-2.133333 21.333334-6.4 12.8-10.666667 14.933333-32 4.266666-44.8-61.866667-70.4-132.266667-106.666667-213.333333-106.666667zM362.666667 512c23.466667 0 42.666667-19.2 42.666666-42.666667v-64c0-23.466667-19.2-42.666667-42.666666-42.666666s-42.666667 19.2-42.666667 42.666666v64c0 23.466667 19.2 42.666667 42.666667 42.666667zM661.333333 512c23.466667 0 42.666667-19.2 42.666667-42.666667v-64c0-23.466667-19.2-42.666667-42.666667-42.666666s-42.666667 19.2-42.666666 42.666666v64c0 23.466667 19.2 42.666667 42.666666 42.666667z"/>
+  </svg>
+  `
+}
+
+export const warningIcon: Icon = {
+  template: `
+  <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
+    <path d="M512 949.333333C270.933333 949.333333 74.666667 753.066667 74.666667 512S270.933333 74.666667 512 74.666667 949.333333 270.933333 949.333333 512 753.066667 949.333333 512 949.333333z m0-810.666666C307.2 138.666667 138.666667 307.2 138.666667 512S307.2 885.333333 512 885.333333 885.333333 716.8 885.333333 512 716.8 138.666667 512 138.666667z"/>
+    <path d="M362.666667 512c-23.466667 0-42.666667-19.2-42.666667-42.666667v-64c0-23.466667 19.2-42.666667 42.666667-42.666666s42.666667 19.2 42.666666 42.666666v64c0 23.466667-19.2 42.666667-42.666666 42.666667zM661.333333 512c-23.466667 0-42.666667-19.2-42.666666-42.666667v-64c0-23.466667 19.2-42.666667 42.666666-42.666666s42.666667 19.2 42.666667 42.666666v64c0 23.466667-19.2 42.666667-42.666667 42.666667zM699.733333 714.666667H324.266667c-17.066667 0-32-14.933333-32-32s14.933333-32 32-32h373.333333c17.066667 0 32 14.933333 32 32s-12.8 32-29.866667 32z"/>
+  </svg>
+  `
+}
+
+export const infoIcon: Icon = {
+  template: `
+  <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
+    <path d="M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667z m0 810.666666c-204.8 0-373.333333-168.533333-373.333333-373.333333S307.2 138.666667 512 138.666667 885.333333 307.2 885.333333 512 716.8 885.333333 512 885.333333z"/>
+    <path d="M512 320m-42.666667 0a42.666667 42.666667 0 1 0 85.333334 0 42.666667 42.666667 0 1 0-85.333334 0Z"/>
+    <path d="M512 437.333333c-17.066667 0-32 14.933333-32 32v234.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V469.333333c0-17.066667-14.933333-32-32-32z"/>
+  </svg>
+  `
+}

+ 23 - 0
src/components/MessageBox.vue

@@ -0,0 +1,23 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useMessagePipeStore } from '@/stores/msg.ts'
+import MessageCard from '@/components/MessageCard.vue'
+
+const msgPipe = useMessagePipeStore()
+
+const msgs = computed(() => msgPipe.pipe)
+</script>
+
+<template>
+  <div class="msg-box">
+    <MessageCard v-for="msg in msgs" :key="msg.key" :target="msg" />
+  </div>
+</template>
+
+<style scoped>
+.msg-box {
+  display: flex;
+  flex-direction: column;
+  transition: all 0.3s;
+}
+</style>

+ 120 - 0
src/components/MessageCard.vue

@@ -0,0 +1,120 @@
+<script setup lang="ts">
+import { type Msg, useMessagePipeStore } from '@/stores/msg.ts'
+import { computed, onMounted, onUnmounted, ref } from 'vue'
+import { delay } from '@/utils'
+import { failedIcon, infoIcon, successIcon, warningIcon } from '@/assets/icons.ts'
+
+const props = defineProps<{
+  target: Msg
+}>()
+
+
+
+const prefixIcon = computed(() => {
+  switch (props.target.type) {
+    case 'danger':
+      return failedIcon
+    case 'warning':
+      return warningIcon
+    case 'info':
+      return infoIcon
+    case 'success':
+      return successIcon
+    default:
+      return infoIcon
+  }
+})
+
+function baseColor() {
+  switch (props.target.type) {
+    case 'danger':
+      return '#ff4c4c'
+    case 'warning':
+      return '#ffbb70'
+    case 'info':
+      return '#ffffff'
+    case 'success':
+      return '#ffffff'
+    default:
+      return '#ffffff'
+  }
+}
+
+const buttonLineStyle = ref({
+  width: '100%',
+  backgroundColor: baseColor(),
+})
+
+const cardStyle = ref({
+  height: '0',
+  marginBottom: '12px',
+  color: baseColor(),
+  fill: baseColor(),
+})
+
+let lock = false
+
+async function onUpdate() {
+  if (lock) return
+  if (useMessagePipeStore().pipe[0].key == props.target.key) {
+    lock = true
+    buttonLineStyle.value.width = '0'
+    await delay(5000)
+    cardStyle.value.height = '0'
+    cardStyle.value.marginBottom = '0'
+    await delay(320)
+    useMessagePipeStore().pop()
+  }
+}
+
+onMounted(async () => {
+  useMessagePipeStore().submit(onUpdate)
+  await delay(20)
+  cardStyle.value.height = '30px'
+  onUpdate()
+})
+
+onUnmounted(() => {
+  useMessagePipeStore().unsubmit(onUpdate)
+})
+</script>
+
+<template>
+  <div class="msg-card" :style="cardStyle">
+    <div class="bottom-line" :style="buttonLineStyle" />
+    <div class="card-content">
+      <div class="prefix-icon" v-html="prefixIcon.template"/>
+      <div class="card-text">{{ props.target.message }}</div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.bottom-line {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  height: 2px;
+  transition: width 5s;
+}
+
+.msg-card {
+  overflow: hidden;
+  padding-left: 12px;
+  position: relative;
+  padding-right: 12px;
+  transition: all 0.3s;
+}
+
+.card-content {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.card-content .prefix-icon {
+  width: 24px;
+  height: 24px;
+}
+</style>

+ 60 - 0
src/stores/msg.ts

@@ -0,0 +1,60 @@
+import { defineStore } from 'pinia'
+
+export interface Msg {
+  type: 'info' | 'warning' | 'danger' | 'success'
+  message: string
+  key: number
+}
+
+export const useMessagePipeStore = defineStore('msg_pipe', {
+  state: () => ({
+    pipe: [] as Msg[],
+    listener: [] as (() => void)[],
+    counter: 0,
+  }),
+  actions: {
+    info(message: string) {
+      this.pipe.push({
+        message,
+        type: 'info',
+        key: this.counter++,
+      })
+      this.listener.forEach(e => e.call(undefined))
+    },
+    warning(message: string) {
+      this.pipe.push({
+        message,
+        type: 'warning',
+        key: this.counter++,
+      })
+      this.listener.forEach(e => e.call(undefined))
+    },
+    danger(message: string) {
+      this.pipe.push({
+        message,
+        type: 'danger',
+        key: this.counter++,
+      })
+      this.listener.forEach(e => e.call(undefined))
+    },
+    success(message: string) {
+      this.pipe.push({
+        message,
+        type: 'success',
+        key: this.counter++,
+      })
+      this.listener.forEach(e => e.call(undefined))
+    },
+    submit(update: () => void) {
+      if (!this.listener.includes(update))
+        this.listener.push(update)
+    },
+    unsubmit(update: () => void) {
+      this.listener.splice(this.listener.indexOf(update), 1)
+    },
+    pop() {
+      this.pipe.splice(0, 1)
+      this.listener.forEach(e => e.call(undefined))
+    },
+  },
+})

+ 5 - 1
src/utils/index.ts

@@ -26,8 +26,12 @@ function lightenHexColor(hexColor: string, percent: number = 60): string {
   return '#' + toHex(r) + toHex(g) + toHex(b)
   return '#' + toHex(r) + toHex(g) + toHex(b)
 }
 }
 
 
+export function isDark() {
+  return window.matchMedia('(prefers-color-scheme: dark)').matches
+}
+
 export function colorMatchTheme(hexColor: string) {
 export function colorMatchTheme(hexColor: string) {
-  if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+  if (isDark()) {
     return lightenHexColor(hexColor);
     return lightenHexColor(hexColor);
   }
   }
   return hexColor;
   return hexColor;