@movk/nuxt-docs 1.7.5 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/app/app.config.ts +5 -0
- package/app/components/content/Mermaid.vue +199 -0
- package/app/components/content/prose/ProsePre.vue +22 -0
- package/modules/config.ts +2 -1
- package/nuxt.config.ts +4 -2
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
- ⚡ **基于 Nuxt 4** - 充分利用最新的 Nuxt 框架,实现卓越性能
|
|
38
38
|
- 🎨 **采用 Nuxt UI** - 集成全面的 UI 组件库,开箱即用
|
|
39
39
|
- 📝 **MDC 语法增强** - 支持 Markdown 与 Vue 组件的无缝集成
|
|
40
|
+
- 📊 **Mermaid 图表** - 内置 Mermaid 支持,渲染流程图、时序图、类图等可视化图表,支持自动主题切换和全屏查看
|
|
40
41
|
- 🔍 **全文搜索** - 基于 Nuxt Content 的 `ContentSearch` 组件,支持键盘快捷键(⌘K)
|
|
41
42
|
- 🌙 **暗黑模式** - 支持亮色/暗色主题切换
|
|
42
43
|
- 📱 **响应式设计** - 移动优先的响应式布局
|
|
@@ -183,6 +184,55 @@ icon: i-lucide-rocket
|
|
|
183
184
|
|
|
184
185
|
了解更多关于 MDC 语法,请查看 [Nuxt Content 文档](https://content.nuxt.com/docs/files/markdown#mdc-syntax)。
|
|
185
186
|
|
|
187
|
+
### Mermaid 图表
|
|
188
|
+
|
|
189
|
+
使用 ` ```mermaid ` 代码块渲染可视化图表,支持流程图、时序图、类图等多种图表类型:
|
|
190
|
+
|
|
191
|
+
````md [md]
|
|
192
|
+
```mermaid
|
|
193
|
+
graph TD
|
|
194
|
+
A[开始] --> B{是否有效?}
|
|
195
|
+
B -->|是| C[处理数据]
|
|
196
|
+
B -->|否| D[显示错误]
|
|
197
|
+
C --> E[完成]
|
|
198
|
+
D --> E
|
|
199
|
+
```
|
|
200
|
+
````
|
|
201
|
+
|
|
202
|
+
**主要特性:**
|
|
203
|
+
- 🎨 自动主题切换(深色/浅色模式)
|
|
204
|
+
- 🔄 懒加载(仅在可见时渲染)
|
|
205
|
+
- 📋 一键复制图表代码
|
|
206
|
+
- 🖼️ 全屏查看功能
|
|
207
|
+
- 🔒 安全渲染(DOMPurify 清理)
|
|
208
|
+
|
|
209
|
+
**支持的图表类型:**
|
|
210
|
+
- **流程图**(`flowchart`/`graph`):用于展示流程和决策
|
|
211
|
+

|
|
212
|
+
- **时序图**(`sequenceDiagram`):用于展示交互时序
|
|
213
|
+

|
|
214
|
+
- **类图**(`classDiagram`):用于展示类关系
|
|
215
|
+
- **状态图**(`stateDiagram`):用于展示状态转换
|
|
216
|
+
- **甘特图**(`gantt`):用于展示项目时间线
|
|
217
|
+
- **饼图**(`pie`):用于展示数据占比
|
|
218
|
+
- **Git 图**(`gitGraph`):用于展示分支历史
|
|
219
|
+
- 以及更多 [Mermaid 支持的图表类型](https://mermaid.js.org/intro/)
|
|
220
|
+
|
|
221
|
+
**带文件名的图表:**
|
|
222
|
+
|
|
223
|
+
````md [md]
|
|
224
|
+
```mermaid [auth-flow.mmd]
|
|
225
|
+
sequenceDiagram
|
|
226
|
+
participant U as 用户
|
|
227
|
+
participant A as 认证服务
|
|
228
|
+
participant D as 数据库
|
|
229
|
+
U->>A: 登录请求
|
|
230
|
+
A->>D: 验证凭证
|
|
231
|
+
D-->>A: 返回用户信息
|
|
232
|
+
A-->>U: 返回 Token
|
|
233
|
+
```
|
|
234
|
+
````
|
|
235
|
+
|
|
186
236
|
## 🛠️ 开发
|
|
187
237
|
|
|
188
238
|
### 本地开发
|
package/app/app.config.ts
CHANGED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { hash } from 'ohash'
|
|
3
|
+
import type { IconProps } from '@nuxt/ui'
|
|
4
|
+
import type { ClassNameValue } from 'tailwind-merge'
|
|
5
|
+
import {
|
|
6
|
+
useClipboard,
|
|
7
|
+
useElementVisibility,
|
|
8
|
+
useEventListener,
|
|
9
|
+
useToggle
|
|
10
|
+
} from '@vueuse/core'
|
|
11
|
+
import { tv } from '@nuxt/ui/utils/tv'
|
|
12
|
+
|
|
13
|
+
const theme = {
|
|
14
|
+
slots: {
|
|
15
|
+
root: 'relative my-5 group border border-muted rounded-md overflow-hidden',
|
|
16
|
+
header: 'flex items-center gap-1.5 border-b border-muted bg-default px-4 py-3',
|
|
17
|
+
filename: 'text-default text-sm/6',
|
|
18
|
+
icon: 'size-4 shrink-0',
|
|
19
|
+
toolbar: 'absolute top-2 right-2 flex gap-1 z-10 opacity-0 group-hover:opacity-100 transition-opacity',
|
|
20
|
+
diagram: 'p-4 flex justify-center bg-elevated overflow-x-auto',
|
|
21
|
+
loading: 'p-4 flex items-center justify-center gap-2 text-sm text-muted',
|
|
22
|
+
error: 'p-4 flex items-center justify-center gap-2 text-sm text-error bg-error/10'
|
|
23
|
+
},
|
|
24
|
+
variants: {
|
|
25
|
+
fullscreen: {
|
|
26
|
+
true: {
|
|
27
|
+
root: 'fixed inset-0 z-50 m-0 rounded-none bg-default flex flex-col',
|
|
28
|
+
diagram: 'flex-1 overflow-auto',
|
|
29
|
+
toolbar: 'opacity-100'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
filename: {
|
|
33
|
+
true: {
|
|
34
|
+
root: ''
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MermaidProps {
|
|
41
|
+
/** 图表代码 */
|
|
42
|
+
code: string
|
|
43
|
+
/**
|
|
44
|
+
* 图标
|
|
45
|
+
* @IconifyIcon
|
|
46
|
+
*/
|
|
47
|
+
icon?: IconProps['name']
|
|
48
|
+
/** 文件名 */
|
|
49
|
+
filename?: string
|
|
50
|
+
class?: ClassNameValue
|
|
51
|
+
ui?: Partial<typeof theme.slots>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface MermaidSlots {
|
|
55
|
+
default?(props: {}): any
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const props = defineProps<MermaidProps>()
|
|
59
|
+
|
|
60
|
+
defineSlots<MermaidSlots>()
|
|
61
|
+
|
|
62
|
+
const appConfig = useAppConfig() as { ui?: { icons?: { copy?: string, copyCheck?: string }, prose?: { mermaid?: typeof theme } } }
|
|
63
|
+
const colorMode = useColorMode()
|
|
64
|
+
|
|
65
|
+
const ui = computed(() => tv({
|
|
66
|
+
extend: tv(theme),
|
|
67
|
+
...(appConfig.ui?.prose?.mermaid || {})
|
|
68
|
+
})({
|
|
69
|
+
fullscreen: isFullscreen.value,
|
|
70
|
+
filename: !!props.filename
|
|
71
|
+
}))
|
|
72
|
+
|
|
73
|
+
const copyIcon = computed(() => appConfig.ui?.icons?.copy || 'i-lucide-copy')
|
|
74
|
+
const copyCheckIcon = computed(() => appConfig.ui?.icons?.copyCheck || 'i-lucide-check')
|
|
75
|
+
|
|
76
|
+
const mermaidId = computed(() => `mermaid-${hash(props.code)}`)
|
|
77
|
+
const mermaidTheme = computed(() => colorMode.value === 'dark' ? 'dark' : 'default')
|
|
78
|
+
|
|
79
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
80
|
+
const diagramRef = ref<HTMLElement | null>(null)
|
|
81
|
+
const isRendered = ref(false)
|
|
82
|
+
const hasError = ref(false)
|
|
83
|
+
const errorMessage = ref('')
|
|
84
|
+
|
|
85
|
+
const [isFullscreen, toggleFullscreen] = useToggle(false)
|
|
86
|
+
const { copy, copied } = useClipboard({ source: () => props.code })
|
|
87
|
+
const isVisible = useElementVisibility(containerRef)
|
|
88
|
+
|
|
89
|
+
async function renderMermaid() {
|
|
90
|
+
if (!props.code || isRendered.value || !diagramRef.value) return
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// 动态导入 mermaid 和 dompurify,仅在客户端执行
|
|
94
|
+
const [mermaid, DOMPurify] = await Promise.all([
|
|
95
|
+
import('mermaid').then(m => m.default),
|
|
96
|
+
import('dompurify').then(m => m.default)
|
|
97
|
+
])
|
|
98
|
+
mermaid.initialize({
|
|
99
|
+
startOnLoad: false,
|
|
100
|
+
theme: mermaidTheme.value,
|
|
101
|
+
securityLevel: 'strict',
|
|
102
|
+
fontFamily: 'inherit'
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const { svg } = await mermaid.render(mermaidId.value, props.code)
|
|
106
|
+
const sanitized = DOMPurify.sanitize(svg, {
|
|
107
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
108
|
+
ADD_TAGS: ['foreignObject']
|
|
109
|
+
})
|
|
110
|
+
diagramRef.value.innerHTML = sanitized
|
|
111
|
+
isRendered.value = true
|
|
112
|
+
} catch (e) {
|
|
113
|
+
hasError.value = true
|
|
114
|
+
errorMessage.value = e instanceof Error ? e.message : 'Mermaid render failed'
|
|
115
|
+
console.error('[Mermaid]', errorMessage.value)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function reRender() {
|
|
120
|
+
if (!diagramRef.value) return
|
|
121
|
+
isRendered.value = false
|
|
122
|
+
hasError.value = false
|
|
123
|
+
diagramRef.value.innerHTML = ''
|
|
124
|
+
await renderMermaid()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
watch(
|
|
128
|
+
[isVisible, diagramRef],
|
|
129
|
+
([visible, el]) => {
|
|
130
|
+
if (visible && el && !isRendered.value) {
|
|
131
|
+
renderMermaid()
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{ immediate: true }
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
watch(mermaidTheme, () => {
|
|
138
|
+
if (isRendered.value) reRender()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
watch(() => props.code, () => {
|
|
142
|
+
if (isVisible.value) reRender()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
useEventListener('keydown', (e: KeyboardEvent) => {
|
|
146
|
+
if (e.key === 'Escape' && isFullscreen.value) {
|
|
147
|
+
isFullscreen.value = false
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<template>
|
|
153
|
+
<div ref="containerRef" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
|
154
|
+
<div v-if="filename" :class="ui.header({ class: props.ui?.header })">
|
|
155
|
+
<UIcon v-if="icon" :name="icon" :class="ui.icon({ class: props.ui?.icon })" />
|
|
156
|
+
<ProseCodeIcon v-else :filename="filename" :class="ui.icon({ class: props.ui?.icon })" />
|
|
157
|
+
<span :class="ui.filename({ class: props.ui?.filename })">{{ filename }}</span>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div v-if="isRendered" :class="ui.toolbar({ class: props.ui?.toolbar })">
|
|
161
|
+
<UButton
|
|
162
|
+
:icon="copied ? copyCheckIcon : copyIcon"
|
|
163
|
+
color="neutral"
|
|
164
|
+
variant="ghost"
|
|
165
|
+
size="xs"
|
|
166
|
+
:aria-label="copied ? 'Copied' : 'Copy code'"
|
|
167
|
+
@click="copy()"
|
|
168
|
+
/>
|
|
169
|
+
<UButton
|
|
170
|
+
:icon="isFullscreen ? 'i-lucide-minimize-2' : 'i-lucide-maximize-2'"
|
|
171
|
+
color="neutral"
|
|
172
|
+
variant="ghost"
|
|
173
|
+
size="xs"
|
|
174
|
+
:aria-label="isFullscreen ? 'Exit fullscreen' : 'Fullscreen'"
|
|
175
|
+
@click="toggleFullscreen()"
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div v-if="hasError" :class="ui.error({ class: props.ui?.error })">
|
|
180
|
+
<UIcon name="i-lucide-alert-triangle" class="size-4" />
|
|
181
|
+
<span>{{ errorMessage }}</span>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<template v-else>
|
|
185
|
+
<div v-if="!isRendered" :class="ui.loading({ class: props.ui?.loading })">
|
|
186
|
+
<UIcon name="i-lucide-loader-2" class="size-4 animate-spin" />
|
|
187
|
+
<span>Loading diagram...</span>
|
|
188
|
+
</div>
|
|
189
|
+
<div v-show="isRendered" ref="diagramRef" :class="ui.diagram({ class: props.ui?.diagram })" />
|
|
190
|
+
</template>
|
|
191
|
+
</div>
|
|
192
|
+
</template>
|
|
193
|
+
|
|
194
|
+
<style>
|
|
195
|
+
[class*="diagram"] :deep(svg) {
|
|
196
|
+
max-width: 100%;
|
|
197
|
+
height: auto;
|
|
198
|
+
}
|
|
199
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ProsePreProps } from '@nuxt/ui'
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import NuxtUIProsePre from '@nuxt/ui/components/prose/Pre.vue'
|
|
5
|
+
import Mermaid from '../Mermaid.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<ProsePreProps>()
|
|
8
|
+
|
|
9
|
+
const isMermaid = computed(() => props.language === 'mermaid')
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<Mermaid
|
|
14
|
+
v-if="isMermaid"
|
|
15
|
+
:code="props.code || ''"
|
|
16
|
+
:filename="props.filename"
|
|
17
|
+
:icon="props.icon"
|
|
18
|
+
/>
|
|
19
|
+
<NuxtUIProsePre v-else v-bind="props">
|
|
20
|
+
<slot />
|
|
21
|
+
</NuxtUIProsePre>
|
|
22
|
+
</template>
|
package/modules/config.ts
CHANGED
|
@@ -66,6 +66,7 @@ export default defineNuxtModule({
|
|
|
66
66
|
resolve('../app/components/content/ComponentProps.vue'),
|
|
67
67
|
resolve('../app/components/content/ComponentSlots.vue'),
|
|
68
68
|
resolve('../app/components/content/PageLastCommit.vue'),
|
|
69
|
+
resolve('../app/components/content/Mermaid.vue'),
|
|
69
70
|
resolve('./ai-chat/runtime/components/AiChatToolCall.vue'),
|
|
70
71
|
resolve('./ai-chat/runtime/components/AiChatReasoning.vue'),
|
|
71
72
|
resolve('./ai-chat/runtime/components/AiChatSlideoverFaq.vue'),
|
|
@@ -78,7 +79,7 @@ export default defineNuxtModule({
|
|
|
78
79
|
join(dir, 'templates/*/app/components')
|
|
79
80
|
]
|
|
80
81
|
|
|
81
|
-
// @ts-ignore - component-meta
|
|
82
|
+
// @ts-ignore - component-meta is not typed
|
|
82
83
|
nuxt.hook('component-meta:extend', (options: any) => {
|
|
83
84
|
options.exclude = [
|
|
84
85
|
...(options.exclude || []),
|
package/nuxt.config.ts
CHANGED
|
@@ -24,7 +24,9 @@ export default defineNuxtConfig({
|
|
|
24
24
|
config.optimizeDeps.include.push(
|
|
25
25
|
'@nuxt/content > slugify',
|
|
26
26
|
'extend',
|
|
27
|
-
'@ai-sdk/gateway > @vercel/oidc'
|
|
27
|
+
'@ai-sdk/gateway > @vercel/oidc',
|
|
28
|
+
'mermaid',
|
|
29
|
+
'dompurify'
|
|
28
30
|
)
|
|
29
31
|
config.optimizeDeps.include = config.optimizeDeps.include
|
|
30
32
|
.map(id => id.replace(/^@nuxt\/content > /, '@movk/nuxt-docs > @nuxt/content > '))
|
|
@@ -42,7 +44,7 @@ export default defineNuxtConfig({
|
|
|
42
44
|
build: {
|
|
43
45
|
markdown: {
|
|
44
46
|
highlight: {
|
|
45
|
-
langs: ['bash', 'diff', 'json', 'js', 'ts', 'html', 'css', 'vue', 'shell', 'mdc', 'md', 'yaml']
|
|
47
|
+
langs: ['bash', 'diff', 'json', 'js', 'ts', 'html', 'css', 'vue', 'shell', 'mdc', 'md', 'yaml', 'mermaid']
|
|
46
48
|
},
|
|
47
49
|
remarkPlugins: {
|
|
48
50
|
'remark-mdc': {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@movk/nuxt-docs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Modern Nuxt 4 documentation theme with auto-generated component docs, AI chat assistant, MCP server, and complete developer experience optimization.",
|
|
7
7
|
"author": "YiXuan <mhaibaraai@gmail.com>",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@ai-sdk/gateway": "^3.0.
|
|
32
|
-
"@ai-sdk/mcp": "^1.0.
|
|
33
|
-
"@ai-sdk/vue": "^3.0.
|
|
31
|
+
"@ai-sdk/gateway": "^3.0.17",
|
|
32
|
+
"@ai-sdk/mcp": "^1.0.11",
|
|
33
|
+
"@ai-sdk/vue": "^3.0.42",
|
|
34
34
|
"@iconify-json/lucide": "^1.2.86",
|
|
35
35
|
"@iconify-json/ph": "^1.2.2",
|
|
36
|
-
"@iconify-json/tabler": "^1.2.26",
|
|
37
36
|
"@iconify-json/simple-icons": "^1.2.67",
|
|
37
|
+
"@iconify-json/tabler": "^1.2.26",
|
|
38
38
|
"@iconify-json/vscode-icons": "^1.2.40",
|
|
39
39
|
"@movk/core": "^1.1.0",
|
|
40
40
|
"@nuxt/a11y": "^1.0.0-alpha.1",
|
|
@@ -45,15 +45,17 @@
|
|
|
45
45
|
"@nuxtjs/mcp-toolkit": "^0.6.2",
|
|
46
46
|
"@nuxtjs/seo": "^3.3.0",
|
|
47
47
|
"@octokit/rest": "^22.0.1",
|
|
48
|
-
"@openrouter/ai-sdk-provider": "^
|
|
48
|
+
"@openrouter/ai-sdk-provider": "^2.0.0",
|
|
49
49
|
"@vercel/analytics": "^1.6.1",
|
|
50
50
|
"@vercel/speed-insights": "^1.3.1",
|
|
51
51
|
"@vueuse/core": "^14.1.0",
|
|
52
52
|
"@vueuse/nuxt": "^14.1.0",
|
|
53
|
-
"ai": "^6.0.
|
|
53
|
+
"ai": "^6.0.42",
|
|
54
54
|
"defu": "^6.1.4",
|
|
55
|
+
"dompurify": "^3.2.6",
|
|
55
56
|
"exsolve": "^1.0.8",
|
|
56
57
|
"git-url-parse": "^16.1.0",
|
|
58
|
+
"mermaid": "^11.12.2",
|
|
57
59
|
"motion-v": "^1.9.0",
|
|
58
60
|
"nuxt": "^4.2.2",
|
|
59
61
|
"nuxt-component-meta": "^0.17.1",
|