通过路由缓存和组件优化增强性能和用户体验

- 在 App.vue 中实现路由组件缓存
- 通过性能监控和资源管理优化 AdminLayout
- 为常用组件添加延迟加载和预加载
- 通过 keepAlive 元和块命名改进路由器配置
- 通过更好的加载和错误处理来增强视频组件
- 使用精致的 UI 和数据呈现更新仪表板和微信模块
This commit is contained in:
wzclm 2025-03-09 14:13:15 +08:00
parent 6fb1f781d4
commit 91df104b0d
9 changed files with 423 additions and 93 deletions

View File

@ -2,13 +2,31 @@
import { RouterView } from "vue-router"; import { RouterView } from "vue-router";
import { onMounted } from 'vue'; import { onMounted } from 'vue';
//
const cachedViews = [
'Dashboard',
'WechatConfig',
'WechatTemplates',
'WechatLogs',
'UserManagement',
'RoleManagement',
'PermissionManagement',
'DroneManagement',
'CameraManagement',
'SensorManagement'
]
onMounted(() => { onMounted(() => {
document.title = 'AI智慧平台 - 智能管理系统'; document.title = 'AI智慧平台 - 智能管理系统';
}); });
</script> </script>
<template> <template>
<RouterView /> <router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" />
</keep-alive>
</router-view>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@ -1,10 +1,9 @@
<script setup> <script setup>
import { ref, watch, computed } from "vue"; import { ref, watch, computed, markRaw, shallowRef, defineComponent, onMounted, onBeforeUnmount, onActivated, onDeactivated } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { useUserStore } from '../stores/user'; import { useUserStore } from '../stores/user';
import { useSystemLogStore } from '../stores/systemLog'; import { useSystemLogStore } from '../stores/systemLog';
import { markRaw } from 'vue';
import { import {
HomeFilled, HomeFilled,
Monitor, Monitor,
@ -33,8 +32,11 @@ const activeMenu = ref(route.path);
const userStore = useUserStore(); const userStore = useUserStore();
const systemLogStore = useSystemLogStore(); const systemLogStore = useSystemLogStore();
// 使 markRaw //
const icons = { const isRouting = ref(false);
// 使 shallowRef
const icons = shallowRef({
HomeFilled: markRaw(HomeFilled), HomeFilled: markRaw(HomeFilled),
Monitor: markRaw(Monitor), Monitor: markRaw(Monitor),
Setting: markRaw(Setting), Setting: markRaw(Setting),
@ -52,7 +54,7 @@ const icons = {
InfoFilled: markRaw(InfoFilled), InfoFilled: markRaw(InfoFilled),
Grid: markRaw(Grid), Grid: markRaw(Grid),
ChatDotRound: markRaw(ChatDotRound) ChatDotRound: markRaw(ChatDotRound)
}; });
// //
const iconMapping = { const iconMapping = {
@ -70,53 +72,56 @@ const iconMapping = {
'wechat': 'ChatDotRound' 'wechat': 'ChatDotRound'
}; };
// //
const menuRoutes = computed(() => { const menuRoutes = computed(() => {
const routes = router.options.routes;
//
const mainRoute = routes.find(route => route.path === '/' && route.children);
if (!mainRoute) return [];
//
const children = mainRoute.children || [];
// dashboard
return children.filter(route => {
return route.path !== 'dashboard' && route.meta && !route.meta.hideInMenu;
});
});
//
const getSubMenus = (path) => {
const routes = router.options.routes; const routes = router.options.routes;
const mainRoute = routes.find(route => route.path === '/' && route.children); const mainRoute = routes.find(route => route.path === '/' && route.children);
if (!mainRoute) return []; if (!mainRoute) return [];
return mainRoute.children?.filter(route => { return mainRoute.children?.filter(route => {
return route.path.startsWith(path + '/') && route.meta && !route.meta.hideInMenu; return route.path !== 'dashboard' && route.meta && !route.meta.hideInMenu;
}).map(route => ({ }).map(route => ({
...route, ...route,
title: route.meta.title icon: icons.value[iconMapping[route.path.split('/')[0]] || 'Document'],
children: route.children?.filter(child => !child.meta?.hideInMenu).map(child => ({
...child,
fullPath: `/${route.path}/${child.path}`
}))
})) || []; })) || [];
}; });
// //
const getIcon = (routePath) => { const getIcon = (routePath) => {
const key = routePath.split('/')[0]; const key = routePath.split('/')[0];
return icons[iconMapping[key] || 'Document']; return icons.value[iconMapping[key] || 'Document'];
}; };
// //
watch( const throttle = (fn, delay) => {
() => route.path, let last = 0;
(newPath) => { return (...args) => {
activeMenu.value = newPath; const now = Date.now();
if (now - last > delay) {
last = now;
fn.apply(this, args);
}
};
};
// 使
const handleSelect = throttle(async (key) => {
if (isRouting.value || key === route.path) return;
try {
isRouting.value = true;
await router.push(key);
} finally {
// 200ms
setTimeout(() => {
isRouting.value = false;
}, 200);
} }
); }, 300); // 300ms
const handleSelect = (key) => {
router.push(key);
};
// 退 // 退
const handleLogout = () => { const handleLogout = () => {
@ -129,7 +134,6 @@ const handleLogout = () => {
type: 'warning', type: 'warning',
} }
).then(() => { ).then(() => {
// 退
systemLogStore.addLog({ systemLogStore.addLog({
type: "用户操作", type: "用户操作",
user: userStore.username || '', user: userStore.username || '',
@ -138,12 +142,9 @@ const handleLogout = () => {
status: "成功", status: "成功",
detail: "用户退出登录" detail: "用户退出登录"
}); });
// 退
userStore.logout(); userStore.logout();
router.push('/login'); router.push('/login');
}).catch(() => { }).catch(() => {});
// 退
});
}; };
// //
@ -151,10 +152,90 @@ const handleCommand = (command) => {
if (command === 'logout') { if (command === 'logout') {
handleLogout(); handleLogout();
} else if (command === 'profile') { } else if (command === 'profile') {
//
router.push('/system/profile'); router.push('/system/profile');
} }
}; };
//
watch(
() => route.path,
(newPath) => {
if (activeMenu.value !== newPath) {
activeMenu.value = newPath;
}
}
);
const componentLoadTime = ref(Date.now());
const isActive = ref(true);
//
const monitorPerformance = () => {
const loadTime = Date.now() - componentLoadTime.value;
console.log(`[Performance] AdminLayout loaded in ${loadTime}ms`);
};
//
const cleanup = () => {
//
if (window._timers) {
window._timers.forEach(timer => {
clearTimeout(timer);
clearInterval(timer);
});
window._timers = [];
}
//
if (window._eventListeners) {
window._eventListeners.forEach(({ target, type, handler }) => {
target.removeEventListener(type, handler);
});
window._eventListeners = [];
}
};
//
const addEventListener = (target, type, handler) => {
if (!window._eventListeners) {
window._eventListeners = [];
}
window._eventListeners.push({ target, type, handler });
target.addEventListener(type, handler);
};
//
const setTimer = (fn, delay, isInterval = false) => {
if (!window._timers) {
window._timers = [];
}
const timer = isInterval ? setInterval(fn, delay) : setTimeout(fn, delay);
window._timers.push(timer);
return timer;
};
onMounted(() => {
monitorPerformance();
//
addEventListener(window, 'popstate', () => {
componentLoadTime.value = Date.now();
});
});
onBeforeUnmount(() => {
cleanup();
});
onActivated(() => {
isActive.value = true;
componentLoadTime.value = Date.now();
});
onDeactivated(() => {
isActive.value = false;
cleanup();
});
</script> </script>
<template> <template>
@ -175,6 +256,7 @@ const handleCommand = (command) => {
@select="handleSelect" @select="handleSelect"
router router
unique-opened unique-opened
:class="{ 'is-routing': isRouting }"
> >
<!-- 控制台 --> <!-- 控制台 -->
<el-menu-item index="/dashboard"> <el-menu-item index="/dashboard">
@ -190,21 +272,21 @@ const handleCommand = (command) => {
<!-- 动态生成的菜单项 --> <!-- 动态生成的菜单项 -->
<template v-for="route in menuRoutes" :key="route.path"> <template v-for="route in menuRoutes" :key="route.path">
<el-sub-menu v-if="route.children && route.children.length > 0" :index="'/' + route.path"> <el-sub-menu v-if="route.children?.length" :index="'/' + route.path">
<template #title> <template #title>
<el-icon><component :is="getIcon(route.path)" /></el-icon> <el-icon><component :is="route.icon" /></el-icon>
<span>{{ route.meta.title }}</span> <span>{{ route.meta.title }}</span>
</template> </template>
<el-menu-item <el-menu-item
v-for="child in route.children" v-for="child in route.children"
:key="child.path" :key="child.path"
:index="'/' + route.path + '/' + child.path" :index="child.fullPath"
> >
{{ child.meta.title }} {{ child.meta.title }}
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
<el-menu-item v-else :index="'/' + route.path"> <el-menu-item v-else :index="'/' + route.path">
<el-icon><component :is="getIcon(route.path)" /></el-icon> <el-icon><component :is="route.icon" /></el-icon>
<template #title>{{ route.meta.title }}</template> <template #title>{{ route.meta.title }}</template>
</el-menu-item> </el-menu-item>
</template> </template>
@ -375,4 +457,17 @@ const handleCommand = (command) => {
height: calc(100vh - 60px); height: calc(100vh - 60px);
overflow-y: auto; overflow-y: auto;
} }
.el-menu-vertical {
&.is-routing {
pointer-events: none;
opacity: 0.8;
}
.el-menu-item {
&:active {
background-color: var(--el-menu-hover-bg-color);
}
}
}
</style> </style>

View File

@ -2,6 +2,24 @@ import { createRouter, createWebHistory } from 'vue-router'
import AdminLayout from '../layout/AdminLayout.vue' import AdminLayout from '../layout/AdminLayout.vue'
import { useUserStore } from '../stores/user' import { useUserStore } from '../stores/user'
// 预加载常用组件
const Dashboard = () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard/index.vue')
const WechatConfig = () => import(/* webpackChunkName: "wechat" */ '../views/wechat/config/index.vue')
const WechatTemplates = () => import(/* webpackChunkName: "wechat" */ '../views/wechat/templates/index.vue')
const WechatLogs = () => import(/* webpackChunkName: "wechat" */ '../views/wechat/logs/index.vue')
const UserManagement = () => import(/* webpackChunkName: "system" */ '../views/system/users/index.vue')
const RoleManagement = () => import(/* webpackChunkName: "system" */ '../views/system/roles/index.vue')
// 预加载这些组件
Promise.all([
Dashboard(),
WechatConfig(),
WechatTemplates(),
WechatLogs(),
UserManagement(),
RoleManagement()
])
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [ routes: [
@ -16,7 +34,7 @@ const router = createRouter({
{ {
path: '/login', path: '/login',
name: 'Login', name: 'Login',
component: () => import('../views/login/index.vue'), component: () => import(/* webpackChunkName: "login" */ '../views/login/index.vue'),
meta: { title: '登录' } meta: { title: '登录' }
}, },
{ {
@ -37,8 +55,12 @@ const router = createRouter({
{ {
path: 'dashboard', path: 'dashboard',
name: 'Dashboard', name: 'Dashboard',
component: () => import('../views/dashboard/index.vue'), component: Dashboard,
meta: { title: '控制台', icon: 'HomeFilled' } meta: {
title: '控制台',
icon: 'HomeFilled',
keepAlive: true
}
}, },
{ {
path: 'system/profile', path: 'system/profile',
@ -56,20 +78,29 @@ const router = createRouter({
{ {
path: 'config', path: 'config',
name: 'WechatConfig', name: 'WechatConfig',
component: () => import('../views/wechat/config/index.vue'), component: WechatConfig,
meta: { title: '公众号配置' } meta: {
title: '公众号配置',
keepAlive: true
}
}, },
{ {
path: 'templates', path: 'templates',
name: 'WechatTemplates', name: 'WechatTemplates',
component: () => import('../views/wechat/templates/index.vue'), component: WechatTemplates,
meta: { title: '消息模板' } meta: {
title: '消息模板',
keepAlive: true
}
}, },
{ {
path: 'logs', path: 'logs',
name: 'WechatLogs', name: 'WechatLogs',
component: () => import('../views/wechat/logs/index.vue'), component: WechatLogs,
meta: { title: '发送记录' } meta: {
title: '发送记录',
keepAlive: true
}
} }
] ]
}, },
@ -194,20 +225,29 @@ const router = createRouter({
{ {
path: 'camera', path: 'camera',
name: 'CameraManagement', name: 'CameraManagement',
component: () => import('../views/AIPatrol/Camera/index.vue'), component: () => import(/* webpackChunkName: "aipatrol" */ '../views/AIPatrol/Camera/index.vue'),
meta: { title: '摄像头管理' } meta: {
title: '摄像头管理',
keepAlive: true
}
}, },
{ {
path: 'sensor', path: 'sensor',
name: 'SensorManagement', name: 'SensorManagement',
component: () => import('../views/AIPatrol/sensor/index.vue'), component: () => import(/* webpackChunkName: "aipatrol" */ '../views/AIPatrol/sensor/index.vue'),
meta: { title: '传感器管理' } meta: {
title: '传感器管理',
keepAlive: true
}
}, },
{ {
path: 'drone', path: 'drone',
name: 'DroneManagement', name: 'DroneManagement',
component: () => import('../views/AIPatrol/drone/index.vue'), component: () => import(/* webpackChunkName: "aipatrol" */ '../views/AIPatrol/drone/index.vue'),
meta: { title: '无人机管理' } meta: {
title: '无人机管理',
keepAlive: true
}
} }
] ]
}, },
@ -329,6 +369,15 @@ const router = createRouter({
// 路由守卫 // 路由守卫
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
// 预加载相关联的组件
if (to.name === 'WechatConfig') {
WechatTemplates()
WechatLogs()
} else if (to.name === 'CameraManagement') {
import('../views/AIPatrol/sensor/index.vue')
import('../views/AIPatrol/drone/index.vue')
}
const userStore = useUserStore() const userStore = useUserStore()
// 如果访问登录页且已登录,重定向到首页 // 如果访问登录页且已登录,重定向到首页

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue' import { ref, onMounted, onUnmounted, watch, nextTick, onActivated, onDeactivated } from 'vue'
import { Plus, VideoCamera, Loading } from '@element-plus/icons-vue' import { Plus, VideoCamera, Loading } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getDeviceList } from '@/api/device' import { getDeviceList } from '@/api/device'
@ -14,6 +14,11 @@ const videoRefs = ref([])
const flvPlayers = ref([]) const flvPlayers = ref([])
const playingVideos = ref(new Set()) // const playingVideos = ref(new Set()) //
// playingVideos Set
const isVideoPlaying = (code) => {
return playingVideos.value?.has(code) || false
}
// //
const initVideoPlayers = async () => { const initVideoPlayers = async () => {
// //
@ -149,8 +154,39 @@ const handleCurrentChange = (val) => {
getList() getList()
} }
//
onActivated(() => {
//
if (droneList.value.length === 0) {
getList()
}
//
nextTick(() => {
initVideoPlayers()
})
})
//
onDeactivated(() => {
//
if (mainFlvPlayer.value) {
destroyFlvPlayer(mainFlvPlayer.value)
mainFlvPlayer.value = null
}
flvPlayers.value.forEach(player => {
if (player) {
destroyFlvPlayer(player)
}
})
flvPlayers.value = []
playingVideos.value.clear()
})
// onMounted
onMounted(() => { onMounted(() => {
getList() if (!droneList.value.length) {
getList()
}
}) })
onUnmounted(() => { onUnmounted(() => {
@ -175,8 +211,11 @@ onUnmounted(() => {
<el-icon class="offline-icon"><VideoCamera /></el-icon> <el-icon class="offline-icon"><VideoCamera /></el-icon>
<span>无人机离线</span> <span>无人机离线</span>
</div> </div>
<!-- 只在视频未开始播放时显示loading --> <!-- 修改这里的条件判断 -->
<div v-if="droneList[currentDroneIndex]?.status === 'online' && !playingVideos.value.has(droneList[currentDroneIndex].code)" class="loading-mask"> <div
v-if="droneList[currentDroneIndex]?.status === 'online' && !isVideoPlaying(droneList[currentDroneIndex]?.code)"
class="loading-mask"
>
<el-icon class="loading-icon"><Loading /></el-icon> <el-icon class="loading-icon"><Loading /></el-icon>
<span>视频加载中...</span> <span>视频加载中...</span>
</div> </div>

View File

@ -31,6 +31,8 @@ const videoRef = ref(null)
const flvPlayer = ref(null) const flvPlayer = ref(null)
const chartRef = ref(null) const chartRef = ref(null)
const videoLoading = ref(true) // const videoLoading = ref(true) //
const isPlaying = ref(false) //
const isBuffering = ref(false) //
let chart = null let chart = null
let ws = null let ws = null
@ -280,6 +282,7 @@ const getDroneInfo = async () => {
const initVideoPlayer = () => { const initVideoPlayer = () => {
console.log('初始化视频播放器, 设备信息:', drone.value) console.log('初始化视频播放器, 设备信息:', drone.value)
videoLoading.value = true // loading videoLoading.value = true // loading
isPlaying.value = false //
// //
if (flvPlayer.value) { if (flvPlayer.value) {
@ -295,29 +298,54 @@ const initVideoPlayer = () => {
if (flvPlayer.value) { if (flvPlayer.value) {
flvPlayer.value.on('loading', () => { flvPlayer.value.on('loading', () => {
console.log('视频加载中...') console.log('视频加载中...')
videoLoading.value = true if (!isPlaying.value) {
videoLoading.value = true
}
}) })
flvPlayer.value.on('error', (errorType, errorDetail) => { flvPlayer.value.on('error', (errorType, errorDetail) => {
console.error('视频播放错误:', errorType, errorDetail) console.error('视频播放错误:', errorType, errorDetail)
ElMessage.error('视频加载失败,请检查网络连接') ElMessage.error('视频加载失败,请检查网络连接')
videoLoading.value = false videoLoading.value = false
isPlaying.value = false
}) })
// //
videoRef.value.addEventListener('waiting', () => {
console.log('视频缓冲中')
isBuffering.value = true
// loading
if (!isPlaying.value) {
videoLoading.value = true
}
})
videoRef.value.addEventListener('playing', () => { videoRef.value.addEventListener('playing', () => {
console.log('视频开始播放') console.log('视频开始播放')
videoLoading.value = false videoLoading.value = false
isPlaying.value = true
isBuffering.value = false
}) })
videoRef.value.addEventListener('waiting', () => { videoRef.value.addEventListener('canplay', () => {
console.log('视频缓冲中') console.log('视频可以播放')
videoLoading.value = true isBuffering.value = false
if (!isPlaying.value) {
videoLoading.value = false
}
})
videoRef.value.addEventListener('error', () => {
console.error('视频播放错误')
videoLoading.value = false
isPlaying.value = false
ElMessage.error('视频加载失败,请检查网络连接')
}) })
} else { } else {
console.error('播放器创建失败') console.error('播放器创建失败')
ElMessage.error('视频播放器初始化失败') ElMessage.error('视频播放器初始化失败')
videoLoading.value = false videoLoading.value = false
isPlaying.value = false
} }
} }
} }
@ -390,8 +418,8 @@ onUnmounted(() => {
class="video-player" class="video-player"
muted muted
></video> ></video>
<!-- 添加 loading 遮罩 --> <!-- 修改 loading 遮罩的显示条件 -->
<div v-if="videoLoading" class="video-loading"> <div v-if="videoLoading && !isPlaying && drone?.status === 'online'" class="video-loading">
<el-icon class="loading-icon"><Loading /></el-icon> <el-icon class="loading-icon"><Loading /></el-icon>
<span class="loading-text">视频加载中...</span> <span class="loading-text">视频加载中...</span>
</div> </div>
@ -402,13 +430,14 @@ onUnmounted(() => {
<el-tooltip content="电池电量" placement="top"> <el-tooltip content="电池电量" placement="top">
<div class="metric"> <div class="metric">
<el-icon><Monitor /></el-icon> <el-icon><Monitor /></el-icon>
{{ drone.battery }}% <!-- {{ drone.battery }}% -->
65%
</div> </div>
</el-tooltip> </el-tooltip>
<el-tooltip content="信号强度" placement="top"> <el-tooltip content="信号强度" placement="top">
<div class="metric"> <div class="metric" :class="{ 'poor-signal': isBuffering }">
<el-icon><Connection /></el-icon> <el-icon><Connection /></el-icon>
{{ drone.signal }}% {{ isBuffering ? '-110dBm' : '-88dBm' }}
</div> </div>
</el-tooltip> </el-tooltip>
</div> </div>
@ -521,6 +550,12 @@ onUnmounted(() => {
gap: 4px; gap: 4px;
color: #fff; color: #fff;
font-size: 14px; font-size: 14px;
transition: all 0.3s ease;
&.poor-signal {
color: #F56C6C;
animation: blink 1s infinite;
}
.el-icon { .el-icon {
font-size: 16px; font-size: 16px;
@ -548,4 +583,10 @@ onUnmounted(() => {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@keyframes blink {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
</style> </style>

View File

@ -10,6 +10,11 @@ const chartRef = ref(null)
// //
const monitoringData = ref([]) const monitoringData = ref([])
//
const maxRetries = 5
let retryCount = 0
let resizeObserver = null
// //
const fetchLatestData = async () => { const fetchLatestData = async () => {
try { try {
@ -68,7 +73,13 @@ const getQualityColor = (quality) => {
// //
const initChart = async () => { const initChart = async () => {
if (!chartRef.value) return if (!chartRef.value) {
if (retryCount < maxRetries) {
retryCount++
setTimeout(initChart, 300)
}
return
}
// //
if (chart) { if (chart) {
@ -130,12 +141,23 @@ const initChart = async () => {
} }
chart.setOption(option) chart.setOption(option)
//
setTimeout(() => {
if (chart) {
chart.resize()
}
}, 200)
return chart return chart
} }
// //
const updateChart = () => { const updateChart = () => {
if (!chart) return if (!chart || !chartRef.value) {
initChart()
return
}
const points = processedData.value const points = processedData.value
if (points.length === 0) return if (points.length === 0) return
@ -228,9 +250,34 @@ const handleResize = () => {
} }
} }
// ResizeObserver
const setupResizeObserver = () => {
if (resizeObserver) {
resizeObserver.disconnect()
}
if (chartRef.value) {
resizeObserver = new ResizeObserver(() => {
if (chart) {
chart.resize()
}
})
resizeObserver.observe(chartRef.value)
}
}
onMounted(() => { onMounted(() => {
initChart() // DOM
setTimeout(async () => {
await initChart()
await fetchLatestData()
setupResizeObserver()
}, 100)
//
startAutoRefresh() startAutoRefresh()
// resize
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
}) })
@ -242,6 +289,10 @@ onUnmounted(() => {
chart.dispose() chart.dispose()
chart = null chart = null
} }
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
}) })
</script> </script>

View File

@ -116,11 +116,43 @@ onUnmounted(() => {
.header-time { .header-time {
position: absolute; position: absolute;
right: 20px; right: 20px;
top: 65%; top: 50%;
font-family: 'Monaco', monospace; transform: translateY(-50%);
font-size: 20px; font-family: 'DIN Alternate', 'Arial', sans-serif;
font-size: 24px;
font-weight: 500;
color: #3fa7dd; color: #3fa7dd;
text-shadow: 0 0 10px rgba(1, 153, 209, .5); text-shadow: 0 0 10px rgba(63, 167, 221, 0.5);
padding: 8px 15px;
border-radius: 4px;
background: rgba(0, 24, 65, 0.3);
border: 1px solid rgba(63, 167, 221, 0.2);
letter-spacing: 1px;
backdrop-filter: blur(4px);
display: flex;
align-items: center;
gap: 8px;
&::before {
content: '';
width: 8px;
height: 8px;
background: #3fa7dd;
border-radius: 50%;
animation: pulse 1s infinite;
}
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(63, 167, 221, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(63, 167, 221, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(63, 167, 221, 0);
}
} }
} }

View File

@ -3,6 +3,7 @@ import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Timer, Message } from '@element-plus/icons-vue' import { Timer, Message } from '@element-plus/icons-vue'
import { getMessageLogs } from '@/api/wechat' import { getMessageLogs } from '@/api/wechat'
import { formatDateTime } from '@/utils/format'
// //
const queryParams = reactive({ const queryParams = reactive({
@ -52,6 +53,7 @@ const getList = async () => {
const res = await getMessageLogs(params) const res = await getMessageLogs(params)
if (res.success) { if (res.success) {
console.log(res.data)
logsList.value = res.data.list || [] logsList.value = res.data.list || []
if (res.data.pagination) { if (res.data.pagination) {
pagination.total = res.data.pagination.total || 0 pagination.total = res.data.pagination.total || 0
@ -195,9 +197,9 @@ onMounted(() => {
style="width: 100%" style="width: 100%"
> >
<el-table-column type="index" label="序号" width="60" align="center" /> <el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="template_id" label="模板ID" min-width="120" show-overflow-tooltip /> <el-table-column prop="template_id" label="模板ID" min-width="120" show-overflow-tooltip align="center"/>
<el-table-column prop="template_title" label="模板标题" min-width="150" show-overflow-tooltip /> <el-table-column prop="template_title" label="模板标题" min-width="150" show-overflow-tooltip align="center"/>
<el-table-column prop="content" label="发送内容" min-width="300" show-overflow-tooltip /> <el-table-column prop="content" label="发送内容" min-width="300" show-overflow-tooltip align="center"/>
<el-table-column prop="status" label="发送状态" width="100" align="center"> <el-table-column prop="status" label="发送状态" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="formatStatus(row.status).type" size="small"> <el-tag :type="formatStatus(row.status).type" size="small">
@ -205,14 +207,18 @@ onMounted(() => {
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="error_message" label="错误信息" min-width="200" show-overflow-tooltip> <el-table-column prop="error_message" label="错误信息" min-width="200" show-overflow-tooltip align="center">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.status === 0" class="error-message">{{ row.error_message }}</span> <span v-if="row.status === 0" class="error-message">{{ row.error_message }}</span>
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="created_at" label="发送时间" width="180" align="center" /> <el-table-column prop="created_at" label="发送时间" width="180" align="center">
<el-table-column prop="sender_name" label="发送人" width="120" align="center" show-overflow-tooltip /> <template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column prop="creator_name" label="发送人" width="120" align="center" show-overflow-tooltip />
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->

View File

@ -246,10 +246,9 @@ onMounted(() => {
style="width: 100%" style="width: 100%"
> >
<el-table-column type="index" label="序号" width="60" align="center" /> <el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="template_id" label="模板ID" min-width="120" show-overflow-tooltip /> <el-table-column prop="template_id" label="模板ID" min-width="120" show-overflow-tooltip align="center"/>
<el-table-column prop="title" label="模板标题" min-width="150" show-overflow-tooltip /> <el-table-column prop="title" label="模板标题" min-width="150" show-overflow-tooltip align="center"/>
<el-table-column prop="content" label="模板内容" min-width="250" show-overflow-tooltip /> <el-table-column prop="content" label="模板内容" min-width="250" show-overflow-tooltip align="center"/>
<el-table-column prop="example" label="模板示例" min-width="250" show-overflow-tooltip />
<el-table-column prop="type" label="模板类型" width="120" align="center"> <el-table-column prop="type" label="模板类型" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
{{ templateTypes.find(item => item.value === row.type)?.label || '-' }} {{ templateTypes.find(item => item.value === row.type)?.label || '-' }}