feat: 支持Dark Mode

This commit is contained in:
shaw
2025-08-22 22:09:38 +08:00
parent 8328b6ddac
commit d2f0ac37a9
37 changed files with 3226 additions and 1155 deletions

View File

@@ -2,13 +2,18 @@
<div ref="triggerRef" class="relative">
<!-- 选择器主体 -->
<div
class="form-input flex w-full cursor-pointer items-center justify-between"
class="form-input flex w-full cursor-pointer items-center justify-between dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
:class="{ 'opacity-50': disabled }"
@click="!disabled && toggleDropdown()"
>
<span :class="modelValue ? 'text-gray-900' : 'text-gray-500'">{{ selectedLabel }}</span>
<span
:class="
modelValue ? 'text-gray-900 dark:text-gray-200' : 'text-gray-500 dark:text-gray-400'
"
>{{ selectedLabel }}</span
>
<i
class="fas fa-chevron-down text-gray-400 transition-transform duration-200"
class="fas fa-chevron-down text-gray-400 transition-transform duration-200 dark:text-gray-500"
:class="{ 'rotate-180': showDropdown }"
/>
</div>
@@ -26,27 +31,27 @@
<div
v-if="showDropdown"
ref="dropdownRef"
class="absolute z-50 flex flex-col rounded-lg border border-gray-200 bg-white shadow-lg"
class="absolute z-50 flex flex-col rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"
:style="dropdownStyle"
>
<!-- 搜索框 -->
<div class="flex-shrink-0 border-b border-gray-200 p-3">
<div class="flex-shrink-0 border-b border-gray-200 p-3 dark:border-gray-600">
<div class="relative">
<input
ref="searchInput"
v-model="searchQuery"
class="form-input w-full text-sm"
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
placeholder="搜索账号名称..."
style="padding-left: 40px; padding-right: 36px"
type="text"
@input="handleSearch"
/>
<i
class="fas fa-search pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-400"
class="fas fa-search pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-400 dark:text-gray-500"
/>
<button
v-if="searchQuery"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400"
type="button"
@click="clearSearch"
>
@@ -59,59 +64,67 @@
<div class="custom-scrollbar flex-1 overflow-y-auto">
<!-- 默认选项 -->
<div
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50"
:class="{ 'bg-blue-50': !modelValue }"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{ 'bg-blue-50 dark:bg-blue-900/20': !modelValue }"
@click="selectAccount(null)"
>
<span class="text-gray-700">{{ defaultOptionText }}</span>
<span class="text-gray-700 dark:text-gray-300">{{ defaultOptionText }}</span>
</div>
<!-- 分组选项 -->
<div v-if="filteredGroups.length > 0">
<div class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500">调度分组</div>
<div
class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-400"
>
调度分组
</div>
<div
v-for="group in filteredGroups"
:key="`group:${group.id}`"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50"
:class="{ 'bg-blue-50': modelValue === `group:${group.id}` }"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{ 'bg-blue-50 dark:bg-blue-900/20': modelValue === `group:${group.id}` }"
@click="selectAccount(`group:${group.id}`)"
>
<div class="flex items-center justify-between">
<span class="text-gray-700">{{ group.name }}</span>
<span class="text-xs text-gray-500">{{ group.memberCount || 0 }} 个成员</span>
<span class="text-gray-700 dark:text-gray-300">{{ group.name }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400"
>{{ group.memberCount || 0 }} 个成员</span
>
</div>
</div>
</div>
<!-- OAuth 账号 -->
<div v-if="filteredOAuthAccounts.length > 0">
<div class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500">
<div
class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-400"
>
{{ platform === 'claude' ? 'Claude OAuth 专属账号' : 'OAuth 专属账号' }}
</div>
<div
v-for="account in filteredOAuthAccounts"
:key="account.id"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50"
:class="{ 'bg-blue-50': modelValue === account.id }"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{ 'bg-blue-50 dark:bg-blue-900/20': modelValue === account.id }"
@click="selectAccount(account.id)"
>
<div class="flex items-center justify-between">
<div>
<span class="text-gray-700">{{ account.name }}</span>
<span class="text-gray-700 dark:text-gray-300">{{ account.name }}</span>
<span
class="ml-2 rounded-full px-2 py-0.5 text-xs"
:class="
account.isActive
? 'bg-green-100 text-green-700'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: account.status === 'unauthorized'
? 'bg-orange-100 text-orange-700'
: 'bg-red-100 text-red-700'
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
"
>
{{ getAccountStatusText(account) }}
</span>
</div>
<span class="text-xs text-gray-400">
<span class="text-xs text-gray-400 dark:text-gray-500">
{{ formatDate(account.createdAt) }}
</span>
</div>
@@ -120,33 +133,37 @@
<!-- Console 账号 Claude -->
<div v-if="platform === 'claude' && filteredConsoleAccounts.length > 0">
<div class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500">
<div
class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-400"
>
Claude Console 专属账号
</div>
<div
v-for="account in filteredConsoleAccounts"
:key="account.id"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50"
:class="{ 'bg-blue-50': modelValue === `console:${account.id}` }"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{
'bg-blue-50 dark:bg-blue-900/20': modelValue === `console:${account.id}`
}"
@click="selectAccount(`console:${account.id}`)"
>
<div class="flex items-center justify-between">
<div>
<span class="text-gray-700">{{ account.name }}</span>
<span class="text-gray-700 dark:text-gray-300">{{ account.name }}</span>
<span
class="ml-2 rounded-full px-2 py-0.5 text-xs"
:class="
account.isActive
? 'bg-green-100 text-green-700'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: account.status === 'unauthorized'
? 'bg-orange-100 text-orange-700'
: 'bg-red-100 text-red-700'
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
"
>
{{ getAccountStatusText(account) }}
</span>
</div>
<span class="text-xs text-gray-400">
<span class="text-xs text-gray-400 dark:text-gray-500">
{{ formatDate(account.createdAt) }}
</span>
</div>
@@ -154,7 +171,10 @@
</div>
<!-- 无搜索结果 -->
<div v-if="searchQuery && !hasResults" class="px-4 py-8 text-center text-gray-500">
<div
v-if="searchQuery && !hasResults"
class="px-4 py-8 text-center text-gray-500 dark:text-gray-400"
>
<i class="fas fa-search mb-2 text-2xl" />
<p class="text-sm">没有找到匹配的账号</p>
</div>

View File

@@ -14,10 +14,10 @@
<i class="fas fa-exclamation-triangle text-lg text-white" />
</div>
<div class="flex-1">
<h3 class="mb-2 text-lg font-semibold text-gray-900">
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
{{ title }}
</h3>
<div class="whitespace-pre-line leading-relaxed text-gray-600">
<div class="whitespace-pre-line leading-relaxed text-gray-700 dark:text-gray-400">
{{ message }}
</div>
</div>
@@ -25,7 +25,7 @@
<div class="flex items-center justify-end gap-3">
<button
class="btn bg-gray-100 px-6 py-3 text-gray-700 hover:bg-gray-200"
class="btn bg-gray-100 px-6 py-3 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
:disabled="isProcessing"
@click="handleCancel"
>
@@ -141,6 +141,10 @@ defineExpose({
backdrop-filter: blur(8px);
}
:global(.dark) .modal {
background: rgba(0, 0, 0, 0.7);
}
.modal-content {
background: white;
border-radius: 16px;
@@ -150,6 +154,12 @@ defineExpose({
overflow-y: auto;
}
:global(.dark) .modal-content {
background: #1f2937;
border: 1px solid #374151;
box-shadow: 0 20px 64px rgba(0, 0, 0, 0.8);
}
.btn {
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2;
}
@@ -197,12 +207,24 @@ defineExpose({
border-radius: 3px;
}
:global(.dark) .modal-content::-webkit-scrollbar-track {
background: #374151;
}
.modal-content::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
:global(.dark) .modal-content::-webkit-scrollbar-thumb {
background: #4b5563;
}
.modal-content::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
:global(.dark) .modal-content::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
</style>

View File

@@ -1,7 +1,9 @@
<template>
<Teleport to="body">
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="modal-content mx-auto w-full max-w-md p-6">
<div
class="modal-content mx-auto w-full max-w-md rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-800"
>
<div class="mb-6 flex items-start gap-4">
<div
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-yellow-400 to-yellow-500"
@@ -9,10 +11,10 @@
<i class="fas fa-exclamation text-xl text-white" />
</div>
<div class="flex-1">
<h3 class="mb-2 text-lg font-bold text-gray-900">
<h3 class="mb-2 text-lg font-bold text-gray-900 dark:text-white">
{{ title }}
</h3>
<p class="whitespace-pre-line text-sm leading-relaxed text-gray-600">
<p class="whitespace-pre-line text-sm leading-relaxed text-gray-700 dark:text-gray-300">
{{ message }}
</p>
</div>
@@ -20,7 +22,7 @@
<div class="flex gap-3">
<button
class="flex-1 rounded-xl bg-gray-100 px-4 py-2.5 font-medium text-gray-700 transition-colors hover:bg-gray-200"
class="flex-1 rounded-xl bg-gray-100 px-4 py-2.5 font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
@click="$emit('cancel')"
>
{{ cancelText }}
@@ -63,3 +65,15 @@ defineProps({
defineEmits(['confirm', 'cancel'])
</script>
<style scoped>
.modal {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
}
:global(.dark) .modal {
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
}
</style>

View File

@@ -3,17 +3,19 @@
<!-- 触发器 -->
<div
ref="triggerRef"
class="relative flex cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 shadow-sm transition-all duration-200 hover:shadow-md"
class="relative flex cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 shadow-sm transition-all duration-200 hover:shadow-md dark:border-gray-600 dark:bg-gray-800"
:class="[isOpen && 'border-blue-400 shadow-md']"
@click="toggleDropdown"
>
<i v-if="icon" :class="['fas', icon, 'text-sm', iconColor]"></i>
<span class="select-none whitespace-nowrap text-sm font-medium text-gray-700">
<span
class="select-none whitespace-nowrap text-sm font-medium text-gray-700 dark:text-gray-200"
>
{{ selectedLabel || placeholder }}
</span>
<i
:class="[
'fas fa-chevron-down ml-auto text-xs text-gray-400 transition-transform duration-200',
'fas fa-chevron-down ml-auto text-xs text-gray-400 transition-transform duration-200 dark:text-gray-500',
isOpen && 'rotate-180'
]"
></i>
@@ -32,7 +34,7 @@
<div
v-if="isOpen"
ref="dropdownRef"
class="fixed z-[9999] min-w-max overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg"
class="fixed z-[9999] min-w-max overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"
:style="dropdownStyle"
>
<div class="max-h-60 overflow-y-auto py-1">
@@ -42,8 +44,8 @@
class="flex cursor-pointer items-center gap-2 whitespace-nowrap px-3 py-2 text-sm transition-colors duration-150"
:class="[
option.value === modelValue
? 'bg-blue-50 font-medium text-blue-700'
: 'text-gray-700 hover:bg-gray-50'
? 'bg-blue-50 font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700'
]"
@click="selectOption(option)"
>

View File

@@ -2,7 +2,7 @@
<div class="flex items-center gap-4">
<!-- Logo区域 -->
<div
class="flex h-12 w-12 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-gray-300/30 bg-gradient-to-br from-blue-500/20 to-purple-500/20 backdrop-blur-sm"
class="flex h-12 w-12 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-gray-300/30 bg-gradient-to-br from-blue-500/20 to-purple-500/20 backdrop-blur-sm dark:border-gray-600/30 dark:from-blue-600/20 dark:to-purple-600/20"
>
<template v-if="!loading">
<img
@@ -12,9 +12,9 @@
:src="logoSrc"
@error="handleLogoError"
/>
<i v-else class="fas fa-cloud text-xl text-gray-700" />
<i v-else class="fas fa-cloud text-xl text-gray-700 dark:text-gray-300" />
</template>
<div v-else class="h-8 w-8 animate-pulse rounded bg-gray-300/50" />
<div v-else class="h-8 w-8 animate-pulse rounded bg-gray-300/50 dark:bg-gray-600/50" />
</div>
<!-- 标题区域 -->
@@ -25,11 +25,14 @@
{{ title }}
</h1>
</template>
<div v-else-if="loading" class="h-8 w-64 animate-pulse rounded bg-gray-300/50" />
<div
v-else-if="loading"
class="h-8 w-64 animate-pulse rounded bg-gray-300/50 dark:bg-gray-600/50"
/>
<!-- 插槽用于版本信息等额外内容 -->
<slot name="after-title" />
</div>
<p v-if="subtitle" class="mt-0.5 text-sm leading-tight text-gray-600">
<p v-if="subtitle" class="mt-0.5 text-sm leading-tight text-gray-600 dark:text-gray-400">
{{ subtitle }}
</p>
</div>

View File

@@ -2,13 +2,16 @@
<div class="stat-card">
<div class="flex items-start justify-between">
<div class="flex-1">
<p class="mb-1 text-xs font-medium text-gray-600 sm:text-sm">
<p class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-400 sm:text-sm">
{{ title }}
</p>
<p class="text-2xl font-bold text-gray-800 sm:text-3xl">
<p class="text-2xl font-bold text-gray-800 dark:text-gray-100 sm:text-3xl">
{{ value }}
</p>
<p v-if="subtitle" class="mt-1.5 text-xs text-gray-500 sm:mt-2 sm:text-sm">
<p
v-if="subtitle"
class="mt-1.5 text-xs text-gray-500 dark:text-gray-400 sm:mt-2 sm:text-sm"
>
{{ subtitle }}
</p>
</div>

View File

@@ -0,0 +1,573 @@
<template>
<div class="theme-toggle-container">
<!-- 紧凑模式仅显示图标按钮 -->
<button
v-if="mode === 'compact'"
class="theme-toggle-button"
:title="themeTooltip"
@click="handleCycleTheme"
>
<transition mode="out-in" name="fade">
<i v-if="themeStore.themeMode === 'light'" key="sun" class="fas fa-sun" />
<i v-else-if="themeStore.themeMode === 'dark'" key="moon" class="fas fa-moon" />
<i v-else key="auto" class="fas fa-circle-half-stroke" />
</transition>
</button>
<!-- 下拉菜单模式 - 改为创意切换开关 -->
<div v-else-if="mode === 'dropdown'" class="theme-switch-wrapper">
<button
class="theme-switch"
:class="{
'is-dark': themeStore.themeMode === 'dark',
'is-auto': themeStore.themeMode === 'auto'
}"
:title="themeTooltip"
@click="handleCycleTheme"
>
<!-- 背景装饰 -->
<div class="switch-bg">
<div class="stars">
<span></span>
<span></span>
<span></span>
</div>
<div class="clouds">
<span></span>
<span></span>
</div>
</div>
<!-- 切换滑块 -->
<div class="switch-handle">
<div class="handle-icon">
<i v-if="themeStore.themeMode === 'light'" class="fas fa-sun" />
<i v-else-if="themeStore.themeMode === 'dark'" class="fas fa-moon" />
<i v-else class="fas fa-circle-half-stroke" />
</div>
</div>
</button>
</div>
<!-- 分段按钮模式 -->
<div v-else-if="mode === 'segmented'" class="theme-segmented">
<button
v-for="option in themeOptions"
:key="option.value"
class="theme-segment"
:class="{ active: themeStore.themeMode === option.value }"
:title="option.label"
@click="selectTheme(option.value)"
>
<i :class="option.icon" />
<span v-if="showLabel" class="ml-1 hidden sm:inline">{{ option.shortLabel }}</span>
</button>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useThemeStore } from '@/stores/theme'
// Props
defineProps({
// 显示模式compact紧凑、dropdown下拉、segmented分段
mode: {
type: String,
default: 'compact',
validator: (value) => ['compact', 'dropdown', 'segmented'].includes(value)
},
// 是否显示文字标签
showLabel: {
type: Boolean,
default: false
}
})
// Store
const themeStore = useThemeStore()
// 主题选项配置
const themeOptions = [
{
value: 'light',
label: '浅色模式',
shortLabel: '浅色',
icon: 'fas fa-sun'
},
{
value: 'dark',
label: '深色模式',
shortLabel: '深色',
icon: 'fas fa-moon'
},
{
value: 'auto',
label: '跟随系统',
shortLabel: '自动',
icon: 'fas fa-circle-half-stroke'
}
]
// 计算属性
const themeTooltip = computed(() => {
const current = themeOptions.find((opt) => opt.value === themeStore.themeMode)
return current ? `点击切换主题 - ${current.label}` : '切换主题'
})
// 方法
const handleCycleTheme = () => {
themeStore.cycleThemeMode()
}
const selectTheme = (mode) => {
themeStore.setThemeMode(mode)
}
</script>
<style scoped>
/* 容器样式 */
.theme-toggle-container {
position: relative;
display: inline-flex;
align-items: center;
}
/* 基础按钮样式 - 更简洁优雅 */
.theme-toggle-button {
@apply flex items-center justify-center;
@apply h-9 w-9 rounded-full;
@apply bg-white/80 dark:bg-gray-800/80;
@apply hover:bg-white/90 dark:hover:bg-gray-700/90;
@apply text-gray-600 dark:text-gray-300;
@apply border border-gray-200/50 dark:border-gray-600/50;
@apply transition-all duration-200 ease-out;
@apply shadow-md backdrop-blur-sm hover:shadow-lg;
@apply hover:scale-110 active:scale-95;
position: relative;
overflow: hidden;
}
/* 添加优雅的光环效果 */
.theme-toggle-button::before {
content: '';
position: absolute;
inset: -2px;
border-radius: inherit;
background: conic-gradient(
from 180deg at 50% 50%,
rgba(59, 130, 246, 0.2),
rgba(147, 51, 234, 0.2),
rgba(59, 130, 246, 0.2)
);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
animation: rotate 3s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.theme-toggle-button:hover::before {
opacity: 0.6;
}
/* 图标样式优化 - 更生动 */
.theme-toggle-button i {
@apply text-base;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.theme-toggle-button:hover i {
transform: rotate(180deg) scale(1.1);
}
/* 不同主题的图标颜色 */
.theme-toggle-button i.fa-sun {
@apply text-amber-500;
}
.theme-toggle-button i.fa-moon {
@apply text-indigo-500;
}
.theme-toggle-button i.fa-circle-half-stroke {
background: linear-gradient(90deg, #60a5fa 0%, #2563eb 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* 创意切换开关样式 */
.theme-switch-wrapper {
@apply inline-flex items-center;
}
.theme-switch {
@apply relative;
width: 76px;
height: 38px;
border-radius: 50px;
padding: 4px;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 2px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 4px 15px rgba(102, 126, 234, 0.3),
inset 0 1px 2px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
align-items: center;
}
.theme-switch:hover {
transform: scale(1.05);
box-shadow:
0 6px 20px rgba(102, 126, 234, 0.4),
inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
.theme-switch:active {
transform: scale(0.98);
}
/* 深色模式样式 */
.theme-switch.is-dark {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
border-color: rgba(148, 163, 184, 0.2);
box-shadow:
0 4px 15px rgba(0, 0, 0, 0.5),
inset 0 1px 2px rgba(255, 255, 255, 0.05);
}
.theme-switch.is-dark:hover {
box-shadow:
0 6px 20px rgba(0, 0, 0, 0.6),
inset 0 1px 2px rgba(255, 255, 255, 0.05);
}
/* 自动模式样式 - 静态蓝紫渐变设计(优化版) */
.theme-switch.is-auto {
background: linear-gradient(
135deg,
#c4b5fd 0%,
/* 更柔和的起始:淡紫 */ #a78bfa 15%,
/* 浅紫 */ #818cf8 40%,
/* 紫蓝 */ #6366f1 60%,
/* 靛蓝 */ #4f46e5 85%,
/* 深蓝紫 */ #4338ca 100% /* 更深的结束:深紫 */
);
border-color: rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
background-size: 120% 120%;
background-position: center;
box-shadow:
0 4px 15px rgba(139, 92, 246, 0.25),
inset 0 1px 3px rgba(0, 0, 0, 0.1),
inset 0 -1px 3px rgba(0, 0, 0, 0.1);
}
/* 自动模式的分割线效果 */
.theme-switch.is-auto::before {
content: '';
position: absolute;
left: 50%;
top: 10%;
bottom: 10%;
width: 1px;
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.3), transparent);
transform: translateX(-50%);
pointer-events: none;
}
/* 背景装饰 */
.switch-bg {
position: absolute;
inset: 0;
border-radius: inherit;
overflow: hidden;
pointer-events: none;
}
/* 星星装饰(深色模式) */
.stars {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.4s ease;
}
.theme-switch.is-dark .stars {
opacity: 1;
}
.stars span {
position: absolute;
display: block;
width: 2px;
height: 2px;
background: white;
border-radius: 50%;
box-shadow: 0 0 2px white;
animation: twinkle 3s infinite;
}
.stars span:nth-child(1) {
top: 25%;
left: 20%;
animation-delay: 0s;
}
.stars span:nth-child(2) {
top: 40%;
left: 40%;
animation-delay: 1s;
}
.stars span:nth-child(3) {
top: 60%;
left: 25%;
animation-delay: 2s;
}
@keyframes twinkle {
0%,
100% {
opacity: 0;
transform: scale(0.5);
}
50% {
opacity: 1;
transform: scale(1);
}
}
/* 云朵装饰(浅色模式) */
.clouds {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.4s ease;
}
.theme-switch:not(.is-dark):not(.is-auto) .clouds {
opacity: 1;
}
.clouds span {
position: absolute;
background: rgba(255, 255, 255, 0.4);
border-radius: 100px;
}
.clouds span:nth-child(1) {
width: 20px;
height: 8px;
top: 40%;
left: 15%;
animation: float 4s infinite ease-in-out;
}
.clouds span:nth-child(2) {
width: 15px;
height: 6px;
top: 60%;
left: 35%;
animation: float 4s infinite ease-in-out;
animation-delay: 1s;
}
@keyframes float {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(5px);
}
}
/* 切换滑块 */
.switch-handle {
position: absolute;
width: 30px;
height: 30px;
background: white;
border-radius: 50%;
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
justify-content: center;
top: 50%;
transform: translateY(-50%) translateX(0);
left: 4px;
}
/* 深色模式滑块位置 */
.theme-switch.is-dark .switch-handle {
transform: translateY(-50%) translateX(38px);
background: linear-gradient(135deg, #1e293b 0%, #475569 100%);
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
/* 自动模式滑块位置 - 玻璃态设计 */
.theme-switch.is-auto .switch-handle {
transform: translateY(-50%) translateX(19px);
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.1),
inset 0 0 8px rgba(255, 255, 255, 0.2);
}
/* 滑块图标 */
.handle-icon {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.handle-icon i {
font-size: 14px;
transition: all 0.3s ease;
}
.handle-icon .fa-sun {
color: #f59e0b;
filter: drop-shadow(0 0 3px rgba(245, 158, 11, 0.5));
}
.handle-icon .fa-moon {
color: #fbbf24;
filter: drop-shadow(0 0 3px rgba(251, 191, 36, 0.5));
}
.handle-icon .fa-circle-half-stroke {
color: rgba(255, 255, 255, 0.9);
filter: drop-shadow(0 0 4px rgba(167, 139, 250, 0.5));
font-size: 15px;
}
/* 滑块悬停动画 */
.theme-switch:hover .switch-handle {
animation: bounce 0.5s ease;
}
@keyframes bounce {
0%,
100% {
transform: translateY(-50%) translateX(var(--handle-x, 0));
}
50% {
transform: translateY(calc(-50% - 3px)) translateX(var(--handle-x, 0));
}
}
.theme-switch.is-dark:hover .switch-handle {
--handle-x: 38px;
}
.theme-switch.is-auto:hover .switch-handle {
--handle-x: 19px;
}
/* 分段按钮样式 - 更现代 */
.theme-segmented {
@apply inline-flex;
@apply bg-gray-100 dark:bg-gray-800;
@apply rounded-full p-1;
@apply border border-gray-200 dark:border-gray-700;
@apply shadow-sm;
}
.theme-segment {
@apply px-3 py-1.5;
@apply text-xs font-medium;
@apply text-gray-500 dark:text-gray-400;
@apply transition-all duration-200;
@apply rounded-full;
@apply flex items-center gap-1;
@apply cursor-pointer;
position: relative;
}
.theme-segment:hover {
@apply text-gray-700 dark:text-gray-300;
@apply bg-white/30 dark:bg-gray-600/30;
transform: scale(1.02);
}
.theme-segment.active {
@apply bg-white dark:bg-gray-700;
@apply text-gray-900 dark:text-white;
@apply shadow-sm;
}
.theme-segment i {
@apply text-xs;
transition: transform 0.2s ease;
}
/* 过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.dropdown-enter-active {
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dropdown-leave-active {
transition: all 0.2s cubic-bezier(0.4, 0, 1, 1);
}
.dropdown-enter-from {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
.dropdown-leave-to {
opacity: 0;
transform: translateY(-5px) scale(0.98);
}
/* 响应式调整 */
@media (max-width: 640px) {
.theme-dropdown {
@apply left-0 right-auto;
}
.theme-segment span {
@apply hidden;
}
}
</style>

View File

@@ -162,6 +162,12 @@ defineExpose({
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
:global(.dark) .toast {
background: #1f2937;
border: 1px solid #374151;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.toast-show {
transform: translateX(0);
opacity: 1;
@@ -227,6 +233,11 @@ defineExpose({
color: #6b7280;
}
:global(.dark) .toast-close:hover {
background: #374151;
color: #9ca3af;
}
.toast-progress {
position: absolute;
bottom: 0;
@@ -256,14 +267,26 @@ defineExpose({
background: #d1fae5;
}
:global(.dark) .toast-success .toast-icon {
background: #064e3b;
}
.toast-success .toast-title {
color: #065f46;
}
:global(.dark) .toast-success .toast-title {
color: #10b981;
}
.toast-success .toast-message {
color: #047857;
}
:global(.dark) .toast-success .toast-message {
color: #34d399;
}
.toast-success .toast-progress {
background: #10b981;
}
@@ -278,14 +301,26 @@ defineExpose({
background: #fee2e2;
}
:global(.dark) .toast-error .toast-icon {
background: #7f1d1d;
}
.toast-error .toast-title {
color: #991b1b;
}
:global(.dark) .toast-error .toast-title {
color: #ef4444;
}
.toast-error .toast-message {
color: #dc2626;
}
:global(.dark) .toast-error .toast-message {
color: #f87171;
}
.toast-error .toast-progress {
background: #ef4444;
}
@@ -300,14 +335,26 @@ defineExpose({
background: #fef3c7;
}
:global(.dark) .toast-warning .toast-icon {
background: #78350f;
}
.toast-warning .toast-title {
color: #92400e;
}
:global(.dark) .toast-warning .toast-title {
color: #f59e0b;
}
.toast-warning .toast-message {
color: #d97706;
}
:global(.dark) .toast-warning .toast-message {
color: #fbbf24;
}
.toast-warning .toast-progress {
background: #f59e0b;
}
@@ -322,14 +369,26 @@ defineExpose({
background: #dbeafe;
}
:global(.dark) .toast-info .toast-icon {
background: #1e3a8a;
}
.toast-info .toast-title {
color: #1e40af;
}
:global(.dark) .toast-info .toast-title {
color: #3b82f6;
}
.toast-info .toast-message {
color: #2563eb;
}
:global(.dark) .toast-info .toast-message {
color: #60a5fa;
}
.toast-info .toast-progress {
background: #3b82f6;
}