fix: opt page style

This commit is contained in:
engigu
2026-01-22 20:03:58 +08:00
parent 05f582bd28
commit f40e43c1a9
2 changed files with 49 additions and 31 deletions
+9 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router' import { RouterLink, RouterView, useRoute } from 'vue-router'
import { resetAuthCache } from '@/router' import { resetAuthCache } from '@/router'
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound, Package, Menu, X, Server } from 'lucide-vue-next' import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound, Package, Menu, X, Server } from 'lucide-vue-next'
@@ -46,6 +46,10 @@ const cachedSentence = loadSentenceFromCache()
const sentence = ref(cachedSentence || '欢迎使用白虎面板') const sentence = ref(cachedSentence || '欢迎使用白虎面板')
const { siteSettings, loadSettings } = useSiteSettings() const { siteSettings, loadSettings } = useSiteSettings()
const mobileMenuOpen = ref(false) const mobileMenuOpen = ref(false)
const sentenceContent = computed(() => {
const match = sentence.value.match(/^"(.+)"—— /)
return match ? match[1] : sentence.value
})
const navItems = [ const navItems = [
{ to: '/', icon: LayoutDashboard, label: '数据仪表', exact: true }, { to: '/', icon: LayoutDashboard, label: '数据仪表', exact: true },
@@ -140,7 +144,10 @@ onMounted(() => {
<Button variant="ghost" size="icon" class="h-8 w-8 lg:hidden" @click="mobileMenuOpen = true"> <Button variant="ghost" size="icon" class="h-8 w-8 lg:hidden" @click="mobileMenuOpen = true">
<Menu class="h-5 w-5" /> <Menu class="h-5 w-5" />
</Button> </Button>
<span class="text-[10px] sm:text-sm text-muted-foreground truncate flex-1 min-w-0 mr-4" :title="sentence">{{ sentence }}</span> <span class="text-sm text-muted-foreground truncate flex-1 min-w-0 mr-4" :title="sentence">
<span class="hidden sm:inline">{{ sentence }}</span>
<span class="sm:hidden">{{ sentenceContent }}</span>
</span>
</div> </div>
<ThemeToggle /> <ThemeToggle />
</div> </div>
+40 -29
View File
@@ -14,6 +14,10 @@ const taskStats = ref<TaskStatsItem[]>([])
const chartsLoaded = ref(false) const chartsLoaded = ref(false)
const sentence = ref('') const sentence = ref('')
const isMobile = ref(window.innerWidth < 768) const isMobile = ref(window.innerWidth < 768)
const sentenceContent = computed(() => {
const match = sentence.value.match(/^"(.+)"—— /)
return match ? match[1] : sentence.value
})
const chartDays = computed(() => isMobile.value ? 15 : 30) const chartDays = computed(() => isMobile.value ? 15 : 30)
let lineChart: ApexCharts | null = null let lineChart: ApexCharts | null = null
@@ -38,23 +42,23 @@ function animateNumber(key: keyof Stats, target: number) {
const start = displayStats.value[key] const start = displayStats.value[key]
const duration = 800 // 动画持续时间 const duration = 800 // 动画持续时间
const startTime = performance.now() const startTime = performance.now()
function update(currentTime: number) { function update(currentTime: number) {
const elapsed = currentTime - startTime const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1) const progress = Math.min(elapsed / duration, 1)
// 使用 easeOutCubic 缓动函数 // 使用 easeOutCubic 缓动函数
const easeProgress = 1 - Math.pow(1 - progress, 3) const easeProgress = 1 - Math.pow(1 - progress, 3)
displayStats.value[key] = Math.round(start + (target - start) * easeProgress) displayStats.value[key] = Math.round(start + (target - start) * easeProgress)
if (progress < 1) { if (progress < 1) {
requestAnimationFrame(update) requestAnimationFrame(update)
} else { } else {
displayStats.value[key] = target displayStats.value[key] = target
} }
} }
requestAnimationFrame(update) requestAnimationFrame(update)
} }
@@ -80,7 +84,7 @@ function getGridColor() {
function handleThemeChange() { function handleThemeChange() {
// 初始化期间忽略主题变化 // 初始化期间忽略主题变化
if (isInitializing) return if (isInitializing) return
const newIsDark = document.documentElement.classList.contains('dark') const newIsDark = document.documentElement.classList.contains('dark')
if (newIsDark !== isDark.value) { if (newIsDark !== isDark.value) {
isDark.value = newIsDark isDark.value = newIsDark
@@ -128,11 +132,11 @@ async function reloadCharts() {
// 等待 Vue 更新 DOM // 等待 Vue 更新 DOM
await nextTick() await nextTick()
await new Promise(resolve => setTimeout(resolve, 100)) await new Promise(resolve => setTimeout(resolve, 100))
// 检查容器是否存在再渲染 // 检查容器是否存在再渲染
const statsChart = document.querySelector("#stats-chart") const statsChart = document.querySelector("#stats-chart")
const pieChartEl = document.querySelector("#pie-chart") const pieChartEl = document.querySelector("#pie-chart")
if (statsChart && pieChartEl && statsChart.parentElement && pieChartEl.parentElement) { if (statsChart && pieChartEl && statsChart.parentElement && pieChartEl.parentElement) {
renderLineChart() renderLineChart()
renderPieChart() renderPieChart()
@@ -144,15 +148,15 @@ async function reloadCharts() {
const renderLineChart = () => { const renderLineChart = () => {
const container = document.querySelector("#stats-chart") const container = document.querySelector("#stats-chart")
if (!container || !container.parentElement) return if (!container || !container.parentElement) return
if (lineChart) { if (lineChart) {
lineChart.destroy() lineChart.destroy()
lineChart = null lineChart = null
} }
// 清空容器 // 清空容器
container.innerHTML = '' container.innerHTML = ''
const textColor = getTextColor() const textColor = getTextColor()
const gridColor = getGridColor() const gridColor = getGridColor()
const options = { const options = {
@@ -283,18 +287,18 @@ const renderLineChart = () => {
const renderPieChart = () => { const renderPieChart = () => {
if (taskStats.value.length === 0) return if (taskStats.value.length === 0) return
const container = document.querySelector("#pie-chart") const container = document.querySelector("#pie-chart")
if (!container || !container.parentElement) return if (!container || !container.parentElement) return
if (pieChart) { if (pieChart) {
pieChart.destroy() pieChart.destroy()
pieChart = null pieChart = null
} }
// 清空容器 // 清空容器
container.innerHTML = '' container.innerHTML = ''
const totalCount = taskStats.value.reduce((sum, item) => sum + item.count, 0) const totalCount = taskStats.value.reduce((sum, item) => sum + item.count, 0)
const textColor = getTextColor() const textColor = getTextColor()
const options = { const options = {
@@ -367,13 +371,13 @@ const renderPieChart = () => {
dataLabels: { dataLabels: {
enabled: true, enabled: true,
formatter: (val: number) => val.toFixed(1) + '%', formatter: (val: number) => val.toFixed(1) + '%',
style: { style: {
fontSize: '11px', fontSize: '11px',
fontFamily: 'Inter, sans-serif', fontFamily: 'Inter, sans-serif',
fontWeight: 'bold', fontWeight: 'bold',
colors: ['#ffffff'] colors: ['#ffffff']
}, },
dropShadow: { dropShadow: {
enabled: true, enabled: true,
top: 1, top: 1,
left: 1, left: 1,
@@ -396,7 +400,7 @@ const renderPieChart = () => {
onMounted(async () => { onMounted(async () => {
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
try { try {
const [statsData, sendStatsData, taskStatsData, sentenceData] = await Promise.all([ const [statsData, sendStatsData, taskStatsData, sentenceData] = await Promise.all([
api.dashboard.stats(), api.dashboard.stats(),
@@ -408,13 +412,13 @@ onMounted(async () => {
sendStats.value = sendStatsData sendStats.value = sendStatsData
taskStats.value = taskStatsData taskStats.value = taskStatsData
sentence.value = sentenceData.sentence sentence.value = sentenceData.sentence
// 渲染图表 // 渲染图表
setTimeout(() => { setTimeout(() => {
renderLineChart() renderLineChart()
renderPieChart() renderPieChart()
chartsLoaded.value = true chartsLoaded.value = true
// 图表渲染完成后,延迟启动主题监听,避免初始化时的闪烁 // 图表渲染完成后,延迟启动主题监听,避免初始化时的闪烁
setTimeout(() => { setTimeout(() => {
isInitializing = false isInitializing = false
@@ -425,7 +429,7 @@ onMounted(async () => {
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
}, 500) }, 500)
}, 100) }, 100)
} catch {} } catch { }
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -447,11 +451,15 @@ onUnmounted(() => {
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<h2 class="text-2xl font-bold tracking-tight">数据仪表</h2> <h2 class="text-2xl font-bold tracking-tight">数据仪表</h2>
<p class="text-muted-foreground">{{ sentence || '查看系统运行状态和统计数据' }}</p> <p class="text-muted-foreground truncate" :title="sentence">
<span class="hidden sm:inline">{{ sentence || '查看系统运行状态和统计数据' }}</span>
<span class="sm:hidden">{{ sentenceContent || '查看系统运行状态和统计数据' }}</span>
</p>
</div> </div>
<div class="grid gap-4 grid-cols-2 sm:grid-cols-3 lg:grid-cols-6"> <div class="grid gap-4 grid-cols-2 sm:grid-cols-3 lg:grid-cols-6">
<Card v-for="item in statItems" :key="item.key" class="cursor-pointer hover:bg-accent/50 transition-colors" @click="navigateTo(item.route)"> <Card v-for="item in statItems" :key="item.key" class="cursor-pointer hover:bg-accent/50 transition-colors"
@click="navigateTo(item.route)">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-xs sm:text-sm font-medium">{{ item.label }}</CardTitle> <CardTitle class="text-xs sm:text-sm font-medium">{{ item.label }}</CardTitle>
<component :is="item.icon" class="h-4 w-4 text-muted-foreground" /> <component :is="item.icon" class="h-4 w-4 text-muted-foreground" />
@@ -470,7 +478,8 @@ onUnmounted(() => {
</CardHeader> </CardHeader>
<CardContent class="relative h-[300px]"> <CardContent class="relative h-[300px]">
<div id="stats-chart" class="w-full h-full"></div> <div id="stats-chart" class="w-full h-full"></div>
<div v-if="!chartsLoaded" class="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-card"> <div v-if="!chartsLoaded"
class="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-card">
加载中... 加载中...
</div> </div>
</CardContent> </CardContent>
@@ -483,10 +492,12 @@ onUnmounted(() => {
</CardHeader> </CardHeader>
<CardContent class="relative h-[300px]"> <CardContent class="relative h-[300px]">
<div id="pie-chart" class="w-full h-full"></div> <div id="pie-chart" class="w-full h-full"></div>
<div v-if="!chartsLoaded" class="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-card"> <div v-if="!chartsLoaded"
class="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-card">
加载中... 加载中...
</div> </div>
<div v-else-if="taskStats.length === 0" class="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-card"> <div v-else-if="taskStats.length === 0"
class="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-card">
暂无数据 暂无数据
</div> </div>
</CardContent> </CardContent>