lint:fix all

This commit is contained in:
2025-07-29 00:32:57 +08:00
parent 7d2f57df97
commit 1745a54eb6
34 changed files with 820 additions and 606 deletions

100
.github/nuxt.config.ts vendored
View File

@@ -1,11 +1,10 @@
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url'
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite'
const currentDir = dirname(fileURLToPath(import.meta.url)); const currentDir = dirname(fileURLToPath(import.meta.url))
export default defineNuxtConfig({ export default defineNuxtConfig({
devtools: { enabled: true },
modules: [ modules: [
'shadcn-nuxt', 'shadcn-nuxt',
'@vueuse/nuxt', '@vueuse/nuxt',
@@ -16,42 +15,33 @@ export default defineNuxtConfig({
'nuxt-og-image', 'nuxt-og-image',
'@nuxt/scripts', '@nuxt/scripts',
'@nuxtjs/i18n', '@nuxtjs/i18n',
'@nuxt/fonts', '@nuxt/fonts'
], ],
shadcn: {
prefix: 'Ui',
componentDir: join(currentDir, './components/ui'),
},
components: { components: {
dirs: [ dirs: [
{ {
path: './components', path: './components',
ignore: ['**/*.ts'], ignore: ['**/*.ts']
}, }
], ]
},
i18n: {
bundle: {
optimizeTranslationDirective: false,
},
strategy: 'prefix_except_default',
},
colorMode: {
classSuffix: '',
disableTransition: true,
}, },
devtools: { enabled: true },
css: [ css: [
join(currentDir, './assets/css/themes.css'), join(currentDir, './assets/css/themes.css'),
'~/assets/css/tailwind.css', '~/assets/css/tailwind.css'
], ],
colorMode: {
classSuffix: '',
disableTransition: true
},
content: { content: {
documentDriven: true, documentDriven: true,
highlight: { highlight: {
theme: { theme: {
default: 'github-light', default: 'github-light',
dark: 'github-dark', dark: 'github-dark'
}, },
preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'mdc', 'yaml', 'bash', 'ini', 'dotenv'], preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'mdc', 'yaml', 'bash', 'ini', 'dotenv']
}, },
navigation: { navigation: {
fields: [ fields: [
@@ -65,40 +55,50 @@ export default defineNuxtConfig({
'editLink', 'editLink',
'prevNext', 'prevNext',
'breadcrumb', 'breadcrumb',
'fullpage', 'fullpage'
], ]
}, },
experimental: { experimental: {
search: { search: {
indexed: true, indexed: true
}, }
}, }
},
icon: {
clientBundle: {
scan: true,
sizeLimitKb: 512,
},
},
fonts: {
defaults: {
weights: ['300 800'],
}, },
compatibilityDate: '2025-05-13',
vite: {
plugins: [
tailwindcss()
],
optimizeDeps: {
include: ['debug']
}
}, },
typescript: { typescript: {
tsConfig: { tsConfig: {
compilerOptions: { compilerOptions: {
baseUrl: '.', baseUrl: '.'
}
}
}, },
fonts: {
defaults: {
weights: ['300 800']
}
}, },
i18n: {
bundle: {
optimizeTranslationDirective: false
}, },
vite: { strategy: 'prefix_except_default'
plugins: [
tailwindcss(),
],
optimizeDeps: {
include: ['debug'],
}, },
icon: {
clientBundle: {
scan: true,
sizeLimitKb: 512
}
}, },
compatibilityDate: '2025-05-13', shadcn: {
}); prefix: 'Ui',
componentDir: join(currentDir, './components/ui')
}
})

View File

@@ -65,4 +65,3 @@ provide('navigation', navigation)
</ClientOnly> </ClientOnly>
</UApp> </UApp>
</template> </template>

View File

@@ -1,49 +1,58 @@
<script setup lang="ts"> <script setup lang="ts">
// import type { NavigationMenuItem } from '@nuxt/ui' // import type { NavigationMenuItem } from '@nuxt/ui'
import type { ContentNavigationItem } from "@nuxt/content"; import type { ContentNavigationItem } from '@nuxt/content'
const navigation = inject<Ref<ContentNavigationItem[]>>("navigation");
const isSettingsOpen = ref(false); const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const isLoginModalOpen = ref(false);
const isRegisterModalOpen = ref(false); const isSettingsOpen = ref(false)
const isLoginModalOpen = ref(false)
const isRegisterModalOpen = ref(false)
// const { header } = useAppConfig(); // const { header } = useAppConfig();
// 定义 props 来接收侧边栏状态和切换函数 // 定义 props 来接收侧边栏状态和切换函数
interface Props { interface Props {
isSidebarOpen?: boolean; isSidebarOpen?: boolean
toggleSidebar?: () => void; toggleSidebar?: () => void
} }
const Props = withDefaults(defineProps<Props>(), { const Props = withDefaults(defineProps<Props>(), {
isSidebarOpen: false, isSidebarOpen: false,
toggleSidebar: () => { }, toggleSidebar: () => { }
}); })
// 登录注册函数 // 登录注册函数
const handleLoginRegister = (type: "login" | "register") => { const handleLoginRegister = (type: 'login' | 'register') => {
if (type === "login") { if (type === 'login') {
isLoginModalOpen.value = true; isLoginModalOpen.value = true
} else if (type === "register") { } else if (type === 'register') {
isRegisterModalOpen.value = true; isRegisterModalOpen.value = true
} }
}; }
</script> </script>
<template> <template>
<UHeader toggle-side="left" title="Estel Docs" mode="modal" class="bg-gray-50 dark:bg-gray-900"> <UHeader
toggle-side="left"
title="Estel Docs"
mode="modal"
class="bg-gray-50 dark:bg-gray-900"
>
<template #title> <template #title>
<h6></h6> <h6 />
</template> </template>
<template #body> <template #body>
<LogoPro class="h-5 w-auto mb-4" /> <LogoPro class="h-5 w-auto mb-4" />
<DocsAsideLeftTop /> <DocsAsideLeftTop />
<div class="mt-4 mb-4 border-t border-gray-200 dark:border-gray-700 w-9/10 mx-5" /> <div class="mt-4 mb-4 border-t border-gray-200 dark:border-gray-700 w-9/10 mx-5" />
<UContentNavigation highlight :navigation="navigation" color="primary" type="single" variant="pill" /> <UContentNavigation
highlight
:navigation="navigation"
color="primary"
type="single"
variant="pill"
/>
</template> </template>
<template #right> <template #right>
@@ -51,16 +60,34 @@ const handleLoginRegister = (type: "login" | "register") => {
<UColorModeButton /> <UColorModeButton />
<button <button
class=" p-2 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" class=" p-2 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title="页面设置" @click="isSettingsOpen = !isSettingsOpen"> title="页面设置"
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> @click="isSettingsOpen = !isSettingsOpen"
<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" /> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-5 h-5"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<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"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg> </svg>
</button> </button>
<UTooltip text="Open on GitHub" :kbds="['meta', 'G']"> <UTooltip
text="Open on GitHub"
:kbds="['meta', 'G']"
>
<UButton <UButton
color="neutral" color="neutral"
variant="ghost" variant="ghost"
@@ -71,32 +98,45 @@ const handleLoginRegister = (type: "login" | "register") => {
/> />
</UTooltip> </UTooltip>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<button @click="handleLoginRegister('login')" <button
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"> 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"
@click="handleLoginRegister('login')"
>
登录 登录
</button> </button>
<button @click="handleLoginRegister('register')" <button
class="text-sm font-medium text-white bg-primary rounded-md px-3 py-2 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-colors"> class="text-sm font-medium text-white bg-primary rounded-md px-3 py-2 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-colors"
@click="handleLoginRegister('register')"
>
注册 注册
</button> </button>
</div> </div>
</template> </template>
</UHeader> </UHeader>
<!-- Theme Settings Panel --> <!-- Theme Settings Panel -->
<ThemeSettings :is-open="isSettingsOpen" @close="isSettingsOpen = false" /> <ThemeSettings
:is-open="isSettingsOpen"
@close="isSettingsOpen = false"
/>
<!-- 登录模态框 --> <!-- 登录模态框 -->
<UModal v-model:open="isLoginModalOpen" title="登录" :dismissible="false"> <UModal
v-model:open="isLoginModalOpen"
title="登录"
:dismissible="false"
>
<template #body> <template #body>
<authLogin /> <authLogin />
</template> </template>
</UModal> </UModal>
<!-- 注册模态框 --> <!-- 注册模态框 -->
<UModal v-model:open="isRegisterModalOpen" title="注册" :dismissible="false"> <UModal
v-model:open="isRegisterModalOpen"
title="注册"
:dismissible="false"
>
<template #body> <template #body>
<authRegister /> <authRegister />
</template> </template>

View File

@@ -3,7 +3,8 @@
<div <div
class="w-64 bg-white dark:bg-gray-900 class="w-64 bg-white dark:bg-gray-900
flex flex-col h-screen border-r border-gray-200 dark:border-gray-700 flex flex-col h-screen border-r border-gray-200 dark:border-gray-700
overflow-hidden"> overflow-hidden"
>
<div class="flex-shrink-0 p-4 border-gray-200 dark:border-gray-700"> <div class="flex-shrink-0 p-4 border-gray-200 dark:border-gray-700">
<!-- Logo --> <!-- Logo -->
<LogoPro /> <LogoPro />
@@ -12,24 +13,33 @@
<!-- Search Box --> <!-- Search Box -->
<div class="p-4 border-gray-200 dark:border-gray-700"> <div class="p-4 border-gray-200 dark:border-gray-700">
<ClientOnly> <ClientOnly>
<UContentSearchButton :collapsed="false" label="搜索文档" class="w-full" <UContentSearchButton
color="primary" /> :collapsed="false"
label="搜索文档"
class="w-full"
color="primary"
/>
</ClientOnly> </ClientOnly>
</div> </div>
<!-- 可滚动的导航区域 --> <!-- 可滚动的导航区域 -->
<div class=" h-full overflow-y-auto "> <div class=" h-full overflow-y-auto ">
<!-- 导航 Section --> <!-- 导航 Section -->
<div> <div>
<NuxtLink to="/" <NuxtLink
to="/"
class="flex items-center px-4 py-2 text-sm font-medium rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 hover:shadow-sm transition-all duration-200" class="flex items-center px-4 py-2 text-sm font-medium rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 hover:shadow-sm transition-all duration-200"
:class="{ :class="{
' text-blue-600 dark:text-blue-400 ': ' text-blue-600 dark:text-blue-400 ':
$route.path === '/', $route.path === '/'
}"> }"
<Icon name="lucide-home" class="text-primary mr-2" size="20" /> >
<Icon
name="lucide-home"
class="text-primary mr-2"
size="20"
/>
站点首页 站点首页
</NuxtLink> </NuxtLink>
<div class="px-2"> <div class="px-2">
@@ -42,7 +52,13 @@
<!-- 文档目录导航 --> <!-- 文档目录导航 -->
<div class="mt-6 flex items-center justify-start pl-4 w-full"> <div class="mt-6 flex items-center justify-start pl-4 w-full">
<UContentNavigation highlight :navigation="navigation" color="primary" type="single" variant="pill" /> <UContentNavigation
highlight
:navigation="navigation"
color="primary"
type="single"
variant="pill"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -50,6 +66,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ContentNavigationItem } from "@nuxt/content"; import type { ContentNavigationItem } from '@nuxt/content'
const navigation = inject<Ref<ContentNavigationItem[]>>("navigation");
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
</script> </script>

View File

@@ -19,7 +19,7 @@ const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const directoryNavigation = computed(() => { const directoryNavigation = computed(() => {
if (!navigation?.value) return [] if (!navigation?.value) return []
return navigation.value.map(item => { return navigation.value.map((item) => {
// 如果是根目录文件(没有 children直接返回 // 如果是根目录文件(没有 children直接返回
if (!item.children || item.children.length === 0) { if (!item.children || item.children.length === 0) {
return { return {

View File

@@ -12,10 +12,12 @@
- mx-auto左右自动外边距使网格居中显示 - mx-auto左右自动外边距使网格居中显示
--> -->
<div class="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 gap-4 w-full mx-auto"> <div class="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 gap-4 w-full mx-auto">
<div v-for="item in firstLevelItems" :key="item.path" <div
v-for="item in firstLevelItems"
:key="item.path"
class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5 sm:p-6 shadow-sm hover:shadow-md transition-all duration-200 cursor-pointer" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5 sm:p-6 shadow-sm hover:shadow-md transition-all duration-200 cursor-pointer"
@click="navigateTo(item.path)"> @click="navigateTo(item.path)"
>
<!-- 卡片头部 --> <!-- 卡片头部 -->
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<h2 class="font-sans text-xl sm:text-xl lg:text-2xl font-bold text-gray-900 dark:text-white leading-tight"> <h2 class="font-sans text-xl sm:text-xl lg:text-2xl font-bold text-gray-900 dark:text-white leading-tight">
@@ -27,19 +29,32 @@
</div> </div>
<!-- 子页面列表 --> <!-- 子页面列表 -->
<div v-if="getChildrenTitles(item).length > 0" class=""> <div
<div v-for="(child, index) in getChildrenWithIcons(item).slice(0, 5)" :key="index" v-if="getChildrenTitles(item).length > 0"
class=""
>
<div
v-for="(child, index) in getChildrenWithIcons(item).slice(0, 5)"
:key="index"
class="flex items-center justify-between class="flex items-center justify-between
text-sm sm:text-base lg:text-lg text-gray-700 dark:text-gray-300 text-sm sm:text-base lg:text-lg text-gray-700 dark:text-gray-300
hover:text-blue-600 dark:hover:text-blue-400 hover:text-blue-600 dark:hover:text-blue-400
transition-colors duration-200 py-1 transition-colors duration-200 py-1
border-b border-gray-200 dark:border-gray-700 pt-2"> border-b border-gray-200 dark:border-gray-700 pt-2"
>
<div class="flex items-center flex-1 min-w-0 pl-1 "> <div class="flex items-center flex-1 min-w-0 pl-1 ">
<UIcon :name="(typeof child.icon === 'string' ? child.icon : 'lucide-file-text')" class="mr-2 text-gray-400" size="14" /> <UIcon
:name="(typeof child.icon === 'string' ? child.icon : 'lucide-file-text')"
class="mr-2 text-gray-400"
size="14"
/>
<span class="text-base font-medium font-sans">{{ child.title }}</span> <span class="text-base font-medium font-sans">{{ child.title }}</span>
</div> </div>
<UIcon name="lucide-chevron-right" class="text-gray-400 flex-shrink-0 ml-2" size="16" /> <UIcon
name="lucide-chevron-right"
class="text-gray-400 flex-shrink-0 ml-2"
size="16"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -48,51 +63,51 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ContentNavigationItem } from "@nuxt/content"; import type { ContentNavigationItem } from '@nuxt/content'
const navigation = inject<Ref<ContentNavigationItem[]>>("navigation"); const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
// 计算属性:提取第一级数据 // 计算属性:提取第一级数据
const firstLevelItems = computed(() => { const firstLevelItems = computed(() => {
if (!navigation?.value) return []; if (!navigation?.value) return []
return navigation.value return navigation.value
.filter(item => item.title && item.path) .filter(item => item.title && item.path)
.map(item => ({ .map(item => ({
...item, ...item,
description: item.description, description: item.description,
icon: item.icon || 'lucide-book-open' icon: item.icon || 'lucide-book-open'
})); }))
}); })
// 获取子页面数量 // 获取子页面数量
const getChildrenCount = (item: ContentNavigationItem) => { const getChildrenCount = (item: ContentNavigationItem) => {
return item.children?.length || 0; return item.children?.length || 0
}; }
// 获取子页面标题列表 // 获取子页面标题列表
const getChildrenTitles = (item: ContentNavigationItem) => { const getChildrenTitles = (item: ContentNavigationItem) => {
if (!item.children) return []; if (!item.children) return []
return item.children.map(child => child.title).filter(Boolean); return item.children.map(child => child.title).filter(Boolean)
}; }
// 获取子页面数据(包含图标) // 获取子页面数据(包含图标)
const getChildrenWithIcons = (item: ContentNavigationItem) => { const getChildrenWithIcons = (item: ContentNavigationItem) => {
if (!item.children) return []; if (!item.children) return []
return item.children.filter(child => child.title); return item.children.filter(child => child.title)
}; }
// 获取描述信息 // 获取描述信息
const getDescription = (item: ContentNavigationItem) => { const getDescription = (item: ContentNavigationItem) => {
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
return `${item.children.length} 个子页面`; return `${item.children.length} 个子页面`
} }
return "文档页面"; return '文档页面'
}; }
// 导航方法 // 导航方法
const navigateTo = (path: string) => { const navigateTo = (path: string) => {
if (path) { if (path) {
window.location.href = path; window.location.href = path
} }
}; }
</script> </script>

View File

@@ -6,8 +6,8 @@
<div class="w-full top-0 left-0 bg-gray-50 dark:bg-gray-900"> <div class="w-full top-0 left-0 bg-gray-50 dark:bg-gray-900">
<!-- 欢迎卡片 --> <!-- 欢迎卡片 -->
<div <div
class="bg-slate-200 dark:bg-slate-600 rounded-lg border border-gray-200 dark:border-gray-700 p-2 sm:p-4 shadow-sm"> class="bg-slate-200 dark:bg-slate-600 rounded-lg border border-gray-200 dark:border-gray-700 p-2 sm:p-4 shadow-sm"
>
<h1 class="text-xl font-bold text-gray-900 dark:text-white mb-2"> <h1 class="text-xl font-bold text-gray-900 dark:text-white mb-2">
Hi 👋, 欢迎使用简单文档系统 Hi 👋, 欢迎使用简单文档系统
</h1> </h1>

View File

@@ -1,6 +1,9 @@
<template> <template>
<div class="h-8"> <div class="h-8">
<NuxtLink to="/" class="flex items-center space-x-3"> <NuxtLink
to="/"
class="flex items-center space-x-3"
>
<div class="w-8 h-8 bg-primary rounded-xl flex items-center justify-center shadow-md"> <div class="w-8 h-8 bg-primary rounded-xl flex items-center justify-center shadow-md">
<span class="text-white font-bold text-lg">E</span> <span class="text-white font-bold text-lg">E</span>
</div> </div>

View File

@@ -15,7 +15,9 @@
<!-- 头部 --> <!-- 头部 -->
<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="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"> <div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">页面设置</h2> <h2 class="text-lg font-semibold text-gray-900 dark:text-white">
页面设置
</h2>
<UButton <UButton
color="neutral" color="neutral"
variant="ghost" variant="ghost"
@@ -30,7 +32,9 @@
<div class="p-6 space-y-8"> <div class="p-6 space-y-8">
<!-- 主题 --> <!-- 主题 -->
<div> <div>
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">主题</h3> <h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">
主题
</h3>
<div class="grid grid-cols-3 gap-3"> <div class="grid grid-cols-3 gap-3">
<UButton <UButton
v-for="theme in themes" v-for="theme in themes"
@@ -47,7 +51,9 @@
<!-- 字体 --> <!-- 字体 -->
<div> <div>
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">字体</h3> <h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">
字体
</h3>
<div class="grid grid-cols-3 gap-3"> <div class="grid grid-cols-3 gap-3">
<UButton <UButton
v-for="font in fonts" v-for="font in fonts"
@@ -64,7 +70,9 @@
<!-- 字号 --> <!-- 字号 -->
<div> <div>
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">字号</h3> <h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">
字号
</h3>
<div class="grid grid-cols-5 gap-2"> <div class="grid grid-cols-5 gap-2">
<UButton <UButton
v-for="size in fontSizes" v-for="size in fontSizes"
@@ -81,7 +89,9 @@
<!-- 主题色 --> <!-- 主题色 -->
<div> <div>
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">主题色</h3> <h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">
主题色
</h3>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<UButton <UButton
v-for="color in themeColors" v-for="color in themeColors"
@@ -105,7 +115,9 @@
<!-- 自定义主题色 --> <!-- 自定义主题色 -->
<div> <div>
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">自定义主题色</h3> <h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">
自定义主题色
</h3>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<input <input
v-model="customColor" v-model="customColor"
@@ -124,7 +136,9 @@
<!-- 代码块主题 --> <!-- 代码块主题 -->
<div> <div>
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">代码块主题</h3> <h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">
代码块主题
</h3>
<USelect <USelect
v-model="selectedCodeTheme" v-model="selectedCodeTheme"
:options="codeThemes" :options="codeThemes"
@@ -136,7 +150,9 @@
<!-- 图注格式 --> <!-- 图注格式 -->
<div> <div>
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">图注格式</h3> <h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">
图注格式
</h3>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<UButton <UButton
v-for="format in captionFormats" v-for="format in captionFormats"
@@ -204,7 +220,7 @@ const {
selectedCaptionFormat, selectedCaptionFormat,
applyCustomColor, applyCustomColor,
resetSettings resetSettings
} = useTheme(); } = useTheme()
// 监听键盘事件(只监听 ESC 键) // 监听键盘事件(只监听 ESC 键)
onMounted(() => { onMounted(() => {

View File

@@ -1,25 +1,34 @@
<template> <template>
<div class="flex flex-row items-center mt-2 mb-2"> <div class="flex flex-row items-center mt-2 mb-2">
<NuxtLink :to="to || href" :target="(blank && '_blank') || target"> <NuxtLink
<UButton :variant="variant" :size="size" :icon="icon" :trailing-icon="trailingIcon" class="min-h-10 max-h-12"> :to="to || href"
<slot/> :target="(blank && '_blank') || target"
>
<UButton
:variant="variant"
:size="size"
:icon="icon"
:trailing-icon="trailingIcon"
class="min-h-10 max-h-12"
>
<slot />
</UButton> </UButton>
</NuxtLink> </NuxtLink>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
type Target = '_blank' | '_parent' | '_self' | '_top' | (string & object) | null | undefined; type Target = '_blank' | '_parent' | '_self' | '_top' | (string & object) | null | undefined
defineProps<{ defineProps<{
variant?: 'solid' | 'outline' | 'ghost' | 'link' | 'soft'; variant?: 'solid' | 'outline' | 'ghost' | 'link' | 'soft'
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
color?: 'gray' | 'red' | 'orange' | 'amber' | 'yellow' | 'lime' | 'green' | 'emerald' | 'teal' | 'cyan' | 'sky' | 'blue' | 'indigo' | 'violet' | 'purple' | 'fuchsia' | 'pink' | 'rose' | 'white' | 'black'; color?: 'gray' | 'red' | 'orange' | 'amber' | 'yellow' | 'lime' | 'green' | 'emerald' | 'teal' | 'cyan' | 'sky' | 'blue' | 'indigo' | 'violet' | 'purple' | 'fuchsia' | 'pink' | 'rose' | 'white' | 'black'
icon?: string; icon?: string
to?: string; to?: string
href?: string; href?: string
target?: Target; target?: Target
trailingIcon?: string; trailingIcon?: string
blank?: boolean; blank?: boolean
}>(); }>()
</script> </script>

View File

@@ -7,7 +7,7 @@ const isLight = computed({
}, },
set(_isLight) { set(_isLight) {
colorMode.preference = _isLight ? 'light' : 'dark' colorMode.preference = _isLight ? 'light' : 'dark'
}, }
}) })
</script> </script>
@@ -24,7 +24,7 @@ const isLight = computed({
:ui="{ :ui="{
base: 'w-70 h-35 rounded-lg rotate-90 data-[state=checked]:bg-[var(--ui-color-neutral-200)]', base: 'w-70 h-35 rounded-lg rotate-90 data-[state=checked]:bg-[var(--ui-color-neutral-200)]',
thumb: 'data-[state=checked]:translate-x-35 data-[state=checked]:rtl:-translate-x-35 rounded-lg size-34', thumb: 'data-[state=checked]:translate-x-35 data-[state=checked]:rtl:-translate-x-35 rounded-lg size-34',
icon: 'rotate-270 size-8', icon: 'rotate-270 size-8'
}" }"
@click="isDark = !isDark" @click="isDark = !isDark"
/> />

View File

@@ -1,6 +1,12 @@
<template> <template>
<UPageCard :title="title" :description="description" :spotlight="spotlight" :icon="icon" class=""> <UPageCard
<slot/> :title="title"
:description="description"
:spotlight="spotlight"
:icon="icon"
class=""
>
<slot />
<NuxtImg <NuxtImg
v-if="img" v-if="img"
:src="img" :src="img"
@@ -8,7 +14,6 @@
/> />
<span> {{ content }} </span> <span> {{ content }} </span>
</UPageCard> </UPageCard>
</template> </template>
<!-- ::card <!-- ::card
@@ -22,19 +27,17 @@ Image Card
Beautifully designed **Nuxt Content** template with **shadcn-vue**. _Customizable. Compatible. Open Source._ Beautifully designed **Nuxt Content** template with **shadcn-vue**. _Customizable. Compatible. Open Source._
:: --> :: -->
<script setup lang="ts"> <script setup lang="ts">
const {
const { content = ''
content="",
} =defineProps<{
title?: string;
content?: string;
img?: string;
icon?: string;
description?: string;
spotlight?:boolean;
slot?:string;
}>();
</script>
} = defineProps<{
title?: string
content?: string
img?: string
icon?: string
description?: string
spotlight?: boolean
slot?: string
}>()
</script>

View File

@@ -1,6 +1,14 @@
<template> <template>
<UTabs :items="tabItems" class="w-full" :unmount-on-hide="false" variant="link"> <UTabs
<template v-for="item in tabItems" #[item.slot]="{ item: slotItem }"> :items="tabItems"
class="w-full"
:unmount-on-hide="false"
variant="link"
>
<template
v-for="item in tabItems"
#[item.slot]="{ item: slotItem }"
>
<div class="mt-4"> <div class="mt-4">
<component :is="getSlotContent(slotItem)" /> <component :is="getSlotContent(slotItem)" />
</div> </div>

View File

@@ -1,8 +1,15 @@
<template> <template>
<UPage class="mt-4 mb-4 bg-white dark:bg-gray-900 rounded-xl border border-gray-300 dark:border-gray-700"> <UPage class="mt-4 mb-4 bg-white dark:bg-gray-900 rounded-xl border border-gray-300 dark:border-gray-700">
<div v-if="title" class="flex items-center font-mono text-base m-3 text-gray-700 dark:text-gray-300 rounded-xl "> <div
v-if="title"
<Icon v-if="icon" :name="icon" class="mr-2" size="20" /> class="flex items-center font-mono text-base m-3 text-gray-700 dark:text-gray-300 rounded-xl "
>
<Icon
v-if="icon"
:name="icon"
class="mr-2"
size="20"
/>
<span>{{ title }}</span> <span>{{ title }}</span>
</div> </div>
@@ -33,7 +40,10 @@
class="file-tree-item" class="file-tree-item"
> >
<div class="flex items-center py-1 px-2"> <div class="flex items-center py-1 px-2">
<div class="flex items-center" :style="{ marginLeft: '0px' }"> <div
class="flex items-center"
:style="{ marginLeft: '0px' }"
>
<Icon <Icon
v-if="showIcon" v-if="showIcon"
:name="item.icon || 'lucide-file'" :name="item.icon || 'lucide-file'"
@@ -61,38 +71,38 @@
</template> </template>
</ClientOnly> </ClientOnly>
</div> </div>
</UPage> </UPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
type InputTreeItem = string | { type InputTreeItem = string | {
[key: string]: InputTreeItem[]; [key: string]: InputTreeItem[]
}; }
type FileTreeItemDiff = 'none' | 'addition' | 'deletion'; type FileTreeItemDiff = 'none' | 'addition' | 'deletion'
interface FileTreeItem { interface FileTreeItem {
title: string; title: string
icon?: string; icon?: string
children?: FileTreeItem[]; children?: FileTreeItem[]
highlighted?: boolean; highlighted?: boolean
diff?: FileTreeItemDiff; diff?: FileTreeItemDiff
isFolder?: boolean; isFolder?: boolean
} }
const { const {
tree, tree,
autoSlash = true, autoSlash = true,
showArrow = false, showArrow = false,
showIcon = true, showIcon = true
} = defineProps<{ } = defineProps<{
title?: string; title?: string
icon?: string; icon?: string
autoSlash?: boolean; autoSlash?: boolean
showArrow?: boolean; showArrow?: boolean
showIcon?: boolean; showIcon?: boolean
tree: InputTreeItem[]; tree: InputTreeItem[]
}>(); }>()
// 默认图标映射 // 默认图标映射
const defaultIcons = { const defaultIcons = {
@@ -103,38 +113,38 @@ const defaultIcons = {
json: 'simple-icons-json', json: 'simple-icons-json',
folder: 'lucide-folder', folder: 'lucide-folder',
file: 'lucide-file' file: 'lucide-file'
}; }
function getIcon(filename: string, type: 'folder' | 'file'): string { function getIcon(filename: string, type: 'folder' | 'file'): string {
if (filename === '...') return 'lucide-more-horizontal'; if (filename === '...') return 'lucide-more-horizontal'
if (filename.endsWith('/')) return defaultIcons.folder; if (filename.endsWith('/')) return defaultIcons.folder
const parts = filename.split('.'); const parts = filename.split('.')
const extension = parts.length > 1 ? parts[parts.length - 1]?.toLowerCase() || '' : ''; const extension = parts.length > 1 ? parts[parts.length - 1]?.toLowerCase() || '' : ''
// 根据扩展名返回对应图标 // 根据扩展名返回对应图标
switch (extension) { switch (extension) {
case 'vue': return defaultIcons.vue; case 'vue': return defaultIcons.vue
case 'ts': return defaultIcons.ts; case 'ts': return defaultIcons.ts
case 'js': return defaultIcons.js; case 'js': return defaultIcons.js
case 'md': return defaultIcons.md; case 'md': return defaultIcons.md
case 'json': return defaultIcons.json; case 'json': return defaultIcons.json
default: return type === 'file' ? defaultIcons.file : defaultIcons.folder; default: return type === 'file' ? defaultIcons.file : defaultIcons.folder
} }
} }
function getItem(key: string, type: 'folder' | 'file', children?: InputTreeItem[]): FileTreeItem { function getItem(key: string, type: 'folder' | 'file', children?: InputTreeItem[]): FileTreeItem {
let title = key; let title = key
let highlighted = false; let highlighted = false
if (title.startsWith('^') && title.endsWith('^')) { if (title.startsWith('^') && title.endsWith('^')) {
title = title.substring(1, title.length - 1); title = title.substring(1, title.length - 1)
highlighted = true; highlighted = true
} }
let diff: FileTreeItemDiff = 'none'; let diff: FileTreeItemDiff = 'none'
if (title.startsWith('+')) diff = 'addition'; if (title.startsWith('+')) diff = 'addition'
else if (title.startsWith('-')) diff = 'deletion'; else if (title.startsWith('-')) diff = 'deletion'
if (type === 'file') { if (type === 'file') {
return { return {
@@ -143,7 +153,7 @@ function getItem(key: string, type: 'folder' | 'file', children?: InputTreeItem[
highlighted, highlighted,
diff, diff,
isFolder: false isFolder: false
}; }
} else { } else {
return { return {
title: `${title}${autoSlash ? '/' : ''}`, title: `${title}${autoSlash ? '/' : ''}`,
@@ -152,35 +162,35 @@ function getItem(key: string, type: 'folder' | 'file', children?: InputTreeItem[
highlighted, highlighted,
diff, diff,
isFolder: true isFolder: true
}; }
} }
} }
function getTree(tree: InputTreeItem[]): FileTreeItem[] { function getTree(tree: InputTreeItem[]): FileTreeItem[] {
const res: FileTreeItem[] = []; const res: FileTreeItem[] = []
for (const item of tree) { for (const item of tree) {
if (typeof item === 'string') { if (typeof item === 'string') {
res.push(getItem(item, 'file')); res.push(getItem(item, 'file'))
} else if (typeof item === 'object' && item !== null) { } else if (typeof item === 'object' && item !== null) {
for (const key of Object.keys(item)) { for (const key of Object.keys(item)) {
const children = (item as Record<string, InputTreeItem[]>)[key]; const children = (item as Record<string, InputTreeItem[]>)[key]
if (children) { if (children) {
res.push(getItem(key, 'folder', children)); res.push(getItem(key, 'folder', children))
} }
} }
} }
} }
return res; return res
} }
const parsedTree = computed(() => { const parsedTree = computed(() => {
return getTree(tree); return getTree(tree)
}); })
// 使用 provide/inject 来管理展开状态 // 使用 provide/inject 来管理展开状态
const expandedState = ref(new Set<string>()); const expandedState = ref(new Set<string>())
provide('expandedState', expandedState); provide('expandedState', expandedState)
</script> </script>

View File

@@ -10,7 +10,10 @@
@click="toggleFolder" @click="toggleFolder"
> >
<!-- 缩进 --> <!-- 缩进 -->
<div class="flex items-center" :style="{ marginLeft: level * 16 + 'px' }"> <div
class="flex items-center"
:style="{ marginLeft: level * 16 + 'px' }"
>
<!-- 箭头仅文件夹且有子项时显示 --> <!-- 箭头仅文件夹且有子项时显示 -->
<Icon <Icon
v-if="showArrow && item.isFolder && item.children && item.children.length > 0" v-if="showArrow && item.isFolder && item.children && item.children.length > 0"
@@ -18,7 +21,10 @@
class="mr-1 text-gray-400 w-4 h-4 transition-transform" class="mr-1 text-gray-400 w-4 h-4 transition-transform"
:class="{ 'rotate-90': isExpanded }" :class="{ 'rotate-90': isExpanded }"
/> />
<div v-else-if="showArrow" class="w-4 mr-1"></div> <div
v-else-if="showArrow"
class="w-4 mr-1"
/>
<!-- 图标 --> <!-- 图标 -->
<Icon <Icon
@@ -47,7 +53,10 @@
</div> </div>
<!-- 子项 --> <!-- 子项 -->
<div v-if="item.children && item.children.length > 0 && isExpanded" class="ml-4"> <div
v-if="item.children && item.children.length > 0 && isExpanded"
class="ml-4"
>
<FileTreeItem <FileTreeItem
v-for="child in item.children" v-for="child in item.children"
:key="child.title" :key="child.title"
@@ -61,48 +70,48 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
type FileTreeItemDiff = 'none' | 'addition' | 'deletion'; type FileTreeItemDiff = 'none' | 'addition' | 'deletion'
interface FileTreeItem { interface FileTreeItem {
title: string; title: string
icon?: string; icon?: string
children?: FileTreeItem[]; children?: FileTreeItem[]
highlighted?: boolean; highlighted?: boolean
diff?: FileTreeItemDiff; diff?: FileTreeItemDiff
isFolder?: boolean; isFolder?: boolean
} }
const props = defineProps<{ const props = defineProps<{
item: FileTreeItem; item: FileTreeItem
level: number; level: number
showArrow: boolean; showArrow: boolean
showIcon: boolean; showIcon: boolean
}>(); }>()
const expandedState = inject('expandedState', ref(new Set<string>())); const expandedState = inject('expandedState', ref(new Set<string>()))
const itemKey = computed(() => `${props.item.title}-${props.level}`); const itemKey = computed(() => `${props.item.title}-${props.level}`)
const isExpanded = computed({ const isExpanded = computed({
get: () => expandedState.value.has(itemKey.value), get: () => expandedState.value.has(itemKey.value),
set: (value: boolean) => { set: (value: boolean) => {
if (value) { if (value) {
expandedState.value.add(itemKey.value); expandedState.value.add(itemKey.value)
} else { } else {
expandedState.value.delete(itemKey.value); expandedState.value.delete(itemKey.value)
} }
} }
}); })
// 初始化时展开所有文件夹 // 初始化时展开所有文件夹
onMounted(() => { onMounted(() => {
if (props.item.isFolder && props.item.children && props.item.children.length > 0) { if (props.item.isFolder && props.item.children && props.item.children.length > 0) {
isExpanded.value = true; isExpanded.value = true
} }
}); })
function toggleFolder() { function toggleFolder() {
if (props.item.isFolder && props.item.children && props.item.children.length > 0) { if (props.item.isFolder && props.item.children && props.item.children.length > 0) {
isExpanded.value = !isExpanded.value; isExpanded.value = !isExpanded.value
} }
} }
</script> </script>

View File

@@ -7,7 +7,10 @@
sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
class="w-full h-full min-h-[600px] overflow-hidden rounded-md" class="w-full h-full min-h-[600px] overflow-hidden rounded-md"
/> />
<USkeleton v-else class="w-full min-h-[600px] rounded-md" /> <USkeleton
v-else
class="w-full min-h-[600px] rounded-md"
/>
</div> </div>
</template> </template>
@@ -19,31 +22,31 @@ const {
branch = 'main', branch = 'main',
dir = '', dir = '',
file, file,
title = 'Playground', title = 'Playground'
} = defineProps<{ } = defineProps<{
provider: 'stackblitz' | 'codesandbox'; provider: 'stackblitz' | 'codesandbox'
id?: string; id?: string
repo?: string; repo?: string
branch?: string; branch?: string
dir?: string; dir?: string
file: string; file: string
title?: string; title?: string
}>(); }>()
const url = ref(''); const url = ref('')
const colorMode = useColorMode(); const colorMode = useColorMode()
onMounted(() => { onMounted(() => {
if (provider === 'stackblitz') { if (provider === 'stackblitz') {
if (repo) if (repo)
url.value = `https://stackblitz.com/github/${repo}/tree/${branch}/${dir}?embed=1&file=${file}&theme=${colorMode.value}`; url.value = `https://stackblitz.com/github/${repo}/tree/${branch}/${dir}?embed=1&file=${file}&theme=${colorMode.value}`
else if (id) else if (id)
url.value = `https://stackblitz.com/edit/${id}?embed=1&file=${file}&theme=${colorMode.value}`; url.value = `https://stackblitz.com/edit/${id}?embed=1&file=${file}&theme=${colorMode.value}`
} else if (provider === 'codesandbox') { } else if (provider === 'codesandbox') {
if (repo) if (repo)
url.value = `https://codesandbox.io/p/sandbox/github/${repo}/tree/${branch}/${dir}?embed=1&file=${file}`; url.value = `https://codesandbox.io/p/sandbox/github/${repo}/tree/${branch}/${dir}?embed=1&file=${file}`
else if (id) else if (id)
url.value = `https://codesandbox.io/embed/${id}?view=editor+%2B+preview&module=${file}`; url.value = `https://codesandbox.io/embed/${id}?view=editor+%2B+preview&module=${file}`
} }
}); })
</script> </script>

View File

@@ -1,5 +1,8 @@
<template> <template>
<h1 :id class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"> <h1
:id
class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"
>
<NuxtLink <NuxtLink
v-if="generate" v-if="generate"
:to="`#${id}`" :to="`#${id}`"
@@ -11,8 +14,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { id } = defineProps<{ id?: string }>(); const { id } = defineProps<{ id?: string }>()
const { headings } = useRuntimeConfig().public.mdc; const { headings } = useRuntimeConfig().public.mdc
const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h1))); const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h1)))
</script> </script>

View File

@@ -1,5 +1,8 @@
<template> <template>
<h2 :id class="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors [&:not(:first-child)]:mt-10"> <h2
:id
class="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors [&:not(:first-child)]:mt-10"
>
<NuxtLink <NuxtLink
v-if="id && generate" v-if="id && generate"
:to="`#${id}`" :to="`#${id}`"
@@ -11,8 +14,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { id } = defineProps<{ id?: string }>(); const { id } = defineProps<{ id?: string }>()
const { headings } = useRuntimeConfig().public.mdc; const { headings } = useRuntimeConfig().public.mdc
const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h2))); const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h2)))
</script> </script>

View File

@@ -1,5 +1,8 @@
<template> <template>
<h3 :id class="scroll-m-20 text-2xl font-semibold tracking-tight [&:not(:first-child)]:mt-8"> <h3
:id
class="scroll-m-20 text-2xl font-semibold tracking-tight [&:not(:first-child)]:mt-8"
>
<NuxtLink <NuxtLink
v-if="id && generate" v-if="id && generate"
:to="`#${id}`" :to="`#${id}`"
@@ -11,8 +14,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { id } = defineProps<{ id?: string }>(); const { id } = defineProps<{ id?: string }>()
const { headings } = useRuntimeConfig().public.mdc; const { headings } = useRuntimeConfig().public.mdc
const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h3))); const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h3)))
</script> </script>

View File

@@ -1,5 +1,8 @@
<template> <template>
<h4 :id class="scroll-m-20 text-xl font-semibold tracking-tight [&:not(:first-child)]:mt-6"> <h4
:id
class="scroll-m-20 text-xl font-semibold tracking-tight [&:not(:first-child)]:mt-6"
>
<NuxtLink <NuxtLink
v-if="id && generate" v-if="id && generate"
:to="`#${id}`" :to="`#${id}`"
@@ -11,8 +14,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { id } = defineProps<{ id?: string }>(); const { id } = defineProps<{ id?: string }>()
const { headings } = useRuntimeConfig().public.mdc; const { headings } = useRuntimeConfig().public.mdc
const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h4))); const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h4)))
</script> </script>

View File

@@ -1,5 +1,8 @@
<template> <template>
<h5 :id class="scroll-m-20 text-lg font-semibold tracking-tight [&:not(:first-child)]:mt-6"> <h5
:id
class="scroll-m-20 text-lg font-semibold tracking-tight [&:not(:first-child)]:mt-6"
>
<NuxtLink <NuxtLink
v-if="id && generate" v-if="id && generate"
:to="`#${id}`" :to="`#${id}`"
@@ -11,8 +14,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { id } = defineProps<{ id?: string }>(); const { id } = defineProps<{ id?: string }>()
const { headings } = useRuntimeConfig().public.mdc; const { headings } = useRuntimeConfig().public.mdc
const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h5))); const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h5)))
</script> </script>

View File

@@ -1,5 +1,8 @@
<template> <template>
<h6 :id class="scroll-m-20 text-lg font-semibold tracking-tight [&:not(:first-child)]:mt-6"> <h6
:id
class="scroll-m-20 text-lg font-semibold tracking-tight [&:not(:first-child)]:mt-6"
>
<NuxtLink <NuxtLink
v-if="id && generate" v-if="id && generate"
:to="`#${id}`" :to="`#${id}`"
@@ -11,8 +14,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { id } = defineProps<{ id?: string }>(); const { id } = defineProps<{ id?: string }>()
const { headings } = useRuntimeConfig().public.mdc; const { headings } = useRuntimeConfig().public.mdc
const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h6))); const generate = computed(() => id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h6)))
</script> </script>

View File

@@ -1,10 +1,20 @@
<template> <template>
<UPage> <UPage>
<NuxtLink v-if="to" :to="to"> <NuxtLink
<UAlert :icon :title :color="color" :variant="variant" :description="to" v-if="to"
class="mt-2 mb-3 text-black dark:text-white" :ui="{ :to="to"
>
<UAlert
:icon
:title
:color="color"
:variant="variant"
:description="to"
class="mt-2 mb-3 text-black dark:text-white"
:ui="{
icon: iconSize icon: iconSize
}"> }"
>
<slot /> <slot />
</UAlert> </UAlert>
</NuxtLink> </NuxtLink>
@@ -12,20 +22,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { const {
variant = 'subtle', variant = 'subtle',
title, title,
icon = 'lucide:bookmark', icon = 'lucide:bookmark',
color = 'primary', color = 'primary'
} = defineProps<{ } = defineProps<{
color?: 'primary' | 'error' | 'secondary' | 'success' | 'info' | 'warning' | 'neutral'; color?: 'primary' | 'error' | 'secondary' | 'success' | 'info' | 'warning' | 'neutral'
description?: string; description?: string
title?: string; title?: string
to?: string; to?: string
icon?: string; icon?: string
iconSize?: string; iconSize?: string
variant?: 'subtle' | 'solid' | 'outline' | 'soft'; variant?: 'subtle' | 'solid' | 'outline' | 'soft'
}>(); }>()
</script> </script>

View File

@@ -1,6 +1,10 @@
<template> <template>
<!-- Iconify Icons --> <!-- Iconify Icons -->
<Icon v-if="checkIcon(name)" :name :size /> <Icon
v-if="checkIcon(name)"
:name
:size
/>
<!-- Emojis --> <!-- Emojis -->
<span <span
v-else-if="/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g.test(name)" v-else-if="/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g.test(name)"
@@ -16,17 +20,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { stringToIcon, validateIconName } from '@iconify/utils'; import { stringToIcon, validateIconName } from '@iconify/utils'
const { size = 16 } = defineProps<{ const { size = 16 } = defineProps<{
name: string; name: string
size?: number; size?: number
}>(); }>()
function checkIcon(name: string): boolean { function checkIcon(name: string): boolean {
if (name.includes('http')) if (name.includes('http'))
return false; return false
return validateIconName(stringToIcon(name)); return validateIconName(stringToIcon(name))
} }
</script> </script>

View File

@@ -7,7 +7,6 @@
> >
<component :is="slot" /> <component :is="slot" />
</div> </div>
</UPageCard> </UPageCard>
</template> </template>

View File

@@ -1,11 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui' import type { NavigationMenuItem } from '@nuxt/ui'
const route = useRoute() const route = useRoute()
const items = ref<NavigationMenuItem[][]>([ const items = ref<NavigationMenuItem[][]>([
[ [
{ {
label: 'Guide', label: 'Guide',
icon: 'lucide-book-open', icon: 'lucide-book-open'
}, },
{ {
label: 'Composables', label: 'Composables',
@@ -74,7 +75,7 @@ const items = ref<NavigationMenuItem[][]>([
} }
] ]
} }
], ]
// [ // [
// { // {
// label: 'GitHub', // label: 'GitHub',
@@ -94,5 +95,10 @@ const items = ref<NavigationMenuItem[][]>([
</script> </script>
<template> <template>
<UNavigationMenu orientation="vertical" :items="items" class="w-full justify-center" color="primary" /> <UNavigationMenu
orientation="vertical"
:items="items"
class="w-full justify-center"
color="primary"
/>
</template> </template>

View File

@@ -17,28 +17,28 @@ const items = [
toast.add({ toast.add({
title: 'Markdown link copied to clipboard', title: 'Markdown link copied to clipboard',
icon: 'lucide-check-circle', icon: 'lucide-check-circle',
color: 'success', color: 'success'
}) })
}, }
}, },
{ {
label: 'View as Markdown', label: 'View as Markdown',
icon: 'simple-icons:markdown', icon: 'simple-icons:markdown',
target: '_blank', target: '_blank',
to: markdownLink.value, to: markdownLink.value
}, },
{ {
label: 'Open in ChatGPT', label: 'Open in ChatGPT',
icon: 'simple-icons:openai', icon: 'simple-icons:openai',
target: '_blank', target: '_blank',
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`, to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`
}, },
{ {
label: 'Open in Claude', label: 'Open in Claude',
icon: 'simple-icons:anthropic', icon: 'simple-icons:anthropic',
target: '_blank', target: '_blank',
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`, to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`
}, }
] ]
</script> </script>
@@ -50,7 +50,7 @@ const items = [
color="neutral" color="neutral"
variant="outline" variant="outline"
:ui="{ :ui="{
leadingIcon: [copied ? 'text-primary' : 'text-neutral', 'size-3.5'], leadingIcon: [copied ? 'text-primary' : 'text-neutral', 'size-3.5']
}" }"
@click="copy(markdownLink)" @click="copy(markdownLink)"
/> />
@@ -61,10 +61,10 @@ const items = [
:content="{ :content="{
align: 'end', align: 'end',
side: 'bottom', side: 'bottom',
sideOffset: 8, sideOffset: 8
}" }"
:ui="{ :ui="{
content: 'w-48', content: 'w-48'
}" }"
> >
<UButton <UButton

View File

@@ -41,20 +41,20 @@
* </template> * </template>
* ``` * ```
*/ */
import { ref, watch } from 'vue'; import { ref, watch } from 'vue'
// 定义各种选项 // 定义各种选项
const themes = [ const themes = [
{ label: '经典', value: 'classic' }, { label: '经典', value: 'classic' },
{ label: '优雅', value: 'elegant' }, { label: '优雅', value: 'elegant' },
{ label: '简洁', value: 'minimal' } { label: '简洁', value: 'minimal' }
]; ]
const fonts = [ const fonts = [
{ label: '无衬线', value: 'sans-serif' }, { label: '无衬线', value: 'sans-serif' },
{ label: '衬线', value: 'serif' }, { label: '衬线', value: 'serif' },
{ label: '等宽', value: 'monospace' } { label: '等宽', value: 'monospace' }
]; ]
const fontSizes = [ const fontSizes = [
{ label: '更小', value: 'xs' }, { label: '更小', value: 'xs' },
@@ -62,7 +62,7 @@ const fontSizes = [
{ label: '推荐', value: 'base' }, { label: '推荐', value: 'base' },
{ label: '稍大', value: 'lg' }, { label: '稍大', value: 'lg' },
{ label: '更大', value: 'xl' } { label: '更大', value: 'xl' }
]; ]
const themeColors = [ const themeColors = [
{ label: '经典蓝', value: 'blue', color: '#3B82F6' }, { label: '经典蓝', value: 'blue', color: '#3B82F6' },
@@ -76,7 +76,7 @@ const themeColors = [
{ label: '石墨黑', value: 'gray', color: '#6B7280' }, { label: '石墨黑', value: 'gray', color: '#6B7280' },
{ label: '雾烟灰', value: 'slate', color: '#64748B' }, { label: '雾烟灰', value: 'slate', color: '#64748B' },
{ label: '樱花粉', value: 'pink', color: '#EC4899' } { label: '樱花粉', value: 'pink', color: '#EC4899' }
]; ]
const codeThemes = [ const codeThemes = [
{ label: 'dark', value: 'dark' }, { label: 'dark', value: 'dark' },
@@ -85,14 +85,14 @@ const codeThemes = [
{ label: 'github-light', value: 'github-light' }, { label: 'github-light', value: 'github-light' },
{ label: 'one-dark-pro', value: 'one-dark-pro' }, { label: 'one-dark-pro', value: 'one-dark-pro' },
{ label: 'material-theme', value: 'material-theme' } { label: 'material-theme', value: 'material-theme' }
]; ]
const captionFormats = [ const captionFormats = [
{ label: 'title 优先', value: 'title' }, { label: 'title 优先', value: 'title' },
{ label: 'alt 优先', value: 'alt' }, { label: 'alt 优先', value: 'alt' },
{ label: '只显示 title', value: 'title-only' }, { label: '只显示 title', value: 'title-only' },
{ label: '只显示 alt', value: 'alt-only' } { label: '只显示 alt', value: 'alt-only' }
]; ]
// 默认设置 // 默认设置
const defaultSettings = { const defaultSettings = {
@@ -102,68 +102,68 @@ const defaultSettings = {
themeColor: 'blue', themeColor: 'blue',
codeTheme: 'dark', codeTheme: 'dark',
captionFormat: 'none' captionFormat: 'none'
}; }
// 响应式状态 // 响应式状态
const selectedTheme = ref<string>(defaultSettings.theme); const selectedTheme = ref<string>(defaultSettings.theme)
const selectedFont = ref<string>(defaultSettings.font); const selectedFont = ref<string>(defaultSettings.font)
const selectedFontSize = ref<string>(defaultSettings.fontSize); const selectedFontSize = ref<string>(defaultSettings.fontSize)
const selectedThemeColor = ref<string>(defaultSettings.themeColor); const selectedThemeColor = ref<string>(defaultSettings.themeColor)
const customColor = ref<string>('#3B82F6'); const customColor = ref<string>('#3B82F6')
const selectedCodeTheme = ref<string>(defaultSettings.codeTheme); const selectedCodeTheme = ref<string>(defaultSettings.codeTheme)
const selectedCaptionFormat = ref<string>(defaultSettings.captionFormat); const selectedCaptionFormat = ref<string>(defaultSettings.captionFormat)
// 这是一个单例,确保在整个应用中只有一个主题状态实例 // 这是一个单例,确保在整个应用中只有一个主题状态实例
let isInitialized = false; let isInitialized = false
export function useTheme() { export function useTheme() {
const colorMode = useColorMode(); const colorMode = useColorMode()
const initializeTheme = () => { const initializeTheme = () => {
if (import.meta.client && !isInitialized) { if (import.meta.client && !isInitialized) {
selectedTheme.value = localStorage.getItem('app-theme') || defaultSettings.theme; selectedTheme.value = localStorage.getItem('app-theme') || defaultSettings.theme
selectedFont.value = localStorage.getItem('app-font') || defaultSettings.font; selectedFont.value = localStorage.getItem('app-font') || defaultSettings.font
selectedFontSize.value = localStorage.getItem('app-font-size') || defaultSettings.fontSize; selectedFontSize.value = localStorage.getItem('app-font-size') || defaultSettings.fontSize
selectedThemeColor.value = localStorage.getItem('app-theme-color') || defaultSettings.themeColor; selectedThemeColor.value = localStorage.getItem('app-theme-color') || defaultSettings.themeColor
selectedCodeTheme.value = localStorage.getItem('app-code-theme') || defaultSettings.codeTheme; selectedCodeTheme.value = localStorage.getItem('app-code-theme') || defaultSettings.codeTheme
selectedCaptionFormat.value = localStorage.getItem('app-caption-format') || defaultSettings.captionFormat; selectedCaptionFormat.value = localStorage.getItem('app-caption-format') || defaultSettings.captionFormat
// 找到自定义颜色对应的主题色值 // 找到自定义颜色对应的主题色值
const initialColor = themeColors.find(c => c.value === selectedThemeColor.value)?.color || '#3B82F6'; const initialColor = themeColors.find(c => c.value === selectedThemeColor.value)?.color || '#3B82F6'
customColor.value = localStorage.getItem('app-custom-color') || initialColor; customColor.value = localStorage.getItem('app-custom-color') || initialColor
applyThemeVariables(); applyThemeVariables()
isInitialized = true; isInitialized = true
console.log('主题已初始化'); console.log('主题已初始化')
}
} }
};
const applyThemeVariables = () => { const applyThemeVariables = () => {
if (import.meta.client) { if (import.meta.client) {
const root = document.documentElement; const root = document.documentElement
// 颜色映射到 @nuxt/ui 支持的颜色 // 颜色映射到 @nuxt/ui 支持的颜色
const colorMapping: { [key: string]: string } = { const colorMapping: { [key: string]: string } = {
'blue': 'blue', blue: 'blue',
'emerald': 'emerald', emerald: 'emerald',
'orange': 'orange', orange: 'orange',
'yellow': 'yellow', yellow: 'yellow',
'violet': 'violet', violet: 'violet',
'sky': 'sky', sky: 'sky',
'rose': 'rose', rose: 'rose',
'lime': 'lime', lime: 'lime',
'gray': 'gray', gray: 'gray',
'slate': 'slate', slate: 'slate',
'pink': 'pink' pink: 'pink'
}; }
// 使用 @nuxt/ui 的主题系统 // 使用 @nuxt/ui 的主题系统
if (selectedThemeColor.value !== 'custom') { if (selectedThemeColor.value !== 'custom') {
const mappedColor = colorMapping[selectedThemeColor.value]; const mappedColor = colorMapping[selectedThemeColor.value]
if (mappedColor) { if (mappedColor) {
const appConfig = useAppConfig(); const appConfig = useAppConfig()
if (!appConfig.ui.colors) appConfig.ui.colors = {}; if (!appConfig.ui.colors) appConfig.ui.colors = {}
appConfig.ui.colors.primary = mappedColor; appConfig.ui.colors.primary = mappedColor
} }
} }
@@ -172,18 +172,18 @@ export function useTheme() {
'sans-serif': 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif', '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', 'serif': 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
'monospace': 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace' 'monospace': 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace'
}; }
root.style.setProperty('--font-family', fontMapping[selectedFont.value as keyof typeof fontMapping]); root.style.setProperty('--font-family', fontMapping[selectedFont.value as keyof typeof fontMapping])
const fontSizeMapping = { const fontSizeMapping = {
'xs': '0.75rem', 'sm': '0.875rem', 'base': '1rem', 'lg': '1.125rem', 'xl': '1.25rem' 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.style.setProperty('--font-size-base', fontSizeMapping[selectedFontSize.value as keyof typeof fontSizeMapping])
root.classList.remove('theme-classic', 'theme-elegant', 'theme-minimal'); root.classList.remove('theme-classic', 'theme-elegant', 'theme-minimal')
root.classList.add(`theme-${selectedTheme.value}`); root.classList.add(`theme-${selectedTheme.value}`)
}
} }
};
/** /**
* 应用自定义主色调 * 应用自定义主色调
@@ -195,62 +195,62 @@ export function useTheme() {
*/ */
const applyCustomColor = () => { const applyCustomColor = () => {
if (import.meta.client) { if (import.meta.client) {
selectedThemeColor.value = 'custom'; selectedThemeColor.value = 'custom'
const root = document.documentElement; const root = document.documentElement
// 使用 Nuxt UI v3 推荐的 CSS 变量覆盖方法 // 使用 Nuxt UI v3 推荐的 CSS 变量覆盖方法
// 设置主色调的 CSS 变量,这会影响所有使用 primary 颜色的组件 // 设置主色调的 CSS 变量,这会影响所有使用 primary 颜色的组件
root.style.setProperty('--ui-primary', customColor.value); root.style.setProperty('--ui-primary', customColor.value)
// 保持 Nuxt UI 使用有效的颜色系统 // 保持 Nuxt UI 使用有效的颜色系统
const appConfig = useAppConfig(); const appConfig = useAppConfig()
if (!appConfig.ui.colors) appConfig.ui.colors = {}; if (!appConfig.ui.colors) appConfig.ui.colors = {}
appConfig.ui.colors.primary = 'blue'; // 使用有效的颜色名称作为基础 appConfig.ui.colors.primary = 'blue' // 使用有效的颜色名称作为基础
}
} }
};
const resetSettings = () => { const resetSettings = () => {
selectedTheme.value = defaultSettings.theme; selectedTheme.value = defaultSettings.theme
selectedFont.value = defaultSettings.font; selectedFont.value = defaultSettings.font
selectedFontSize.value = defaultSettings.fontSize; selectedFontSize.value = defaultSettings.fontSize
selectedThemeColor.value = defaultSettings.themeColor; selectedThemeColor.value = defaultSettings.themeColor
const defaultColor = themeColors.find(c => c.value === defaultSettings.themeColor)?.color || '#3B82F6'; const defaultColor = themeColors.find(c => c.value === defaultSettings.themeColor)?.color || '#3B82F6'
customColor.value = defaultColor; customColor.value = defaultColor
selectedCodeTheme.value = defaultSettings.codeTheme; selectedCodeTheme.value = defaultSettings.codeTheme
selectedCaptionFormat.value = defaultSettings.captionFormat; selectedCaptionFormat.value = defaultSettings.captionFormat
colorMode.preference = 'system'; colorMode.preference = 'system'
applyThemeVariables(); applyThemeVariables()
}; }
// 监听预设主题色的变化,并同步更新自定义颜色选择器的值 // 监听预设主题色的变化,并同步更新自定义颜色选择器的值
watch(selectedThemeColor, (newThemeColor) => { watch(selectedThemeColor, (newThemeColor) => {
if (newThemeColor !== 'custom') { if (newThemeColor !== 'custom') {
const colorObject = themeColors.find(c => c.value === newThemeColor); const colorObject = themeColors.find(c => c.value === newThemeColor)
if (colorObject) { if (colorObject) {
customColor.value = colorObject.color; customColor.value = colorObject.color
//应用颜色 // 应用颜色
applyCustomColor(); applyCustomColor()
} }
} }
}); })
watch([selectedTheme, selectedFont, selectedFontSize, selectedThemeColor, selectedCodeTheme, selectedCaptionFormat, customColor], () => { watch([selectedTheme, selectedFont, selectedFontSize, selectedThemeColor, selectedCodeTheme, selectedCaptionFormat, customColor], () => {
applyThemeVariables(); applyThemeVariables()
if (import.meta.client) { if (import.meta.client) {
localStorage.setItem('app-theme', selectedTheme.value); localStorage.setItem('app-theme', selectedTheme.value)
localStorage.setItem('app-font', selectedFont.value); localStorage.setItem('app-font', selectedFont.value)
localStorage.setItem('app-font-size', selectedFontSize.value); localStorage.setItem('app-font-size', selectedFontSize.value)
localStorage.setItem('app-theme-color', selectedThemeColor.value); localStorage.setItem('app-theme-color', selectedThemeColor.value)
localStorage.setItem('app-code-theme', selectedCodeTheme.value); localStorage.setItem('app-code-theme', selectedCodeTheme.value)
localStorage.setItem('app-caption-format', selectedCaptionFormat.value); localStorage.setItem('app-caption-format', selectedCaptionFormat.value)
if (selectedThemeColor.value === 'custom') { if (selectedThemeColor.value === 'custom') {
localStorage.setItem('app-custom-color', customColor.value); localStorage.setItem('app-custom-color', customColor.value)
} }
} }
}); })
return { return {
initializeTheme, initializeTheme,
@@ -270,5 +270,5 @@ export function useTheme() {
applyCustomColor, applyCustomColor,
resetSettings, resetSettings,
colorMode colorMode
}; }
} }

View File

@@ -1,37 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NuxtError } from "#app"; import type { NuxtError } from '#app'
definePageMeta({ definePageMeta({
layout: "default", layout: 'default'
}); })
defineProps<{ defineProps<{
error: NuxtError; error: NuxtError
}>(); }>()
useHead({ useHead({
htmlAttrs: { htmlAttrs: {
lang: "zh-CN", lang: 'zh-CN'
}, }
}); })
useSeoMeta({ useSeoMeta({
title: "Page not found", title: 'Page not found',
description: "We are sorry but this page could not be found.", description: 'We are sorry but this page could not be found.'
}); })
const { data: navigation } = await useAsyncData("navigation", () => const { data: navigation } = await useAsyncData('navigation', () =>
queryCollectionNavigation("docs"), queryCollectionNavigation('docs')
); )
const { data: files } = useLazyAsyncData( const { data: files } = useLazyAsyncData(
"search", 'search',
() => queryCollectionSearchSections("docs"), () => queryCollectionSearchSections('docs'),
{ {
server: false, server: false
}, }
); )
provide("navigation", navigation); provide('navigation', navigation)
</script> </script>
<template> <template>
@@ -43,7 +43,7 @@ provide("navigation", navigation);
:clear="{ :clear="{
size: 'xl', size: 'xl',
icon: 'lucide-arrow-left', icon: 'lucide-arrow-left',
class: 'rounded-full', class: 'rounded-full'
}" }"
redirect="/" redirect="/"
/> />
@@ -51,7 +51,10 @@ provide("navigation", navigation);
<AppFooter /> <AppFooter />
<ClientOnly> <ClientOnly>
<LazyUContentSearch :files="files" :navigation="navigation" /> <LazyUContentSearch
:files="files"
:navigation="navigation"
/>
</ClientOnly> </ClientOnly>
</UApp> </UApp>
</template> </template>

View File

@@ -35,9 +35,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { watch } from 'vue'
const isSidebarOpen = ref(false) const isSidebarOpen = ref(false)
@@ -45,6 +44,4 @@ const isSidebarOpen = ref(false)
const toggleSidebar = () => { const toggleSidebar = () => {
isSidebarOpen.value = !isSidebarOpen.value isSidebarOpen.value = !isSidebarOpen.value
} }
</script> </script>

View File

@@ -24,7 +24,7 @@ const { data: page } = await useAsyncData(
{ {
default: () => null // 提供默认值 default: () => null // 提供默认值
} }
); )
if (!page.value) { if (!page.value) {
throw createError({ throw createError({
@@ -32,10 +32,9 @@ if (!page.value) {
statusMessage: '文档不存在', statusMessage: '文档不存在',
message: '当前页面不存在,请您检查路径是否正确', message: '当前页面不存在,请您检查路径是否正确',
fatal: true fatal: true
}); })
} }
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => { const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
return queryCollectionItemSurroundings('docs', route.path, { return queryCollectionItemSurroundings('docs', route.path, {
fields: ['description'] fields: ['description']
@@ -69,7 +68,7 @@ const editLink = computed(() => {
appConfig.github.branch, appConfig.github.branch,
appConfig.github.rootDir, appConfig.github.rootDir,
'content', 'content',
`${page.value?.stem}.${page.value?.extension}`, `${page.value?.stem}.${page.value?.extension}`
].filter(Boolean).join('/') ].filter(Boolean).join('/')
}) })
@@ -89,29 +88,60 @@ const links = computed(() => {
</script> </script>
<template> <template>
<UPage v-if="page" :class="pageFontSizeClass"> <UPage
<UPageHeader :title="page.title" :description="page.description" :headline="headline" :ui="{ v-if="page"
wrapper: 'flex-row items-center flex-wrap justify-between ', :class="pageFontSizeClass"
}"> >
<UPageHeader
:title="page.title"
:description="page.description"
:headline="headline"
:ui="{
wrapper: 'flex-row items-center flex-wrap justify-between '
}"
>
<template #links> <template #links>
<UButton v-for="(link, index) in page.links" :key="index" size="sm" v-bind="link" /> <UButton
v-for="(link, index) in page.links"
:key="index"
size="sm"
v-bind="link"
/>
<DocsPageHeaderLinks /> <DocsPageHeaderLinks />
</template> </template>
</UPageHeader> </UPageHeader>
<UPageBody> <UPageBody>
<ContentRenderer v-if="page" :value="page" /> <ContentRenderer
v-if="page"
:value="page"
/>
<USeparator> <USeparator>
<div v-if="editLink" class="flex items-center gap-2 text-sm text-muted"> <div
<UButton variant="link" color="neutral" :to="editLink" target="_blank" icon="lucide-pen" v-if="editLink"
:ui="{ leadingIcon: 'size-4' }"> class="flex items-center gap-2 text-sm text-muted"
>
<UButton
variant="link"
color="neutral"
:to="editLink"
target="_blank"
icon="lucide-pen"
:ui="{ leadingIcon: 'size-4' }"
>
编辑页面 编辑页面
</UButton> </UButton>
or or
<UButton variant="link" color="neutral" :to="`${appConfig.github.url}/issues/new/choose`" target="_blank" <UButton
icon="lucide-alert-circle" :ui="{ leadingIcon: 'size-4' }"> variant="link"
color="neutral"
:to="`${appConfig.github.url}/issues/new/choose`"
target="_blank"
icon="lucide-alert-circle"
:ui="{ leadingIcon: 'size-4' }"
>
提交问题 提交问题
</UButton> </UButton>
</div> </div>
@@ -119,15 +149,32 @@ const links = computed(() => {
<UContentSurround :surround="surround" /> <UContentSurround :surround="surround" />
</UPageBody> </UPageBody>
<template v-if="page?.body?.toc?.links?.length" #right> <template
v-if="page?.body?.toc?.links?.length"
#right
>
<div class="fixed top-24 right-15 w-auto"> <div class="fixed top-24 right-15 w-auto">
<UContentToc
:title="appConfig.toc?.title"
:links="page.body?.toc?.links"
>
<template
v-if="appConfig.toc?.bottom"
#bottom
>
<div
class="hidden lg:block space-y-6 "
:class="{ '!mt-5': page.body?.toc?.links?.length }"
>
<USeparator
v-if="page.body?.toc?.links?.length"
type="dashed"
/>
<UContentToc :title="appConfig.toc?.title" :links="page.body?.toc?.links"> <UPageLinks
<template v-if="appConfig.toc?.bottom" #bottom> :title="appConfig.toc.bottom.title"
<div class="hidden lg:block space-y-6 " :class="{ '!mt-5': page.body?.toc?.links?.length }"> :links="links"
<USeparator v-if="page.body?.toc?.links?.length" type="dashed" /> />
<UPageLinks :title="appConfig.toc.bottom.title" :links="links" />
</div> </div>
</template> </template>
</UContentToc> </UContentToc>

View File

@@ -3,8 +3,8 @@ definePageMeta({
layout: 'default' layout: 'default'
}) })
const title = "Estel Docs" const title = 'Estel Docs'
const description = "Estel Docs" const description = 'Estel Docs'
useSeoMeta({ useSeoMeta({
titleTemplate: '', titleTemplate: '',

View File

@@ -26,12 +26,12 @@ export default defineNuxtConfig({
pathMeta: { pathMeta: {
slugifyOptions: { slugifyOptions: {
// Keep everything except invalid chars, this will preserve Chinese characters // Keep everything except invalid chars, this will preserve Chinese characters
remove: /[$*+~()'"!\-=#?:@.]/g, remove: /[$*+~()'"!\-=#?:@.]/g
} }
} }
}, },
preview: { preview: {
dev:true, dev: true,
api: 'https://api.nuxt.studio' api: 'https://api.nuxt.studio'
} }
}, },
@@ -60,7 +60,7 @@ export default defineNuxtConfig({
icon: { icon: {
clientBundle: { clientBundle: {
scan: true, scan: true,
sizeLimitKb: 512, sizeLimitKb: 512
}, }
}, }
}) })