|
|
@@ -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>
|