fix: dashbaord page style
This commit is contained in:
@@ -203,6 +203,16 @@ input[type="number"]:hover::-webkit-outer-spin-button {
|
|||||||
scrollbar-color: oklch(0.5 0 0 / 40%) transparent;
|
scrollbar-color: oklch(0.5 0 0 / 40%) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar but keep functionality */
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ApexCharts tooltip fix */
|
/* ApexCharts tooltip fix */
|
||||||
.apexcharts-tooltip {
|
.apexcharts-tooltip {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
|||||||
@@ -224,16 +224,16 @@ onUnmounted(() => {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="agents" class="mt-4">
|
<TabsContent value="agents" class="mt-4">
|
||||||
<div class="rounded-lg border bg-card overflow-x-auto">
|
<div class="rounded-lg border bg-card overflow-x-auto hide-scrollbar">
|
||||||
<div class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium min-w-[360px]">
|
<div class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium min-w-[360px]">
|
||||||
<span class="w-10 sm:w-12 shrink-0">ID</span>
|
<span class="w-10 sm:w-12 shrink-0">ID</span>
|
||||||
<span class="w-6 shrink-0"></span>
|
<span class="w-6 shrink-0"></span>
|
||||||
<span class="flex-1 min-w-[80px] sm:w-32 sm:shrink-0 sm:flex-none">名称</span>
|
<span class="w-24 sm:w-32 shrink-0">名称</span>
|
||||||
<span class="w-24 sm:w-28 shrink-0 hidden sm:block">IP</span>
|
<span class="w-24 sm:w-28 shrink-0 hidden sm:block">IP</span>
|
||||||
<span class="w-20 sm:w-32 shrink-0 hidden md:block">主机名</span>
|
<span class="w-20 sm:w-32 shrink-0 hidden md:block">主机名</span>
|
||||||
<span class="w-20 sm:w-36 shrink-0 hidden lg:block">版本</span>
|
<span class="w-20 sm:w-36 shrink-0 hidden lg:block">版本</span>
|
||||||
<span class="w-40 shrink-0 hidden xl:block">心跳时间</span>
|
<span class="w-40 shrink-0 hidden xl:block">心跳时间</span>
|
||||||
<span class="flex-1 min-w-[140px] hidden xl:block">创建时间</span>
|
<span class="w-40 shrink-0 hidden xl:block">创建时间</span>
|
||||||
<span class="w-20 sm:w-36 shrink-0">操作</span>
|
<span class="w-20 sm:w-36 shrink-0">操作</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="divide-y min-w-[360px]">
|
<div class="divide-y min-w-[360px]">
|
||||||
@@ -249,12 +249,12 @@ onUnmounted(() => {
|
|||||||
<span :class="isOnline(agent) ? 'bg-green-500' : 'bg-gray-400'" class="relative inline-flex rounded-full h-2.5 w-2.5"></span>
|
<span :class="isOnline(agent) ? 'bg-green-500' : 'bg-gray-400'" class="relative inline-flex rounded-full h-2.5 w-2.5"></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="flex-1 min-w-[80px] sm:w-32 sm:shrink-0 sm:flex-none font-medium text-xs sm:text-sm truncate cursor-pointer hover:text-primary" @click="viewDetail(agent)" :title="agent.name">{{ agent.name }}</span>
|
<span class="w-24 sm:w-32 shrink-0 font-medium text-xs sm:text-sm truncate cursor-pointer hover:text-primary" @click="viewDetail(agent)" :title="agent.name">{{ agent.name }}</span>
|
||||||
<span class="w-24 sm:w-28 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden sm:block">{{ agent.ip || '-' }}</span>
|
<span class="w-24 sm:w-28 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden sm:block">{{ agent.ip || '-' }}</span>
|
||||||
<span class="w-20 sm:w-32 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden md:block">{{ agent.hostname || '-' }}</span>
|
<span class="w-20 sm:w-32 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden md:block">{{ agent.hostname || '-' }}</span>
|
||||||
<span class="w-20 sm:w-36 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden lg:block">{{ agent.version || '-' }}</span>
|
<span class="w-20 sm:w-36 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden lg:block">{{ agent.version || '-' }}</span>
|
||||||
<span class="w-40 shrink-0 text-xs sm:text-sm text-muted-foreground hidden xl:block">{{ agent.last_seen || '-' }}</span>
|
<span class="w-40 shrink-0 text-xs sm:text-sm text-muted-foreground hidden xl:block">{{ agent.last_seen || '-' }}</span>
|
||||||
<span class="flex-1 min-w-[140px] text-xs sm:text-sm text-muted-foreground hidden xl:block">{{ agent.created_at || '-' }}</span>
|
<span class="w-40 shrink-0 text-xs sm:text-sm text-muted-foreground hidden xl:block">{{ agent.created_at || '-' }}</span>
|
||||||
<span class="w-20 sm:w-36 shrink-0 flex justify-end gap-0.5 sm:gap-1">
|
<span class="w-20 sm:w-36 shrink-0 flex justify-end gap-0.5 sm:gap-1">
|
||||||
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="toggleEnabled(agent)" :title="agent.enabled ? '禁用' : '启用'">
|
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="toggleEnabled(agent)" :title="agent.enabled ? '禁用' : '启用'">
|
||||||
<Power v-if="agent.enabled" class="h-3 w-3 sm:h-3.5 sm:w-3.5 text-green-600" />
|
<Power v-if="agent.enabled" class="h-3 w-3 sm:h-3.5 sm:w-3.5 text-green-600" />
|
||||||
@@ -282,7 +282,7 @@ onUnmounted(() => {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="regcodes" class="mt-4">
|
<TabsContent value="regcodes" class="mt-4">
|
||||||
<div class="rounded-lg border bg-card overflow-x-auto">
|
<div class="rounded-lg border bg-card overflow-x-auto hide-scrollbar">
|
||||||
<div class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium min-w-[500px]">
|
<div class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium min-w-[500px]">
|
||||||
<span class="w-6 shrink-0"></span>
|
<span class="w-6 shrink-0"></span>
|
||||||
<span class="flex-1 min-w-[200px]">令牌</span>
|
<span class="flex-1 min-w-[200px]">令牌</span>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const chartDays = computed(() => isMobile.value ? 15 : 30)
|
|||||||
|
|
||||||
let lineChart: ApexCharts | null = null
|
let lineChart: ApexCharts | null = null
|
||||||
let pieChart: ApexCharts | null = null
|
let pieChart: ApexCharts | null = null
|
||||||
|
let themeObserver: MutationObserver | null = null
|
||||||
|
let themeChangeTimeout: number | null = null
|
||||||
|
|
||||||
const statItems = [
|
const statItems = [
|
||||||
{ key: 'today_execs', label: '今日执行', icon: Play, route: '/history' },
|
{ key: 'today_execs', label: '今日执行', icon: Play, route: '/history' },
|
||||||
@@ -26,6 +28,28 @@ const statItems = [
|
|||||||
{ key: 'running', label: '正在运行', icon: Play, route: '/tasks' },
|
{ key: 'running', label: '正在运行', icon: Play, route: '/tasks' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const isDark = ref(document.documentElement.classList.contains('dark'))
|
||||||
|
|
||||||
|
function getTextColor() {
|
||||||
|
return isDark.value ? '#94a3b8' : '#64748b'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGridColor() {
|
||||||
|
return isDark.value ? '#334155' : '#e2e8f0'
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleThemeChange() {
|
||||||
|
const newIsDark = document.documentElement.classList.contains('dark')
|
||||||
|
if (newIsDark !== isDark.value) {
|
||||||
|
isDark.value = newIsDark
|
||||||
|
chartsLoaded.value = false
|
||||||
|
if (themeChangeTimeout) clearTimeout(themeChangeTimeout)
|
||||||
|
themeChangeTimeout = window.setTimeout(() => {
|
||||||
|
reloadCharts()
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function navigateTo(route?: string) {
|
function navigateTo(route?: string) {
|
||||||
if (route) router.push(route)
|
if (route) router.push(route)
|
||||||
}
|
}
|
||||||
@@ -48,7 +72,6 @@ async function reloadCharts() {
|
|||||||
pieChart.destroy()
|
pieChart.destroy()
|
||||||
pieChart = null
|
pieChart = null
|
||||||
}
|
}
|
||||||
chartsLoaded.value = false
|
|
||||||
|
|
||||||
// 重新获取数据
|
// 重新获取数据
|
||||||
const [sendStatsData, taskStatsData] = await Promise.all([
|
const [sendStatsData, taskStatsData] = await Promise.all([
|
||||||
@@ -62,10 +85,23 @@ async function reloadCharts() {
|
|||||||
renderLineChart()
|
renderLineChart()
|
||||||
renderPieChart()
|
renderPieChart()
|
||||||
chartsLoaded.value = true
|
chartsLoaded.value = true
|
||||||
}, 100)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderLineChart = () => {
|
const renderLineChart = () => {
|
||||||
|
if (lineChart) {
|
||||||
|
lineChart.destroy()
|
||||||
|
lineChart = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.querySelector("#stats-chart")
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
// 清空容器
|
||||||
|
container.innerHTML = ''
|
||||||
|
|
||||||
|
const textColor = getTextColor()
|
||||||
|
const gridColor = getGridColor()
|
||||||
const options = {
|
const options = {
|
||||||
series: [
|
series: [
|
||||||
{ name: '执行总数', data: sendStats.value.map(item => item.total) },
|
{ name: '执行总数', data: sendStats.value.map(item => item.total) },
|
||||||
@@ -97,7 +133,7 @@ const renderLineChart = () => {
|
|||||||
markers: {
|
markers: {
|
||||||
size: 6,
|
size: 6,
|
||||||
colors: ['#3b82f6', '#10b981', '#ef4444'],
|
colors: ['#3b82f6', '#10b981', '#ef4444'],
|
||||||
strokeColors: '#fff',
|
strokeColors: isDark.value ? '#1e293b' : '#fff',
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
hover: { size: 8, sizeOffset: 3 }
|
hover: { size: 8, sizeOffset: 3 }
|
||||||
},
|
},
|
||||||
@@ -105,11 +141,11 @@ const renderLineChart = () => {
|
|||||||
categories: sendStats.value.map(item => item.day.slice(5)),
|
categories: sendStats.value.map(item => item.day.slice(5)),
|
||||||
axisBorder: { show: false },
|
axisBorder: { show: false },
|
||||||
axisTicks: { show: false },
|
axisTicks: { show: false },
|
||||||
labels: { style: { colors: '#64748b', fontSize: '12px', fontFamily: 'Inter, sans-serif' } }
|
labels: { style: { colors: textColor, fontSize: '12px', fontFamily: 'Inter, sans-serif' } }
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
labels: {
|
labels: {
|
||||||
style: { colors: '#64748b', fontSize: '12px', fontFamily: 'Inter, sans-serif' },
|
style: { colors: textColor, fontSize: '12px', fontFamily: 'Inter, sans-serif' },
|
||||||
formatter: (val: number) => String(val),
|
formatter: (val: number) => String(val),
|
||||||
offsetX: -15,
|
offsetX: -15,
|
||||||
minWidth: 20
|
minWidth: 20
|
||||||
@@ -119,7 +155,7 @@ const renderLineChart = () => {
|
|||||||
fill: {
|
fill: {
|
||||||
type: 'gradient',
|
type: 'gradient',
|
||||||
gradient: {
|
gradient: {
|
||||||
shade: 'light',
|
shade: isDark.value ? 'dark' : 'light',
|
||||||
type: 'vertical',
|
type: 'vertical',
|
||||||
shadeIntensity: 0.5,
|
shadeIntensity: 0.5,
|
||||||
gradientToColors: ['#60a5fa', '#34d399', '#f87171'],
|
gradientToColors: ['#60a5fa', '#34d399', '#f87171'],
|
||||||
@@ -130,7 +166,7 @@ const renderLineChart = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
borderColor: '#e2e8f0',
|
borderColor: gridColor,
|
||||||
strokeDashArray: 3,
|
strokeDashArray: 3,
|
||||||
xaxis: { lines: { show: false } },
|
xaxis: { lines: { show: false } },
|
||||||
yaxis: { lines: { show: true } },
|
yaxis: { lines: { show: true } },
|
||||||
@@ -144,13 +180,14 @@ const renderLineChart = () => {
|
|||||||
offsetX: -5,
|
offsetX: -5,
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontFamily: 'Inter, sans-serif',
|
fontFamily: 'Inter, sans-serif',
|
||||||
|
labels: { colors: textColor },
|
||||||
markers: { width: 8, height: 8, radius: 4 }
|
markers: { width: 8, height: 8, radius: 4 }
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
shared: true,
|
shared: true,
|
||||||
intersect: false,
|
intersect: false,
|
||||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
theme: isDark.value ? 'dark' : 'light',
|
||||||
style: { fontSize: '12px', fontFamily: 'Inter, sans-serif' },
|
style: { fontSize: '12px', fontFamily: 'Inter, sans-serif' },
|
||||||
x: { show: true },
|
x: { show: true },
|
||||||
marker: { show: true },
|
marker: { show: true },
|
||||||
@@ -186,60 +223,124 @@ const renderLineChart = () => {
|
|||||||
},
|
},
|
||||||
responsive: [{ breakpoint: 768, options: { chart: { height: 200 }, legend: { position: 'bottom', offsetY: 0 } } }]
|
responsive: [{ breakpoint: 768, options: { chart: { height: 200 }, legend: { position: 'bottom', offsetY: 0 } } }]
|
||||||
}
|
}
|
||||||
lineChart = new ApexCharts(document.querySelector("#stats-chart"), options)
|
lineChart = new ApexCharts(container, options)
|
||||||
lineChart.render()
|
lineChart.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderPieChart = () => {
|
const renderPieChart = () => {
|
||||||
if (taskStats.value.length === 0) return
|
if (taskStats.value.length === 0) return
|
||||||
|
|
||||||
|
if (pieChart) {
|
||||||
|
pieChart.destroy()
|
||||||
|
pieChart = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.querySelector("#pie-chart")
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
// 清空容器
|
||||||
|
container.innerHTML = ''
|
||||||
|
|
||||||
|
const totalCount = taskStats.value.reduce((sum, item) => sum + item.count, 0)
|
||||||
|
const textColor = getTextColor()
|
||||||
const options = {
|
const options = {
|
||||||
series: taskStats.value.map(item => item.count),
|
series: taskStats.value.map(item => item.count),
|
||||||
chart: {
|
chart: {
|
||||||
type: 'pie',
|
type: 'donut',
|
||||||
height: 240,
|
height: 200,
|
||||||
toolbar: { show: false },
|
toolbar: { show: false },
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
animations: {
|
animations: {
|
||||||
enabled: true,
|
enabled: false
|
||||||
easing: 'easeinout',
|
|
||||||
speed: 800,
|
|
||||||
animateGradually: { enabled: true, delay: 150 },
|
|
||||||
dynamicAnimation: { enabled: true, speed: 350 }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
labels: taskStats.value.map(item => item.task_name),
|
labels: taskStats.value.map(item => item.task_name),
|
||||||
colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'],
|
colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'],
|
||||||
legend: {
|
legend: {
|
||||||
position: 'bottom',
|
show: false
|
||||||
fontSize: '12px',
|
},
|
||||||
fontFamily: 'Inter, sans-serif',
|
stroke: {
|
||||||
markers: { width: 8, height: 8, radius: 4 }
|
show: true,
|
||||||
|
width: 2,
|
||||||
|
colors: [isDark.value ? '#1e293b' : '#ffffff']
|
||||||
},
|
},
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
pie: {
|
pie: {
|
||||||
donut: { size: '0%' },
|
donut: {
|
||||||
|
size: '50%',
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
name: {
|
||||||
|
show: true,
|
||||||
|
fontSize: '12px',
|
||||||
|
fontFamily: 'Inter, sans-serif',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: textColor,
|
||||||
|
offsetY: -10
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
show: true,
|
||||||
|
fontSize: '12px',
|
||||||
|
fontFamily: 'Inter, sans-serif',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: textColor,
|
||||||
|
offsetY: 5,
|
||||||
|
formatter: (val: string) => val + ' 次'
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
show: true,
|
||||||
|
showAlways: false,
|
||||||
|
label: '总执行',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontFamily: 'Inter, sans-serif',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: textColor,
|
||||||
|
formatter: () => String(totalCount) + ' 次'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
expandOnClick: true
|
expandOnClick: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
formatter: (val: number) => val.toFixed(1) + '%',
|
formatter: (val: number) => val.toFixed(1) + '%',
|
||||||
style: { fontSize: '10px', fontFamily: 'Inter, sans-serif', fontWeight: 'bold' }
|
style: {
|
||||||
|
fontSize: '11px',
|
||||||
|
fontFamily: 'Inter, sans-serif',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
colors: ['#ffffff']
|
||||||
|
},
|
||||||
|
dropShadow: {
|
||||||
|
enabled: true,
|
||||||
|
top: 1,
|
||||||
|
left: 1,
|
||||||
|
blur: 2,
|
||||||
|
color: '#000',
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
theme: isDark.value ? 'dark' : 'light',
|
||||||
style: { fontSize: '12px', fontFamily: 'Inter, sans-serif' },
|
style: { fontSize: '12px', fontFamily: 'Inter, sans-serif' },
|
||||||
y: { formatter: (val: number) => val + ' 次' }
|
y: { formatter: (val: number) => val + ' 次' }
|
||||||
},
|
},
|
||||||
responsive: [{ breakpoint: 768, options: { chart: { height: 200 }, legend: { position: 'bottom' } } }]
|
responsive: [{ breakpoint: 768, options: { chart: { height: 200 }, legend: { position: 'bottom' } } }]
|
||||||
}
|
}
|
||||||
pieChart = new ApexCharts(document.querySelector("#pie-chart"), options)
|
pieChart = new ApexCharts(container, options)
|
||||||
pieChart.render()
|
pieChart.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
// 监听主题变化
|
||||||
|
themeObserver = new MutationObserver(() => {
|
||||||
|
handleThemeChange()
|
||||||
|
})
|
||||||
|
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [statsData, sendStatsData, taskStatsData] = await Promise.all([
|
const [statsData, sendStatsData, taskStatsData] = await Promise.all([
|
||||||
api.dashboard.stats(),
|
api.dashboard.stats(),
|
||||||
@@ -259,6 +360,14 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
|
if (themeObserver) {
|
||||||
|
themeObserver.disconnect()
|
||||||
|
themeObserver = null
|
||||||
|
}
|
||||||
|
if (themeChangeTimeout) {
|
||||||
|
clearTimeout(themeChangeTimeout)
|
||||||
|
themeChangeTimeout = null
|
||||||
|
}
|
||||||
if (lineChart) lineChart.destroy()
|
if (lineChart) lineChart.destroy()
|
||||||
if (pieChart) pieChart.destroy()
|
if (pieChart) pieChart.destroy()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user