619 lines
21 KiB
Vue
619 lines
21 KiB
Vue
<template>
|
|
<div>
|
|
<LayoutsBreadcrumb />
|
|
|
|
<!-- Header Section -->
|
|
<rs-card class="mb-6">
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<Icon class="mr-2 text-primary" name="ic:outline-speed"></Icon>
|
|
<h1 class="text-xl font-bold text-primary">Performance Monitoring</h1>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center">
|
|
<div class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></div>
|
|
<span class="text-sm text-gray-600">Real-time Metrics</span>
|
|
</div>
|
|
<rs-button variant="outline" size="sm" @click="refreshMetrics">
|
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
|
Refresh
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<p class="text-gray-600">
|
|
Monitor system performance metrics including throughput, uptime, and scaling status to ensure
|
|
the system meets the 10,000 notifications per minute requirement with 99.9% uptime.
|
|
</p>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Key Performance Indicators -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
|
<rs-card
|
|
v-for="(kpi, index) in keyMetrics"
|
|
:key="index"
|
|
class="transition-all duration-300 hover:shadow-lg"
|
|
:class="kpi.status === 'critical' ? 'border-red-500' : kpi.status === 'warning' ? 'border-yellow-500' : 'border-green-500'"
|
|
>
|
|
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
|
|
<div
|
|
class="p-4 flex justify-center items-center rounded-2xl"
|
|
:class="kpi.bgColor"
|
|
>
|
|
<Icon class="text-2xl" :class="kpi.iconColor" :name="kpi.icon"></Icon>
|
|
</div>
|
|
<div class="flex-1 truncate">
|
|
<span class="block font-bold text-2xl leading-tight" :class="kpi.valueColor">
|
|
{{ kpi.value }}
|
|
</span>
|
|
<span class="text-sm font-medium text-gray-600">
|
|
{{ kpi.title }}
|
|
</span>
|
|
<div class="flex items-center mt-1">
|
|
<Icon
|
|
class="text-xs mr-1"
|
|
:class="kpi.trend === 'up' ? 'text-green-500' : kpi.trend === 'down' ? 'text-red-500' : 'text-gray-500'"
|
|
:name="kpi.trend === 'up' ? 'ic:outline-trending-up' : kpi.trend === 'down' ? 'ic:outline-trending-down' : 'ic:outline-trending-flat'"
|
|
></Icon>
|
|
<span class="text-xs" :class="kpi.trend === 'up' ? 'text-green-500' : kpi.trend === 'down' ? 'text-red-500' : 'text-gray-500'">
|
|
{{ kpi.change }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</rs-card>
|
|
</div>
|
|
|
|
<!-- Performance Charts -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
<!-- Throughput Chart -->
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<Icon class="mr-2 text-primary" name="ic:outline-show-chart"></Icon>
|
|
<h3 class="text-lg font-semibold text-primary">Throughput (Last 24 Hours)</h3>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm text-gray-600">Target: 10,000/min</span>
|
|
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
|
<div class="text-center">
|
|
<Icon class="text-4xl text-gray-400 mb-2" name="ic:outline-show-chart"></Icon>
|
|
<p class="text-gray-600">Throughput Chart</p>
|
|
<p class="text-sm text-gray-500">Real-time notifications per minute</p>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 grid grid-cols-3 gap-4 text-center">
|
|
<div>
|
|
<p class="text-sm text-gray-600">Current</p>
|
|
<p class="font-bold text-lg text-green-600">{{ currentThroughput }}/min</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-600">Peak Today</p>
|
|
<p class="font-bold text-lg text-blue-600">{{ peakThroughput }}/min</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-600">Average</p>
|
|
<p class="font-bold text-lg text-purple-600">{{ avgThroughput }}/min</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- System Uptime -->
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<Icon class="mr-2 text-primary" name="ic:outline-timeline"></Icon>
|
|
<h3 class="text-lg font-semibold text-primary">System Uptime</h3>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm text-gray-600">Target: 99.9%</span>
|
|
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-4">
|
|
<!-- Current Uptime -->
|
|
<div class="text-center p-6 bg-green-50 rounded-lg">
|
|
<div class="text-4xl font-bold text-green-600 mb-2">{{ currentUptime }}%</div>
|
|
<p class="text-green-700 font-medium">Current Uptime</p>
|
|
<p class="text-sm text-green-600 mt-1">{{ uptimeDuration }} continuous operation</p>
|
|
</div>
|
|
|
|
<!-- Uptime History -->
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="text-center">
|
|
<p class="text-sm text-gray-600">Last 24h</p>
|
|
<p class="font-bold text-lg">{{ uptime24h }}%</p>
|
|
</div>
|
|
<div class="text-center">
|
|
<p class="text-sm text-gray-600">Last 7 days</p>
|
|
<p class="font-bold text-lg">{{ uptime7d }}%</p>
|
|
</div>
|
|
<div class="text-center">
|
|
<p class="text-sm text-gray-600">Last 30 days</p>
|
|
<p class="font-bold text-lg">{{ uptime30d }}%</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Incidents -->
|
|
<div class="border-t pt-4">
|
|
<h4 class="font-medium text-gray-700 mb-2">Recent Incidents</h4>
|
|
<div class="space-y-2">
|
|
<div
|
|
v-for="(incident, index) in recentIncidents"
|
|
:key="index"
|
|
class="flex items-center justify-between p-2 bg-gray-50 rounded"
|
|
>
|
|
<div class="flex items-center">
|
|
<div class="w-2 h-2 bg-red-500 rounded-full mr-2"></div>
|
|
<span class="text-sm">{{ incident.description }}</span>
|
|
</div>
|
|
<span class="text-xs text-gray-500">{{ incident.duration }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
|
|
<!-- Worker Scaling Status -->
|
|
<rs-card class="mb-6">
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<Icon class="mr-2 text-primary" name="ic:outline-dns"></Icon>
|
|
<h3 class="text-lg font-semibold text-primary">Horizontal Scaling Status</h3>
|
|
</div>
|
|
<rs-button variant="outline" size="sm" @click="showScalingModal = true">
|
|
<Icon class="mr-1" name="ic:outline-settings"></Icon>
|
|
Configure
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Active Workers -->
|
|
<div class="text-center p-4 bg-blue-50 rounded-lg">
|
|
<Icon class="text-3xl text-blue-600 mb-2" name="ic:outline-memory"></Icon>
|
|
<div class="text-2xl font-bold text-blue-600">{{ activeWorkers }}</div>
|
|
<p class="text-blue-700 font-medium">Active Workers</p>
|
|
<p class="text-sm text-blue-600 mt-1">Processing notifications</p>
|
|
</div>
|
|
|
|
<!-- Auto-scaling Status -->
|
|
<div class="text-center p-4 bg-green-50 rounded-lg">
|
|
<Icon class="text-3xl text-green-600 mb-2" name="ic:outline-auto-awesome"></Icon>
|
|
<div class="text-lg font-bold text-green-600">{{ autoScalingEnabled ? 'Enabled' : 'Disabled' }}</div>
|
|
<p class="text-green-700 font-medium">Auto-scaling</p>
|
|
<p class="text-sm text-green-600 mt-1">{{ autoScalingEnabled ? 'Automatically adjusting' : 'Manual scaling only' }}</p>
|
|
</div>
|
|
|
|
<!-- Queue Load -->
|
|
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
|
<Icon class="text-3xl text-purple-600 mb-2" name="ic:outline-queue"></Icon>
|
|
<div class="text-2xl font-bold text-purple-600">{{ queueLoad }}%</div>
|
|
<p class="text-purple-700 font-medium">Queue Load</p>
|
|
<p class="text-sm text-purple-600 mt-1">Current capacity usage</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Worker Details -->
|
|
<div class="mt-6">
|
|
<h4 class="font-medium text-gray-700 mb-4">Worker Instances</h4>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Worker ID
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
CPU Usage
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Memory Usage
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Jobs Processed
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Last Heartbeat
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr v-for="(worker, index) in workers" :key="index">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
{{ worker.id }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span
|
|
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
|
:class="{
|
|
'bg-green-100 text-green-800': worker.status === 'healthy',
|
|
'bg-yellow-100 text-yellow-800': worker.status === 'warning',
|
|
'bg-red-100 text-red-800': worker.status === 'error'
|
|
}"
|
|
>
|
|
{{ worker.status }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{{ worker.cpuUsage }}%
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{{ worker.memoryUsage }}%
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{{ worker.jobsProcessed.toLocaleString() }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{{ worker.lastHeartbeat }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Performance Alerts -->
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<Icon class="mr-2 text-primary" name="ic:outline-warning"></Icon>
|
|
<h3 class="text-lg font-semibold text-primary">Performance Alerts</h3>
|
|
</div>
|
|
<rs-button variant="outline" size="sm" @click="showAlertsConfig = true">
|
|
<Icon class="mr-1" name="ic:outline-settings"></Icon>
|
|
Configure Alerts
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-3">
|
|
<div
|
|
v-for="(alert, index) in performanceAlerts"
|
|
:key="index"
|
|
class="flex items-center justify-between p-4 rounded-lg"
|
|
:class="{
|
|
'bg-red-50 border border-red-200': alert.severity === 'critical',
|
|
'bg-yellow-50 border border-yellow-200': alert.severity === 'warning',
|
|
'bg-blue-50 border border-blue-200': alert.severity === 'info'
|
|
}"
|
|
>
|
|
<div class="flex items-center">
|
|
<Icon
|
|
class="mr-3 text-xl"
|
|
:class="{
|
|
'text-red-500': alert.severity === 'critical',
|
|
'text-yellow-500': alert.severity === 'warning',
|
|
'text-blue-500': alert.severity === 'info'
|
|
}"
|
|
:name="alert.icon"
|
|
></Icon>
|
|
<div>
|
|
<p class="font-medium" :class="{
|
|
'text-red-800': alert.severity === 'critical',
|
|
'text-yellow-800': alert.severity === 'warning',
|
|
'text-blue-800': alert.severity === 'info'
|
|
}">
|
|
{{ alert.title }}
|
|
</p>
|
|
<p class="text-sm" :class="{
|
|
'text-red-600': alert.severity === 'critical',
|
|
'text-yellow-600': alert.severity === 'warning',
|
|
'text-blue-600': alert.severity === 'info'
|
|
}">
|
|
{{ alert.description }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-xs text-gray-500">{{ alert.timestamp }}</p>
|
|
<rs-button
|
|
size="sm"
|
|
variant="outline"
|
|
@click="acknowledgeAlert(index)"
|
|
>
|
|
Acknowledge
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Scaling Configuration Modal -->
|
|
<rs-modal v-model="showScalingModal" title="Horizontal Scaling Configuration">
|
|
<div class="space-y-6">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Auto-scaling</label>
|
|
<div class="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
v-model="scalingConfig.autoScalingEnabled"
|
|
class="mr-2"
|
|
>
|
|
<span class="text-sm text-gray-600">Enable automatic worker scaling</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Min Workers</label>
|
|
<input
|
|
type="number"
|
|
v-model="scalingConfig.minWorkers"
|
|
class="w-full p-2 border border-gray-300 rounded-md"
|
|
min="1"
|
|
>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Max Workers</label>
|
|
<input
|
|
type="number"
|
|
v-model="scalingConfig.maxWorkers"
|
|
class="w-full p-2 border border-gray-300 rounded-md"
|
|
min="1"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Scale Up Threshold (%)</label>
|
|
<input
|
|
type="number"
|
|
v-model="scalingConfig.scaleUpThreshold"
|
|
class="w-full p-2 border border-gray-300 rounded-md"
|
|
min="1"
|
|
max="100"
|
|
>
|
|
<p class="text-xs text-gray-500 mt-1">Scale up when queue load exceeds this percentage</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Scale Down Threshold (%)</label>
|
|
<input
|
|
type="number"
|
|
v-model="scalingConfig.scaleDownThreshold"
|
|
class="w-full p-2 border border-gray-300 rounded-md"
|
|
min="1"
|
|
max="100"
|
|
>
|
|
<p class="text-xs text-gray-500 mt-1">Scale down when queue load falls below this percentage</p>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-end gap-3">
|
|
<rs-button variant="outline" @click="showScalingModal = false">
|
|
Cancel
|
|
</rs-button>
|
|
<rs-button @click="saveScalingConfig">
|
|
Save Configuration
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
</rs-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
definePageMeta({
|
|
title: "Performance Monitoring",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{
|
|
name: "Notification",
|
|
path: "/notification",
|
|
},
|
|
{
|
|
name: "Queue & Scheduler",
|
|
path: "/notification/queue-scheduler",
|
|
},
|
|
{
|
|
name: "Performance",
|
|
path: "/notification/queue-scheduler/performance",
|
|
},
|
|
],
|
|
});
|
|
|
|
// Reactive data
|
|
const showScalingModal = ref(false);
|
|
const showAlertsConfig = ref(false);
|
|
|
|
// Key Performance Metrics
|
|
const keyMetrics = ref([
|
|
{
|
|
title: "Throughput",
|
|
value: "12,847/min",
|
|
icon: "ic:outline-speed",
|
|
bgColor: "bg-green-100",
|
|
iconColor: "text-green-600",
|
|
valueColor: "text-green-600",
|
|
status: "healthy",
|
|
trend: "up",
|
|
change: "+15.3%"
|
|
},
|
|
{
|
|
title: "System Uptime",
|
|
value: "99.97%",
|
|
icon: "ic:outline-timeline",
|
|
bgColor: "bg-blue-100",
|
|
iconColor: "text-blue-600",
|
|
valueColor: "text-blue-600",
|
|
status: "healthy",
|
|
trend: "stable",
|
|
change: "0.02%"
|
|
},
|
|
{
|
|
title: "Active Workers",
|
|
value: "8",
|
|
icon: "ic:outline-memory",
|
|
bgColor: "bg-purple-100",
|
|
iconColor: "text-purple-600",
|
|
valueColor: "text-purple-600",
|
|
status: "healthy",
|
|
trend: "up",
|
|
change: "+2"
|
|
},
|
|
{
|
|
title: "Queue Load",
|
|
value: "67%",
|
|
icon: "ic:outline-queue",
|
|
bgColor: "bg-yellow-100",
|
|
iconColor: "text-yellow-600",
|
|
valueColor: "text-yellow-600",
|
|
status: "warning",
|
|
trend: "up",
|
|
change: "+12%"
|
|
}
|
|
]);
|
|
|
|
// Throughput metrics
|
|
const currentThroughput = ref("12,847");
|
|
const peakThroughput = ref("15,234");
|
|
const avgThroughput = ref("9,876");
|
|
|
|
// Uptime metrics
|
|
const currentUptime = ref("99.97");
|
|
const uptimeDuration = ref("47 days, 12 hours");
|
|
const uptime24h = ref("100.00");
|
|
const uptime7d = ref("99.95");
|
|
const uptime30d = ref("99.92");
|
|
|
|
// Recent incidents
|
|
const recentIncidents = ref([
|
|
{
|
|
description: "Database connection timeout",
|
|
duration: "2m 15s",
|
|
timestamp: "2 days ago"
|
|
},
|
|
{
|
|
description: "Worker node restart",
|
|
duration: "45s",
|
|
timestamp: "5 days ago"
|
|
}
|
|
]);
|
|
|
|
// Scaling configuration
|
|
const activeWorkers = ref(8);
|
|
const autoScalingEnabled = ref(true);
|
|
const queueLoad = ref(67);
|
|
|
|
const scalingConfig = ref({
|
|
autoScalingEnabled: true,
|
|
minWorkers: 2,
|
|
maxWorkers: 20,
|
|
scaleUpThreshold: 80,
|
|
scaleDownThreshold: 30
|
|
});
|
|
|
|
// Worker instances
|
|
const workers = ref([
|
|
{
|
|
id: "worker-001",
|
|
status: "healthy",
|
|
cpuUsage: 45,
|
|
memoryUsage: 62,
|
|
jobsProcessed: 15847,
|
|
lastHeartbeat: "2 seconds ago"
|
|
},
|
|
{
|
|
id: "worker-002",
|
|
status: "healthy",
|
|
cpuUsage: 52,
|
|
memoryUsage: 58,
|
|
jobsProcessed: 14923,
|
|
lastHeartbeat: "1 second ago"
|
|
},
|
|
{
|
|
id: "worker-003",
|
|
status: "warning",
|
|
cpuUsage: 78,
|
|
memoryUsage: 85,
|
|
jobsProcessed: 12456,
|
|
lastHeartbeat: "5 seconds ago"
|
|
},
|
|
{
|
|
id: "worker-004",
|
|
status: "healthy",
|
|
cpuUsage: 41,
|
|
memoryUsage: 55,
|
|
jobsProcessed: 16234,
|
|
lastHeartbeat: "1 second ago"
|
|
}
|
|
]);
|
|
|
|
// Performance alerts
|
|
const performanceAlerts = ref([
|
|
{
|
|
title: "High Queue Load",
|
|
description: "Queue load has exceeded 80% for the last 10 minutes",
|
|
severity: "warning",
|
|
icon: "ic:outline-warning",
|
|
timestamp: "5 minutes ago"
|
|
},
|
|
{
|
|
title: "Worker High CPU Usage",
|
|
description: "Worker-003 CPU usage is at 78%",
|
|
severity: "warning",
|
|
icon: "ic:outline-memory",
|
|
timestamp: "2 minutes ago"
|
|
},
|
|
{
|
|
title: "Throughput Target Met",
|
|
description: "Successfully processing 12,847 notifications per minute",
|
|
severity: "info",
|
|
icon: "ic:outline-check-circle",
|
|
timestamp: "1 minute ago"
|
|
}
|
|
]);
|
|
|
|
// Methods
|
|
const refreshMetrics = () => {
|
|
console.log('Refreshing performance metrics...');
|
|
// Simulate data refresh
|
|
};
|
|
|
|
const acknowledgeAlert = (index) => {
|
|
performanceAlerts.value.splice(index, 1);
|
|
};
|
|
|
|
const saveScalingConfig = () => {
|
|
console.log('Saving scaling configuration:', scalingConfig.value);
|
|
showScalingModal.value = false;
|
|
// Here you would typically send the config to your backend
|
|
};
|
|
|
|
// Auto-refresh metrics every 30 seconds
|
|
onMounted(() => {
|
|
const interval = setInterval(refreshMetrics, 30000);
|
|
|
|
onUnmounted(() => {
|
|
clearInterval(interval);
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped></style> |