开始主题设置
This commit is contained in:
@@ -17,7 +17,7 @@ export default defineAppConfig({
|
|||||||
siteName: 'Nuxt Docs Template'
|
siteName: 'Nuxt Docs Template'
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
title: '',
|
title: 'Estel Docs',
|
||||||
to: '/',
|
to: '/',
|
||||||
logo: {
|
logo: {
|
||||||
alt: '',
|
alt: '',
|
||||||
|
@@ -1,65 +1,72 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ContentNavigationItem } from '@nuxt/content'
|
const isSettingsOpen = ref(false)
|
||||||
|
|
||||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
const { header } = useAppConfig();
|
||||||
|
|
||||||
const { header } = useAppConfig()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UHeader
|
<UPage>
|
||||||
>
|
<template>
|
||||||
|
<header
|
||||||
|
class="header bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 sticky top-0 z-50"
|
||||||
|
>
|
||||||
|
<div class="px-2 sm:px-4 lg:px-6">
|
||||||
|
<div class="flex justify-between items-center h-12">
|
||||||
|
<UContentSearchButton v-if="header?.search" class="lg:hidden" />
|
||||||
|
<div class="ml-auto">
|
||||||
|
<UColorModeButton v-if="header?.colorMode" />
|
||||||
|
|
||||||
<template
|
<!-- Settings Button -->
|
||||||
v-if="header?.logo?.dark || header?.logo?.light || header?.title"
|
<button
|
||||||
#title
|
class="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||||
>
|
title="页面设置"
|
||||||
<UColorModeImage
|
@click="isSettingsOpen = !isSettingsOpen"
|
||||||
v-if="header?.logo?.dark || header?.logo?.light"
|
>
|
||||||
:light="header?.logo?.light!"
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
:dark="header?.logo?.dark!"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
:alt="header?.logo?.alt"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
class="h-6 w-auto shrink-0"
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- User Actions - Mobile and Desktop -->
|
||||||
|
<div class="hidden sm:flex items-center space-x-2">
|
||||||
|
<NuxtLink
|
||||||
|
to="/login"
|
||||||
|
class="px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/register"
|
||||||
|
class="text-sm font-medium text-white bg-primary rounded-md"
|
||||||
|
>
|
||||||
|
<UButton icon="lucide-rocket">注册</UButton>
|
||||||
|
</NuxtLink>
|
||||||
|
<!-- Mobile Menu Button -->
|
||||||
|
<div class="sm:hidden">
|
||||||
|
<NuxtLink
|
||||||
|
to="/login"
|
||||||
|
class="px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/register"
|
||||||
|
class="ml-2 px-3 py-2 text-sm font-medium text-white bg-primary rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-colors"
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Theme Settings Panel -->
|
||||||
|
<ThemeSettings
|
||||||
|
:is-open="isSettingsOpen"
|
||||||
|
@close="isSettingsOpen = false"
|
||||||
/>
|
/>
|
||||||
|
</header>
|
||||||
<span v-else-if="header?.title">
|
|
||||||
{{ header.title }}
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
</UPage>
|
||||||
<template
|
|
||||||
v-else
|
|
||||||
#left
|
|
||||||
>
|
|
||||||
<NuxtLink :to="header?.to || '/'">
|
|
||||||
<LogoPro class="w-auto h-6 shrink-0" />
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<TemplateMenu />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #right>
|
|
||||||
<UContentSearchButton
|
|
||||||
v-if="header?.search"
|
|
||||||
class="lg:hidden"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UColorModeButton v-if="header?.colorMode" />
|
|
||||||
|
|
||||||
<template v-if="header?.links">
|
|
||||||
<UButton
|
|
||||||
v-for="(link, index) of header.links"
|
|
||||||
:key="index"
|
|
||||||
v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<UContentNavigation
|
|
||||||
highlight
|
|
||||||
:navigation="navigation"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</UHeader>
|
|
||||||
</template>
|
</template>
|
||||||
|
223
app/components/ThemeSettings.vue
Normal file
223
app/components/ThemeSettings.vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 设置面板 -->
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-transform duration-300 ease-out"
|
||||||
|
enter-from-class="translate-x-full"
|
||||||
|
enter-to-class="translate-x-0"
|
||||||
|
leave-active-class="transition-transform duration-300 ease-in"
|
||||||
|
leave-from-class="translate-x-0"
|
||||||
|
leave-to-class="translate-x-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
class="fixed right-0 top-16 h-[calc(100vh-4rem)] w-full sm:w-96 bg-white dark:bg-gray-900 shadow-2xl z-40 overflow-y-auto border-l border-gray-200 dark:border-gray-700 custom-scrollbar"
|
||||||
|
>
|
||||||
|
<!-- 头部 -->
|
||||||
|
<div class="sticky top-0 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 px-6 py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">页面设置</h2>
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-lucide-x"
|
||||||
|
square
|
||||||
|
@click="$emit('close')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置内容 -->
|
||||||
|
<div class="p-6 space-y-8">
|
||||||
|
<!-- 主题 -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">主题</h3>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<UButton
|
||||||
|
v-for="theme in themes"
|
||||||
|
:key="theme.value"
|
||||||
|
:label="theme.label"
|
||||||
|
:color="selectedTheme === theme.value ? 'primary' : 'neutral'"
|
||||||
|
:variant="selectedTheme === theme.value ? 'solid' : 'outline'"
|
||||||
|
size="sm"
|
||||||
|
class="justify-center"
|
||||||
|
@click="selectedTheme = theme.value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 字体 -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">字体</h3>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<UButton
|
||||||
|
v-for="font in fonts"
|
||||||
|
:key="font.value"
|
||||||
|
:label="font.label"
|
||||||
|
:color="selectedFont === font.value ? 'primary' : 'neutral'"
|
||||||
|
:variant="selectedFont === font.value ? 'solid' : 'outline'"
|
||||||
|
size="sm"
|
||||||
|
class="justify-center"
|
||||||
|
@click="selectedFont = font.value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 字号 -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">字号</h3>
|
||||||
|
<div class="grid grid-cols-5 gap-2">
|
||||||
|
<UButton
|
||||||
|
v-for="size in fontSizes"
|
||||||
|
:key="size.value"
|
||||||
|
:label="size.label"
|
||||||
|
:color="selectedFontSize === size.value ? 'primary' : 'neutral'"
|
||||||
|
:variant="selectedFontSize === size.value ? 'solid' : 'outline'"
|
||||||
|
size="xs"
|
||||||
|
class="justify-center"
|
||||||
|
@click="selectedFontSize = size.value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主题色 -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">主题色</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<UButton
|
||||||
|
v-for="color in themeColors"
|
||||||
|
:key="color.value"
|
||||||
|
:color="selectedThemeColor === color.value ? 'primary' : 'neutral'"
|
||||||
|
:variant="selectedThemeColor === color.value ? 'solid' : 'outline'"
|
||||||
|
size="sm"
|
||||||
|
class="justify-start"
|
||||||
|
@click="selectedThemeColor = color.value"
|
||||||
|
>
|
||||||
|
<template #leading>
|
||||||
|
<div
|
||||||
|
class="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600"
|
||||||
|
:style="{ backgroundColor: color.color }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span>{{ color.label }}</span>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义主题色 -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">自定义主题色</h3>
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<input
|
||||||
|
v-model="customColor"
|
||||||
|
type="color"
|
||||||
|
class="w-12 h-10 rounded-md border border-gray-300 dark:border-gray-600 cursor-pointer"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
label="应用"
|
||||||
|
color="primary"
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
@click="applyCustomColor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 代码块主题 -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">代码块主题</h3>
|
||||||
|
<USelect
|
||||||
|
v-model="selectedCodeTheme"
|
||||||
|
:options="codeThemes"
|
||||||
|
option-attribute="label"
|
||||||
|
value-attribute="value"
|
||||||
|
placeholder="选择代码主题"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图注格式 -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">图注格式</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<UButton
|
||||||
|
v-for="format in captionFormats"
|
||||||
|
:key="format.value"
|
||||||
|
:label="format.label"
|
||||||
|
:color="selectedCaptionFormat === format.value ? 'primary' : 'neutral'"
|
||||||
|
:variant="selectedCaptionFormat === format.value ? 'solid' : 'outline'"
|
||||||
|
size="sm"
|
||||||
|
class="justify-center"
|
||||||
|
@click="selectedCaptionFormat = format.value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<UButton
|
||||||
|
label="不显示"
|
||||||
|
:color="selectedCaptionFormat === 'none' ? 'primary' : 'neutral'"
|
||||||
|
:variant="selectedCaptionFormat === 'none' ? 'solid' : 'outline'"
|
||||||
|
size="sm"
|
||||||
|
class="w-full justify-center"
|
||||||
|
@click="selectedCaptionFormat = 'none'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 重置按钮 -->
|
||||||
|
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<UButton
|
||||||
|
label="重置为默认设置"
|
||||||
|
color="neutral"
|
||||||
|
variant="soft"
|
||||||
|
size="sm"
|
||||||
|
class="w-full justify-center"
|
||||||
|
@click="resetSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface ThemeSettingsProps {
|
||||||
|
isOpen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<ThemeSettingsProps>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
themes,
|
||||||
|
fonts,
|
||||||
|
fontSizes,
|
||||||
|
themeColors,
|
||||||
|
codeThemes,
|
||||||
|
captionFormats,
|
||||||
|
selectedTheme,
|
||||||
|
selectedFont,
|
||||||
|
selectedFontSize,
|
||||||
|
selectedThemeColor,
|
||||||
|
customColor,
|
||||||
|
selectedCodeTheme,
|
||||||
|
selectedCaptionFormat,
|
||||||
|
applyCustomColor,
|
||||||
|
resetSettings
|
||||||
|
} = useTheme();
|
||||||
|
|
||||||
|
// 监听键盘事件(只监听 ESC 键)
|
||||||
|
onMounted(() => {
|
||||||
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeydown)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
274
app/composables/useTheme.ts
Normal file
274
app/composables/useTheme.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
/**
|
||||||
|
* @file app/composables/useTheme.ts
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* `useTheme` 是一个全局的主题管理"管家"(Composable)。它采用单例模式,确保在整个应用中只有一份主题状态。
|
||||||
|
* 这个模块负责:
|
||||||
|
* 1. **状态管理**:集中管理所有与主题相关的响应式状态,如主题模式(经典、优雅、简洁)、主题色、字体、字号等。
|
||||||
|
* 2. **持久化**:将用户的设置保存到 localStorage,以便在下次访问时恢复。
|
||||||
|
* 3. **应用样式**:动态地将主题设置应用到 `<html>` 根元素上,通过 CSS 变量和类名来控制全局样式。
|
||||||
|
* 4. **提供接口**:向外暴露所有状态和方法,方便任何组件读取和修改主题设置。
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* @usage
|
||||||
|
*
|
||||||
|
* **自动导入**:
|
||||||
|
* 由于这个文件位于 `composables/` 目录下,Nuxt 会自动导入 `useTheme` 函数。
|
||||||
|
* 你可以在任何 `.vue` 组件的 `<script setup>` 中直接使用,无需手动 `import`。
|
||||||
|
*
|
||||||
|
* **初始化**:
|
||||||
|
* 应用启动时,`plugins/theme.client.ts` 插件会自动调用 `initializeTheme()` 方法,
|
||||||
|
* 从 localStorage 加载用户设置并应用。
|
||||||
|
*
|
||||||
|
* **在组件中使用**:
|
||||||
|
* ```vue
|
||||||
|
* <script setup lang="ts">
|
||||||
|
* // 直接解构获取需要的数据和方法
|
||||||
|
* const { selectedTheme, selectedThemeColor, resetSettings } = useTheme();
|
||||||
|
*
|
||||||
|
* function handleReset() {
|
||||||
|
* // 调用方法来修改主题
|
||||||
|
* resetSettings();
|
||||||
|
* }
|
||||||
|
* </script>
|
||||||
|
*
|
||||||
|
* <template>
|
||||||
|
* <div>
|
||||||
|
* <p>当前主题: {{ selectedTheme }}</p>
|
||||||
|
* <button @click="handleReset">重置设置</button>
|
||||||
|
* </div>
|
||||||
|
* </template>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
// 定义各种选项
|
||||||
|
const themes = [
|
||||||
|
{ label: '经典', value: 'classic' },
|
||||||
|
{ label: '优雅', value: 'elegant' },
|
||||||
|
{ label: '简洁', value: 'minimal' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const fonts = [
|
||||||
|
{ label: '无衬线', value: 'sans-serif' },
|
||||||
|
{ label: '衬线', value: 'serif' },
|
||||||
|
{ label: '等宽', value: 'monospace' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const fontSizes = [
|
||||||
|
{ label: '更小', value: 'xs' },
|
||||||
|
{ label: '稍小', value: 'sm' },
|
||||||
|
{ label: '推荐', value: 'base' },
|
||||||
|
{ label: '稍大', value: 'lg' },
|
||||||
|
{ label: '更大', value: 'xl' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const themeColors = [
|
||||||
|
{ label: '经典蓝', value: 'blue', color: '#3B82F6' },
|
||||||
|
{ label: '翡翠绿', value: 'emerald', color: '#10B981' },
|
||||||
|
{ label: '活力橙', value: 'orange', color: '#F97316' },
|
||||||
|
{ label: '柠檬黄', value: 'yellow', color: '#EAB308' },
|
||||||
|
{ label: '薰衣紫', value: 'violet', color: '#8B5CF6' },
|
||||||
|
{ label: '天空蓝', value: 'sky', color: '#0EA5E9' },
|
||||||
|
{ label: '玫瑰金', value: 'rose', color: '#F43F5E' },
|
||||||
|
{ label: '橄榄绿', value: 'lime', color: '#84CC16' },
|
||||||
|
{ label: '石墨黑', value: 'gray', color: '#6B7280' },
|
||||||
|
{ label: '雾烟灰', value: 'slate', color: '#64748B' },
|
||||||
|
{ label: '樱花粉', value: 'pink', color: '#EC4899' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const codeThemes = [
|
||||||
|
{ label: 'dark', value: 'dark' },
|
||||||
|
{ label: 'light', value: 'light' },
|
||||||
|
{ label: 'github-dark', value: 'github-dark' },
|
||||||
|
{ label: 'github-light', value: 'github-light' },
|
||||||
|
{ label: 'one-dark-pro', value: 'one-dark-pro' },
|
||||||
|
{ label: 'material-theme', value: 'material-theme' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const captionFormats = [
|
||||||
|
{ label: 'title 优先', value: 'title' },
|
||||||
|
{ label: 'alt 优先', value: 'alt' },
|
||||||
|
{ label: '只显示 title', value: 'title-only' },
|
||||||
|
{ label: '只显示 alt', value: 'alt-only' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 默认设置
|
||||||
|
const defaultSettings = {
|
||||||
|
theme: 'elegant',
|
||||||
|
font: 'sans-serif',
|
||||||
|
fontSize: 'base',
|
||||||
|
themeColor: 'blue',
|
||||||
|
codeTheme: 'dark',
|
||||||
|
captionFormat: 'none'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 响应式状态
|
||||||
|
const selectedTheme = ref<string>(defaultSettings.theme);
|
||||||
|
const selectedFont = ref<string>(defaultSettings.font);
|
||||||
|
const selectedFontSize = ref<string>(defaultSettings.fontSize);
|
||||||
|
const selectedThemeColor = ref<string>(defaultSettings.themeColor);
|
||||||
|
const customColor = ref<string>('#3B82F6');
|
||||||
|
const selectedCodeTheme = ref<string>(defaultSettings.codeTheme);
|
||||||
|
const selectedCaptionFormat = ref<string>(defaultSettings.captionFormat);
|
||||||
|
|
||||||
|
// 这是一个单例,确保在整个应用中只有一个主题状态实例
|
||||||
|
let isInitialized = false;
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const colorMode = useColorMode();
|
||||||
|
|
||||||
|
const initializeTheme = () => {
|
||||||
|
if (import.meta.client && !isInitialized) {
|
||||||
|
selectedTheme.value = localStorage.getItem('app-theme') || defaultSettings.theme;
|
||||||
|
selectedFont.value = localStorage.getItem('app-font') || defaultSettings.font;
|
||||||
|
selectedFontSize.value = localStorage.getItem('app-font-size') || defaultSettings.fontSize;
|
||||||
|
selectedThemeColor.value = localStorage.getItem('app-theme-color') || defaultSettings.themeColor;
|
||||||
|
selectedCodeTheme.value = localStorage.getItem('app-code-theme') || defaultSettings.codeTheme;
|
||||||
|
selectedCaptionFormat.value = localStorage.getItem('app-caption-format') || defaultSettings.captionFormat;
|
||||||
|
|
||||||
|
// 找到自定义颜色对应的主题色值
|
||||||
|
const initialColor = themeColors.find(c => c.value === selectedThemeColor.value)?.color || '#3B82F6';
|
||||||
|
customColor.value = localStorage.getItem('app-custom-color') || initialColor;
|
||||||
|
|
||||||
|
applyThemeVariables();
|
||||||
|
isInitialized = true;
|
||||||
|
console.log('主题已初始化');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyThemeVariables = () => {
|
||||||
|
if (import.meta.client) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
// 颜色映射到 @nuxt/ui 支持的颜色
|
||||||
|
const colorMapping: { [key: string]: string } = {
|
||||||
|
'blue': 'blue',
|
||||||
|
'emerald': 'emerald',
|
||||||
|
'orange': 'orange',
|
||||||
|
'yellow': 'yellow',
|
||||||
|
'violet': 'violet',
|
||||||
|
'sky': 'sky',
|
||||||
|
'rose': 'rose',
|
||||||
|
'lime': 'lime',
|
||||||
|
'gray': 'gray',
|
||||||
|
'slate': 'slate',
|
||||||
|
'pink': 'pink'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用 @nuxt/ui 的主题系统
|
||||||
|
if (selectedThemeColor.value !== 'custom') {
|
||||||
|
const mappedColor = colorMapping[selectedThemeColor.value];
|
||||||
|
if (mappedColor) {
|
||||||
|
const appConfig = useAppConfig();
|
||||||
|
if (!appConfig.ui.colors) appConfig.ui.colors = {};
|
||||||
|
appConfig.ui.colors.primary = mappedColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字体设置保持不变
|
||||||
|
const fontMapping = {
|
||||||
|
'sans-serif': 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
|
||||||
|
'serif': 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
|
||||||
|
'monospace': 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace'
|
||||||
|
};
|
||||||
|
root.style.setProperty('--font-family', fontMapping[selectedFont.value as keyof typeof fontMapping]);
|
||||||
|
|
||||||
|
const fontSizeMapping = {
|
||||||
|
'xs': '0.75rem', 'sm': '0.875rem', 'base': '1rem', 'lg': '1.125rem', 'xl': '1.25rem'
|
||||||
|
};
|
||||||
|
root.style.setProperty('--font-size-base', fontSizeMapping[selectedFontSize.value as keyof typeof fontSizeMapping]);
|
||||||
|
|
||||||
|
root.classList.remove('theme-classic', 'theme-elegant', 'theme-minimal');
|
||||||
|
root.classList.add(`theme-${selectedTheme.value}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用自定义主色调
|
||||||
|
*
|
||||||
|
* Nuxt UI v3 不支持运行时动态自定义颜色,因此我们使用 CSS 变量覆盖的方法:
|
||||||
|
* 1. 保持 Nuxt UI 使用有效的颜色系统(这里使用 'blue' 作为基础)
|
||||||
|
* 2. 通过 CSS 变量 --ui-primary 覆盖实际显示的颜色
|
||||||
|
* 3. 这样既保证了 Nuxt UI 系统正常工作,又实现了自定义颜色功能
|
||||||
|
*/
|
||||||
|
const applyCustomColor = () => {
|
||||||
|
if (import.meta.client) {
|
||||||
|
selectedThemeColor.value = 'custom';
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
// 使用 Nuxt UI v3 推荐的 CSS 变量覆盖方法
|
||||||
|
// 设置主色调的 CSS 变量,这会影响所有使用 primary 颜色的组件
|
||||||
|
root.style.setProperty('--ui-primary', customColor.value);
|
||||||
|
|
||||||
|
// 保持 Nuxt UI 使用有效的颜色系统
|
||||||
|
const appConfig = useAppConfig();
|
||||||
|
if (!appConfig.ui.colors) appConfig.ui.colors = {};
|
||||||
|
appConfig.ui.colors.primary = 'blue'; // 使用有效的颜色名称作为基础
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSettings = () => {
|
||||||
|
selectedTheme.value = defaultSettings.theme;
|
||||||
|
selectedFont.value = defaultSettings.font;
|
||||||
|
selectedFontSize.value = defaultSettings.fontSize;
|
||||||
|
selectedThemeColor.value = defaultSettings.themeColor;
|
||||||
|
const defaultColor = themeColors.find(c => c.value === defaultSettings.themeColor)?.color || '#3B82F6';
|
||||||
|
customColor.value = defaultColor;
|
||||||
|
selectedCodeTheme.value = defaultSettings.codeTheme;
|
||||||
|
selectedCaptionFormat.value = defaultSettings.captionFormat;
|
||||||
|
|
||||||
|
colorMode.preference = 'system';
|
||||||
|
|
||||||
|
applyThemeVariables();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听预设主题色的变化,并同步更新自定义颜色选择器的值
|
||||||
|
watch(selectedThemeColor, (newThemeColor) => {
|
||||||
|
if (newThemeColor !== 'custom') {
|
||||||
|
const colorObject = themeColors.find(c => c.value === newThemeColor);
|
||||||
|
if (colorObject) {
|
||||||
|
customColor.value = colorObject.color;
|
||||||
|
//应用颜色
|
||||||
|
applyCustomColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([selectedTheme, selectedFont, selectedFontSize, selectedThemeColor, selectedCodeTheme, selectedCaptionFormat, customColor], () => {
|
||||||
|
applyThemeVariables();
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
localStorage.setItem('app-theme', selectedTheme.value);
|
||||||
|
localStorage.setItem('app-font', selectedFont.value);
|
||||||
|
localStorage.setItem('app-font-size', selectedFontSize.value);
|
||||||
|
localStorage.setItem('app-theme-color', selectedThemeColor.value);
|
||||||
|
localStorage.setItem('app-code-theme', selectedCodeTheme.value);
|
||||||
|
localStorage.setItem('app-caption-format', selectedCaptionFormat.value);
|
||||||
|
if (selectedThemeColor.value === 'custom') {
|
||||||
|
localStorage.setItem('app-custom-color', customColor.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
initializeTheme,
|
||||||
|
themes,
|
||||||
|
fonts,
|
||||||
|
fontSizes,
|
||||||
|
themeColors,
|
||||||
|
codeThemes,
|
||||||
|
captionFormats,
|
||||||
|
selectedTheme,
|
||||||
|
selectedFont,
|
||||||
|
selectedFontSize,
|
||||||
|
selectedThemeColor,
|
||||||
|
customColor,
|
||||||
|
selectedCodeTheme,
|
||||||
|
selectedCaptionFormat,
|
||||||
|
applyCustomColor,
|
||||||
|
resetSettings,
|
||||||
|
colorMode
|
||||||
|
};
|
||||||
|
}
|
@@ -1,5 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex">
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex">
|
||||||
|
<div
|
||||||
|
v-if="isSidebarOpen"
|
||||||
|
class="fixed inset-0 bg-gray-900/50 z-40 lg:hidden"
|
||||||
|
@click="isSidebarOpen = false"
|
||||||
|
/>
|
||||||
|
<AppSidebar
|
||||||
|
class="fixed top-0 bottom-0 z-50 transition-transform duration-300 ease-in-out"
|
||||||
|
:class="isSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'"
|
||||||
|
/>
|
||||||
<!-- Mobile Sidebar (Drawer) -->
|
<!-- Mobile Sidebar (Drawer) -->
|
||||||
<div
|
<div
|
||||||
v-if="isSidebarOpen"
|
v-if="isSidebarOpen"
|
||||||
@@ -14,11 +23,7 @@
|
|||||||
<!-- Right Content Area -->
|
<!-- Right Content Area -->
|
||||||
<div class="flex-1 lg:ml-64 flex flex-col">
|
<div class="flex-1 lg:ml-64 flex flex-col">
|
||||||
<!-- Fixed Header -->
|
<!-- Fixed Header -->
|
||||||
<AppHeader
|
<AppHeader />
|
||||||
class="fixed top-0 right-0 z-30 transition-all duration-300"
|
|
||||||
:class="{ 'left-0': !isSidebarOpen, 'lg:left-64': true }"
|
|
||||||
@toggle-sidebar="isSidebarOpen = !isSidebarOpen"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="flex-1 overflow-y-auto pt-16">
|
<main class="flex-1 overflow-y-auto pt-16">
|
||||||
|
96
app/pages/login.vue
Normal file
96
app/pages/login.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||||
|
登录您的账户
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
或
|
||||||
|
<NuxtLink to="/register" class="font-medium text-blue-600 hover:text-blue-500">
|
||||||
|
创建新账户
|
||||||
|
</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form class="mt-8 space-y-6" @submit.prevent="handleLogin">
|
||||||
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="sr-only">邮箱地址</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="邮箱地址"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="sr-only">密码</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="密码"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="remember-me"
|
||||||
|
v-model="rememberMe"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
>
|
||||||
|
<label for="remember-me" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
||||||
|
记住我
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm">
|
||||||
|
<a href="#" class="font-medium text-blue-600 hover:text-blue-500">
|
||||||
|
忘记密码?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||||
|
<svg class="h-5 w-5 text-blue-500 group-hover:text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const rememberMe = ref(false)
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
// TODO: Implement actual login logic
|
||||||
|
console.log('Login attempt:', { email: email.value, password: password.value, rememberMe: rememberMe.value })
|
||||||
|
|
||||||
|
// For demo purposes, redirect to home
|
||||||
|
navigateTo('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: '登录 - Easy Docs',
|
||||||
|
description: '登录您的 Easy Docs 账户'
|
||||||
|
})
|
||||||
|
</script>
|
117
app/pages/register.vue
Normal file
117
app/pages/register.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||||
|
创建新账户
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
或
|
||||||
|
<NuxtLink to="/login" class="font-medium text-blue-600 hover:text-blue-500">
|
||||||
|
登录现有账户
|
||||||
|
</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form class="mt-8 space-y-6" @submit.prevent="handleRegister">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-300">用户名</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="用户名"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">邮箱地址</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="邮箱地址"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">密码</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="密码"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="confirm-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">确认密码</label>
|
||||||
|
<input
|
||||||
|
id="confirm-password"
|
||||||
|
v-model="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="确认密码"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="agree-terms"
|
||||||
|
v-model="agreeTerms"
|
||||||
|
type="checkbox"
|
||||||
|
required
|
||||||
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
>
|
||||||
|
<label for="agree-terms" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
||||||
|
我同意
|
||||||
|
<a href="#" class="font-medium text-blue-600 hover:text-blue-500">服务条款</a>
|
||||||
|
和
|
||||||
|
<a href="#" class="font-medium text-blue-600 hover:text-blue-500">隐私政策</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
创建账户
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const username = ref('')
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
const agreeTerms = ref(false)
|
||||||
|
|
||||||
|
const handleRegister = () => {
|
||||||
|
// TODO: Implement actual registration logic
|
||||||
|
console.log('Registration attempt:', {
|
||||||
|
username: username.value,
|
||||||
|
email: email.value,
|
||||||
|
password: password.value,
|
||||||
|
confirmPassword: confirmPassword.value,
|
||||||
|
agreeTerms: agreeTerms.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// For demo purposes, redirect to home
|
||||||
|
navigateTo('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: '注册 - Easy Docs',
|
||||||
|
description: '创建您的 Easy Docs 账户'
|
||||||
|
})
|
||||||
|
</script>
|
Reference in New Issue
Block a user