Prechádzať zdrojové kódy

增加账号异常提醒

dodo hold 1 rok pred
rodič
commit
47f87ffedb

+ 14 - 0
src/api/message.ts

@@ -36,3 +36,17 @@ export interface ChatRecord {
 export function queryChatList() {
   return axios.post<ChatRecord[]>('/api/chat/list');
 }
+
+export interface AccountWarningStats {
+  [platform: string]: {
+    [type: string]: [number, number]; // [总数, 在线数]
+  };
+}
+
+export interface AccountWarningResponse {
+  data: AccountWarningStats;
+}
+
+export function queryAccountWarning() {
+  return axios.get<AccountWarningResponse>('/api/message/AccountWarning');
+}

+ 203 - 0
src/components/account-warning/index.vue

@@ -0,0 +1,203 @@
+<template>
+  <div class="account-warning-container" v-if="platformNames.length > 0 || isLoading">
+    <a-popover placement="bottom" trigger="hover">
+      <div class="warning-tags">
+        <icon-loading v-if="isLoading" style="margin-right: 8px;" />
+        <a-tag 
+          v-for="platform in platformNames" 
+          :key="platform"
+          :color="getPlatformStatus(platform).hasOffline ? '#f53f3f' : '#165dff'"
+          :style="{ marginRight: '8px' }"
+          :class="{ 'warning-pulse': getPlatformStatus(platform).hasOffline }"
+        >
+          {{ getPlatformDisplayName(platform) }}:{{ getPlatformStatus(platform).status }}
+          <icon-exclamation-circle v-if="getPlatformStatus(platform).hasOffline" 
+            style="margin-left: 4px;" 
+            class="warning-icon" />
+        </a-tag>
+      </div>
+      <template #content>
+        <div class="warning-detail">
+          <div v-for="platform in platformNames" :key="platform" class="platform-detail">
+            <div class="platform-title">{{ getPlatformDisplayName(platform) }}</div>
+            <div class="platform-stats">
+              <a-tag
+                v-for="(stats, type) in accountWarningData[platform]"
+                :key="type"
+                :color="stats[1] === 0 ? '#f53f3f' : '#165dff'"
+                size="small"
+                :style="{ marginRight: '4px', marginBottom: '4px' }"
+              >
+                {{ type }}: {{ stats[1] }}/{{ stats[0] }}
+              </a-tag>
+            </div>
+          </div>
+        </div>
+      </template>
+    </a-popover>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, onMounted, onUnmounted } from 'vue';
+import { queryAccountWarning, type AccountWarningStats } from '@/api/message';
+
+const accountWarningData = ref<AccountWarningStats>({});
+const isLoading = ref(false);
+let intervalId: ReturnType<typeof setInterval> | null = null;
+let loadingTimer: ReturnType<typeof setTimeout> | null = null;
+
+const fetchAccountWarning = async () => {
+  const startTime = Date.now();
+  isLoading.value = true;
+  
+  if (loadingTimer) {
+    clearTimeout(loadingTimer);
+  }
+  
+  try {
+    const { data } = await queryAccountWarning();
+    console.log(data);
+    accountWarningData.value = data;
+    
+    const elapsed = Date.now() - startTime;
+    const remainingTime = Math.max(500 - elapsed, 0);
+    
+    loadingTimer = setTimeout(() => {
+      isLoading.value = false;
+    }, remainingTime);
+    
+  } catch (error) {
+    console.error('获取账户警告数据失败:', error);
+    
+    const elapsed = Date.now() - startTime;
+    const remainingTime = Math.max(500 - elapsed, 0);
+    
+    loadingTimer = setTimeout(() => {
+      isLoading.value = false;
+    }, remainingTime);
+  }
+};
+
+const platformNames = computed(() => Object.keys(accountWarningData.value || {}));
+
+const getPlatformDisplayName = (platform: string) => {
+  const nameMap: Record<string, string> = {
+    'tb': '淘宝',
+    'jd': '京东'
+  };
+  return nameMap[platform] || platform;
+};
+
+const getPlatformStatus = (platform: string) => {
+  const platformData = accountWarningData.value[platform];
+  if (!platformData) return { status: '无数据', hasOffline: false };
+  
+  const hasOffline = Object.values(platformData).some(([, onlineCount]) => onlineCount === 0);
+  
+  if (hasOffline) {
+    const offlineTypes = Object.entries(platformData)
+      .filter(([, [, onlineCount]]) => onlineCount === 0)
+      .map(([type]) => type);
+    return { 
+      status: `异常(${offlineTypes.join(',')})`, 
+      hasOffline: true 
+    };
+  }
+  return { status: '正常', hasOffline: false };
+};
+
+onMounted(() => {
+  fetchAccountWarning();
+  intervalId = setInterval(fetchAccountWarning, 5000);
+});
+
+onUnmounted(() => {
+  if (intervalId) {
+    clearInterval(intervalId);
+    intervalId = null;
+  }
+  if (loadingTimer) {
+    clearTimeout(loadingTimer);
+    loadingTimer = null;
+  }
+});
+</script>
+
+<style scoped lang="less">
+.account-warning-container {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+
+  .warning-tags {
+    display: flex;
+    align-items: center;
+  }
+}
+
+.warning-detail {
+  min-width: 200px;
+  
+  .platform-detail {
+    margin-bottom: 12px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+    
+    .platform-title {
+      font-weight: bold;
+      margin-bottom: 6px;
+      color: var(--color-text-1);
+    }
+    
+    .platform-stats {
+      display: flex;
+      flex-wrap: wrap;
+    }
+  }
+}
+
+.warning-pulse {
+  animation: pulse 1s infinite;
+  box-shadow: 0 0 12px rgba(245, 63, 63, 0.8);
+  font-weight: bold;
+  border: 2px solid rgba(245, 63, 63, 0.6);
+}
+
+.warning-icon {
+  animation: shake 1s infinite;
+  filter: drop-shadow(0 0 4px rgba(245, 63, 63, 0.8));
+}
+
+@keyframes pulse {
+  0% {
+    box-shadow: 0 0 12px rgba(245, 63, 63, 0.8);
+    border-color: rgba(245, 63, 63, 0.6);
+    transform: scale(1);
+  }
+  50% {
+    box-shadow: 0 0 24px rgba(245, 63, 63, 1);
+    border-color: rgba(245, 63, 63, 1);
+    transform: scale(1.05);
+  }
+  100% {
+    box-shadow: 0 0 12px rgba(245, 63, 63, 0.8);
+    border-color: rgba(245, 63, 63, 0.6);
+    transform: scale(1);
+  }
+}
+
+@keyframes shake {
+  0%, 100% {
+    transform: translateX(0);
+  }
+  10%, 30%, 50%, 70%, 90% {
+    transform: translateX(-2px);
+  }
+  20%, 40%, 60%, 80% {
+    transform: translateX(2px);
+  }
+}
+</style>

+ 6 - 1
src/components/navbar/index.vue

@@ -47,6 +47,9 @@
             <Menu v-if="topMenu" />
         </div>
         <ul class="right-side">
+            <li>
+                <AccountWarning />
+            </li>
             <li>
                 <a-tooltip :content="$t('settings.search')">
                     <a-button class="nav-btn" type="outline" :shape="'circle'">
@@ -167,12 +170,13 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, inject } from 'vue';
+import { computed, ref, inject, onMounted } from 'vue';
 import { useDark, useToggle, useFullscreen, } from '@vueuse/core';
 import { useAppStore, useUserStore } from '@/store';
 import { LOCALE_OPTIONS } from '@/locale';
 import useLocale from '@/hooks/locale';
 import useUser from '@/hooks/user';
+import AccountWarning from '@/components/account-warning/index.vue';
 import Menu from '@/components/menu/index.vue';
 import MessageBox from '../message-box/index.vue';
 
@@ -189,6 +193,7 @@ const theme = computed(() => {
     return appStore.theme;
 });
 const topMenu = computed(() => appStore.topMenu && appStore.menu);
+
 const isDark = useDark({
     selector: 'body',
     attribute: 'arco-theme',