feat: add dark/light theme
This commit is contained in:
@@ -25,6 +25,34 @@ const isUserMenuOpen = ref(false)
|
||||
const userAccount = ref('管理员')
|
||||
const siteConfig = ref<any>({})
|
||||
|
||||
// 主题:明暗模式
|
||||
const getInitialTheme = (): 'light' | 'dark' => {
|
||||
try {
|
||||
const stored = localStorage.getItem('theme') as 'light' | 'dark' | null
|
||||
if (stored === 'light' || stored === 'dark') return stored
|
||||
const systemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
return systemDark ? 'dark' : 'light'
|
||||
} catch {
|
||||
return 'light'
|
||||
}
|
||||
}
|
||||
|
||||
const theme = ref<'light' | 'dark'>(getInitialTheme())
|
||||
|
||||
const applyTheme = (next: 'light' | 'dark') => {
|
||||
theme.value = next
|
||||
if (next === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
try { localStorage.setItem('theme', next) } catch {}
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
applyTheme(theme.value === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
|
||||
// 从JWT中解析用户名
|
||||
const parseJwtUsername = (token: string): string => {
|
||||
try {
|
||||
@@ -255,14 +283,14 @@ const siteTitle = computed(() => {
|
||||
<template>
|
||||
<router-view v-if="!isAuthenticated || route.path == '/login' || route.path == 'login'"></router-view>
|
||||
|
||||
<div class="min-h-screen bg-gray-50" v-else>
|
||||
<div class="min-h-screen bg-background" v-else>
|
||||
<!-- 顶部导航栏 -->
|
||||
<nav class="bg-white shadow-sm border-b border-gray-200">
|
||||
<nav class="bg-background shadow-sm border-b border-border">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Logo/品牌名称 -->
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-lg sm:text-xl font-bold text-gray-900">{{ siteTitle }}</h1>
|
||||
<h1 class="text-lg sm:text-xl font-bold text-foreground">{{ siteTitle }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端导航 -->
|
||||
@@ -270,8 +298,8 @@ const siteTitle = computed(() => {
|
||||
<button v-for="tab in tabRoutes" :key="tab.name" @click="handleTabClick(tab)" :class="[
|
||||
'relative py-2 px-3 text-sm font-medium transition-all duration-200 rounded-md whitespace-nowrap',
|
||||
activeTab === tab.name
|
||||
? 'text-blue-600 bg-blue-50'
|
||||
: 'text-gray-600 hover:text-blue-600 hover:bg-gray-50'
|
||||
? 'text-blue-600 bg-blue-50 dark:text-blue-400 dark:bg-blue-400/10'
|
||||
: 'text-gray-600 hover:text-blue-600 hover:bg-gray-50 dark:text-gray-300 dark:hover:text-blue-400 dark:hover:bg-white/5'
|
||||
]">
|
||||
{{ tab.name }}
|
||||
<!-- 活动状态指示器 -->
|
||||
@@ -282,27 +310,65 @@ const siteTitle = computed(() => {
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- 主题切换(仅桌面显示) -->
|
||||
<button @click="toggleTheme" class="hidden md:inline-flex p-2 rounded-md text-gray-600 hover:text-blue-600 hover:bg-gray-50 transition-colors dark:text-gray-300 dark:hover:text-blue-400 dark:hover:bg-white/5" :title="theme === 'dark' ? '切换到浅色' : '切换到深色'">
|
||||
<svg v-if="theme === 'dark'" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12"/>
|
||||
<line x1="21" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 用户账号下拉菜单 -->
|
||||
<div class="relative user-menu-container">
|
||||
<button @click="toggleUserMenu" class="flex items-center space-x-2 p-2 rounded-md hover:bg-gray-50 transition-colors">
|
||||
<button @click="toggleUserMenu" class="flex items-center space-x-2 p-2 rounded-md hover:bg-muted transition-colors dark:hover:bg-white/5">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="hidden sm:block text-sm font-medium text-gray-700">{{ userAccount }}</span>
|
||||
<span class="hidden sm:block text-sm font-medium text-foreground">{{ userAccount }}</span>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 下拉菜单 -->
|
||||
<div v-if="isUserMenuOpen" class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-50">
|
||||
<div v-if="isUserMenuOpen" class="absolute right-0 mt-2 w-52 bg-card text-foreground rounded-md shadow-lg border border-border z-50">
|
||||
<div class="py-1">
|
||||
<div class="px-4 py-2 text-sm text-gray-500 border-b border-gray-100">
|
||||
<div class="font-medium text-gray-900">{{ userAccount }}</div>
|
||||
<div class="px-4 py-2 text-sm text-muted-foreground border-b border-border">
|
||||
<div class="font-medium text-foreground">{{ userAccount }}</div>
|
||||
<!-- <div class="text-xs">当前登录账号</div> -->
|
||||
</div>
|
||||
<!-- 移动端主题切换入口 -->
|
||||
<button @click="toggleTheme" class="md:hidden w-full text-left px-4 py-2 text-sm hover:bg-muted dark:hover:bg-white/5 flex items-center justify-between">
|
||||
<span>外观</span>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<svg v-if="theme === 'dark'" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12"/>
|
||||
<line x1="21" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
<span class="text-xs text-muted-foreground">{{ theme === 'dark' ? '深色' : '浅色' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button @click="logout" class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors">
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -328,14 +394,14 @@ const siteTitle = computed(() => {
|
||||
</div>
|
||||
|
||||
<!-- 移动端菜单 -->
|
||||
<div v-if="isMobileMenuOpen" class="md:hidden border-t border-gray-200 py-2">
|
||||
<div v-if="isMobileMenuOpen" class="md:hidden border-t border-border py-2">
|
||||
<div class="flex flex-col space-y-1">
|
||||
<button v-for="tab in tabRoutes" :key="tab.name" @click="handleTabClick(tab); isMobileMenuOpen = false"
|
||||
:class="[
|
||||
'text-left py-3 px-4 text-sm font-medium transition-all duration-200 rounded-md',
|
||||
activeTab === tab.name
|
||||
? 'text-blue-600 bg-blue-50'
|
||||
: 'text-gray-600 hover:text-blue-600 hover:bg-gray-50'
|
||||
? 'text-blue-600 bg-blue-50 dark:text-blue-400 dark:bg-blue-400/10'
|
||||
: 'text-muted-foreground hover:text-blue-600 hover:bg-muted dark:hover:bg-white/5'
|
||||
]">
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
|
||||
@@ -161,7 +161,7 @@ onMounted(async () => {
|
||||
<div class="flex-shrink-0">
|
||||
<Dialog v-model:open="isAddCronMessageDialogOpen">
|
||||
<DialogTrigger as-child>
|
||||
<Button variant="default" class="w-full sm:w-auto bg-gray-800 hover:bg-gray-900">
|
||||
<Button variant="default" class="w-full sm:w-auto">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -212,7 +212,9 @@ onMounted(async () => {
|
||||
<TableCell class="max-w-32 truncate" :title="cronMessage.title">{{ cronMessage.title }}</TableCell>
|
||||
<TableCell class="max-w-32 truncate" :title="cronMessage.content">{{ cronMessage.content }}</TableCell>
|
||||
<TableCell>
|
||||
<code class="bg-gray-100 px-2 py-1 rounded text-sm">{{ cronMessage.cron }}</code>
|
||||
<code class="px-2 py-1 rounded text-sm font-mono bg-muted text-foreground border border-border">
|
||||
{{ cronMessage.cron }}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell class="max-w-24 truncate" :title="cronMessage.task_id">{{ cronMessage.task_id }}</TableCell>
|
||||
<TableCell class="max-w-32 truncate" :title="cronMessage.next_time || '-'">{{ cronMessage.next_time || '-' }}
|
||||
|
||||
@@ -256,7 +256,7 @@ const renderLineChart = () => {
|
||||
enabled: true,
|
||||
shared: true,
|
||||
intersect: false,
|
||||
theme: 'light',
|
||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
@@ -279,28 +279,33 @@ const renderLineChart = () => {
|
||||
const total = successCount + failedCount;
|
||||
const successRate = total > 0 ? ((successCount / total) * 100).toFixed(1) : '0.0';
|
||||
|
||||
const containerCls = 'bg-card text-foreground p-3 rounded-lg shadow-lg border border-border';
|
||||
const labelMutedCls = 'text-sm text-muted-foreground';
|
||||
const valueStrongCls = 'text-sm font-medium text-foreground';
|
||||
const successRateCls = 'text-sm font-medium';
|
||||
|
||||
return `
|
||||
<div class="bg-white p-3 rounded-lg shadow-lg border">
|
||||
<div class="font-medium text-gray-900 mb-2">${w.globals.categoryLabels[dataPointIndex]}</div>
|
||||
<div class="${containerCls}">
|
||||
<div class="font-medium mb-2">${w.globals.categoryLabels[dataPointIndex]}</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
|
||||
<span class="text-sm text-gray-600">成功:</span>
|
||||
<span class="${labelMutedCls}">成功:</span>
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900">${successCount} 条</span>
|
||||
<span class="${valueStrongCls}">${successCount} 条</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<span class="w-2 h-2 bg-red-500 rounded-full mr-2"></span>
|
||||
<span class="text-sm text-gray-600">失败:</span>
|
||||
<span class="${labelMutedCls}">失败:</span>
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900">${failedCount} 条</span>
|
||||
<span class="${valueStrongCls}">${failedCount} 条</span>
|
||||
</div>
|
||||
<div class="border-t pt-1 mt-2">
|
||||
<div class="border-t border-border pt-1 mt-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">成功率:</span>
|
||||
<span class="text-sm font-medium text-green-600">${successRate}%</span>
|
||||
<span class="${labelMutedCls}">成功率:</span>
|
||||
<span class="${successRateCls}">${successRate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -165,8 +165,8 @@ onMounted(async () => {
|
||||
<SheetTitle>{{ selectedTaskName }} - 托管消息详情</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div class="mt-4">
|
||||
<div class="bg-gray-50 p-4 rounded-lg max-h-[82vh] overflow-y-auto break-words">
|
||||
<pre class="whitespace-pre-wrap text-sm font-mono">{{ selectedLog }}</pre>
|
||||
<div class="bg-card p-4 rounded-lg border border-border max-h-[82vh] overflow-y-auto break-words">
|
||||
<pre class="whitespace-pre-wrap text-sm font-mono text-foreground">{{ selectedLog }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
||||
@@ -240,8 +240,8 @@ onMounted(async () => {
|
||||
<SheetTitle>{{ selectedTaskName }} - 发信日志详情</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div class="mt-4">
|
||||
<div class="bg-gray-50 p-4 rounded-lg max-h-[82vh] overflow-y-auto break-words">
|
||||
<pre class="whitespace-pre-wrap text-sm font-mono">{{ selectedLog }}</pre>
|
||||
<div class="bg-card p-4 rounded-lg border border-border max-h-[82vh] overflow-y-auto break-words">
|
||||
<pre class="whitespace-pre-wrap text-sm font-mono text-foreground">{{ selectedLog }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
||||
@@ -177,7 +177,7 @@ onMounted(async () => {
|
||||
<div class="flex-shrink-0">
|
||||
<Dialog v-model:open="isAddChannelDrawerOpen">
|
||||
<DialogTrigger as-child>
|
||||
<Button variant="default" class="w-full sm:w-auto bg-gray-800 hover:bg-gray-900">
|
||||
<Button variant="default" class="w-full sm:w-auto">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -265,8 +265,8 @@ onMounted(async () => {
|
||||
<SheetTitle>{{ selectedChannelName }} - 发信方式配置详情</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div class="mt-6">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<pre class="whitespace-pre-wrap text-sm font-mono">{{ selectedConfig }}</pre>
|
||||
<div class="bg-card p-4 rounded-lg border border-border">
|
||||
<pre class="whitespace-pre-wrap text-sm font-mono text-foreground">{{ selectedConfig }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
||||
@@ -186,7 +186,7 @@ onMounted(async () => {
|
||||
<div class="flex-shrink-0">
|
||||
<Drawer v-model:open="isAddChannelDrawerOpen">
|
||||
<DrawerTrigger as-child>
|
||||
<Button variant="default" class="w-full sm:w-auto bg-gray-800 hover:bg-gray-900">
|
||||
<Button variant="default" class="w-full sm:w-auto">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -275,8 +275,8 @@ onMounted(async () => {
|
||||
<SheetTitle>{{ selectedChannelName }} - 发信方式配置详情</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div class="mt-6">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<pre class="whitespace-pre-wrap text-sm font-mono">{{ selectedConfig }}</pre>
|
||||
<div class="bg-card p-4 rounded-lg border border-border">
|
||||
<pre class="whitespace-pre-wrap text-sm font-mono text-foreground">{{ selectedConfig }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
||||
@@ -44,10 +44,10 @@ export default {
|
||||
:key="item.id"
|
||||
@click="$emit('update:activeTab', item.id)"
|
||||
:class="[
|
||||
'w-full flex items-center px-4 py-3 text-left text-sm font-medium transition-colors',
|
||||
'w-full flex items-center px-4 py-3 text-left text-sm font-medium transition-colors rounded-md',
|
||||
activeTab === item.id
|
||||
? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
|
||||
? 'bg-accent text-accent-foreground border-r-2 border-primary dark:bg-accent/40'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground dark:hover:bg-white/5'
|
||||
]"
|
||||
>
|
||||
<component :is="item.icon" class="mr-3 w-5 h-5" />
|
||||
|
||||
@@ -111,9 +111,9 @@ export default {
|
||||
<label class="text-sm font-medium text-gray-700">站点图标(只支持svg文本)</label>
|
||||
<Input v-model="state.logo" placeholder="请输入自定义的网站logo(svg文本)" />
|
||||
<!-- SVG预览 -->
|
||||
<div v-if="state.logo" class="mt-2 p-3 border border-gray-200 rounded-md bg-gray-50">
|
||||
<div class="text-xs text-gray-500 mb-2">预览效果:</div>
|
||||
<div class="flex items-center justify-center w-16 h-16 bg-white border border-gray-300 rounded"
|
||||
<div v-if="state.logo" class="mt-2 p-3 border border-border rounded-md bg-card">
|
||||
<div class="text-xs text-muted-foreground mb-2">预览效果:</div>
|
||||
<div class="flex items-center justify-center w-16 h-16 bg-white dark:bg-white border border-border rounded"
|
||||
v-html="state.logo"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user