mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 支持Dark Mode
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
573
web/admin-spa/src/components/common/ThemeToggle.vue
Normal file
573
web/admin-spa/src/components/common/ThemeToggle.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user