Skip to content

Commit 382bca9

Browse files
committed
feat: update pnpm configuration and add new components for documentation playground; include CodeHighlighter, Playground, Select, and Tabs components, and implement API generation logic
1 parent 0f1a215 commit 382bca9

16 files changed

Lines changed: 517 additions & 22 deletions

File tree

0 Bytes
Binary file not shown.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<template>
2+
<div
3+
v-if="highlighted"
4+
ref="highlightRef"
5+
class="p-4 font-mono text-sm leading-relaxed pointer-events-none [&_pre]:!p-3 [&_pre]:!m-0 [&_pre]:!rounded [&_pre]:!bg-transparent [&_pre]:!leading-3"
6+
v-html="highlighted"
7+
/>
8+
<div
9+
v-else-if="!highlighted"
10+
class="p-4 font-mono text-sm leading-relaxed flex items-center justify-center"
11+
>
12+
<pre class="m-0 whitespace-pre-wrap break-words text-[var(--ui-text-muted)] italic">
13+
{{ placeholder }}
14+
</pre>
15+
</div>
16+
</template>
17+
18+
<script setup lang="ts">
19+
interface Props {
20+
modelValue: string
21+
highlighted?: string
22+
editable?: boolean
23+
placeholder?: string
24+
}
25+
26+
const props = withDefaults(defineProps<Props>(), {
27+
editable: false,
28+
placeholder: '代码将在这里显示...',
29+
})
30+
31+
const emit = defineEmits<{
32+
'update:modelValue': [value: string]
33+
input: [value: string]
34+
}>()
35+
36+
function handleInput(event: Event) {
37+
const target = event.target as HTMLTextAreaElement
38+
emit('update:modelValue', target.value)
39+
emit('input', target.value)
40+
}
41+
</script>
42+
43+
<style scoped>
44+
/* 代码高亮样式 */
45+
:deep(.shiki) {
46+
margin: 0;
47+
padding: 0;
48+
background: transparent !important;
49+
}
50+
51+
:deep(.shiki code) {
52+
background: transparent !important;
53+
}
54+
</style>
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
<template>
2+
<div class="flex flex-col bg-[var(--ui-bg)] text-[var(--ui-text)]">
3+
<div class="px-4 pt-4 border-b border-[var(--ui-border)] bg-[var(--ui-bg-muted)] flex-shrink-0">
4+
<div class="flex gap-4 items-center flex-wrap">
5+
<Select
6+
v-model="selectedPreset"
7+
:options="presetOptions"
8+
@change="handlePresetChange"
9+
/>
10+
<Select
11+
v-model="selectedMode"
12+
:options="modeOptions"
13+
@change="handleModeChange"
14+
/>
15+
</div>
16+
</div>
17+
<div class="flex">
18+
<div class="flex flex-col flex-1 border-r border-[var(--ui-border)] min-w-0">
19+
<div
20+
class="px-4 py-3 border-b border-[var(--ui-border)] bg-[var(--ui-bg-muted)] flex items-center font-medium text-sm flex-shrink-0 text-[var(--ui-text)]">
21+
Swagger JSON
22+
</div>
23+
<CodeHighlighter
24+
v-model="swaggerJson"
25+
:highlighted="jsonHighlighted"
26+
:editable="true"
27+
placeholder="请输入 Swagger/OpenAPI JSON..."
28+
@input="handleJsonChange"
29+
/>
30+
</div>
31+
<div class="flex flex-col flex-1 min-w-0">
32+
<div
33+
class="px-4 py-3 border-b border-[var(--ui-border)] bg-[var(--ui-bg-muted)] flex items-center font-medium text-sm flex-shrink-0">
34+
<Tabs
35+
v-model="activeTab"
36+
:tabs="tabs"
37+
/>
38+
</div>
39+
<CodeHighlighter
40+
:model-value="activeCode"
41+
:highlighted="codeHighlighted"
42+
:editable="false"
43+
placeholder="代码将在这里显示..."
44+
/>
45+
</div>
46+
</div>
47+
</div>
48+
</template>
49+
50+
<script setup lang="ts">
51+
import { ref, computed, watch, onMounted } from 'vue'
52+
import { useDebounceFn } from '@vueuse/core'
53+
import { createHighlighter, type Highlighter } from 'shiki'
54+
import Select from './Select.vue'
55+
import Tabs from './Tabs.vue'
56+
import CodeHighlighter from './CodeHighlighter.vue'
57+
58+
// undocs/Nuxt uses @nuxtjs/color-mode: useColorMode().value is the active theme ('light' | 'dark' etc.)
59+
const colorMode = useColorMode()
60+
const isDark = computed(() => colorMode.value === 'dark')
61+
const theme = computed(() => isDark.value ? 'github-dark' : 'github-light')
62+
const swaggerJson = ref(`{
63+
"swagger": "2.0",
64+
"info": {
65+
"title": "Example API",
66+
"version": "1.0.0"
67+
},
68+
"paths": {
69+
"/users": {
70+
"get": {
71+
"summary": "Get users",
72+
"responses": {
73+
"200": {
74+
"description": "Success",
75+
"schema": {
76+
"type": "array",
77+
"items": {
78+
"$ref": "#/definitions/User"
79+
}
80+
}
81+
}
82+
}
83+
}
84+
}
85+
},
86+
"definitions": {
87+
"User": {
88+
"type": "object",
89+
"properties": {
90+
"id": {
91+
"type": "integer"
92+
},
93+
"name": {
94+
"type": "string"
95+
}
96+
}
97+
}
98+
}
99+
}`)
100+
101+
const selectedPreset = ref('axios')
102+
const selectedMode = ref('ts')
103+
const activeTab = ref<'main' | 'type'>('main')
104+
const generatedCode = ref<{ main?: string; type?: string }>({})
105+
const highlighter = ref<Highlighter | null>(null)
106+
107+
const supportsSchema = computed(() => {
108+
return selectedPreset.value === 'fetch' || selectedPreset.value === 'ofetch'
109+
})
110+
111+
const presetOptions = computed(() => [
112+
{ value: 'axios', label: 'axios' },
113+
{ value: 'fetch', label: 'fetch' },
114+
{ value: 'ky', label: 'ky' },
115+
{ value: 'got', label: 'got' },
116+
{ value: 'ofetch', label: 'ofetch' },
117+
{ value: 'uni', label: 'uni' },
118+
])
119+
120+
const modeOptions = computed(() => {
121+
const options = [
122+
{ value: 'ts', label: 'TypeScript' },
123+
{ value: 'js', label: 'JavaScript' },
124+
]
125+
if (supportsSchema.value) {
126+
options.push({ value: 'schema', label: 'Schema' })
127+
}
128+
return options
129+
})
130+
131+
const tabs = computed(() => {
132+
const items = [
133+
{ value: 'main', label: 'index.ts' },
134+
]
135+
if (generatedCode.value.type) {
136+
items.push({ value: 'type', label: 'index.type.ts' })
137+
}
138+
return items
139+
})
140+
141+
const activeCode = computed(() => {
142+
if (activeTab.value === 'main') {
143+
return generatedCode.value.main || ''
144+
} else {
145+
return generatedCode.value.type || ''
146+
}
147+
})
148+
149+
const jsonHighlighted = computed(() => {
150+
if (!highlighter.value || !swaggerJson.value.trim()) {
151+
return ''
152+
}
153+
return highlighter.value.codeToHtml(swaggerJson.value, {
154+
lang: 'json',
155+
theme: theme.value,
156+
})
157+
})
158+
159+
const codeHighlighted = computed(() => {
160+
if (!highlighter.value || !activeCode.value)
161+
return ''
162+
const lang = activeTab.value === 'type' ? 'typescript' : (selectedMode.value === 'ts' ? 'typescript' : 'javascript')
163+
return highlighter.value.codeToHtml(activeCode.value, {
164+
lang,
165+
theme: theme.value,
166+
})
167+
})
168+
169+
function handlePresetChange() {
170+
// 如果当前模式是 schema 但新预设不支持,切换到 ts
171+
if (selectedMode.value === 'schema' && !supportsSchema.value) {
172+
selectedMode.value = 'ts'
173+
}
174+
}
175+
176+
function handleModeChange() {
177+
// 如果选择了 schema 但当前预设不支持,切换到 fetch
178+
if (selectedMode.value === 'schema' && !supportsSchema.value) {
179+
selectedPreset.value = 'fetch'
180+
}
181+
}
182+
183+
async function generateCode() {
184+
if (!swaggerJson.value.trim()) {
185+
return
186+
}
187+
188+
const jsonData = JSON.parse(swaggerJson.value)
189+
190+
const response = await $fetch<{ main?: string; type?: string; error?: string }>('/api/generate', {
191+
method: 'POST',
192+
body: {
193+
swagger: jsonData,
194+
preset: selectedPreset.value,
195+
mode: selectedMode.value,
196+
},
197+
})
198+
199+
if (response.error) {
200+
return
201+
}
202+
203+
generatedCode.value = {
204+
main: response.main || '',
205+
type: response.type || '',
206+
}
207+
208+
// 如果有类型文件,默认显示主文件
209+
if (generatedCode.value.main) {
210+
activeTab.value = 'main'
211+
}
212+
}
213+
214+
function handleJsonChange() {
215+
// 自动生成代码
216+
if (!swaggerJson.value.trim()) {
217+
generatedCode.value = {}
218+
return
219+
}
220+
debouncedGenerateCode()
221+
}
222+
223+
// 使用 VueUse 的防抖函数
224+
const debouncedGenerateCode = useDebounceFn(generateCode, 500)
225+
226+
// 监听预设和模式变化,自动生成代码
227+
watch([selectedPreset, selectedMode], debouncedGenerateCode, { immediate: true })
228+
229+
// 初始化 shiki 高亮器
230+
onMounted(async () => {
231+
highlighter.value = await createHighlighter({
232+
themes: ['github-dark'],
233+
langs: ['json', 'typescript', 'javascript'],
234+
})
235+
})
236+
</script>
237+
238+
<style scoped>
239+
/* 响应式布局 */
240+
@media (max-width: 768px) {
241+
.flex.flex-1.overflow-hidden.min-h-0 {
242+
flex-direction: column;
243+
}
244+
245+
.flex.flex-col.flex-1.border-r {
246+
border-right: none;
247+
border-bottom: 1px solid var(--ui-border);
248+
}
249+
}
250+
</style>

docs/.docs/components/Select.vue

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<template>
2+
<div class="flex items-center gap-2">
3+
<label v-if="label" class="font-medium text-sm text-[var(--ui-text)]">{{ label }}</label>
4+
<select :value="modelValue" @change="handleChange" class="select-input">
5+
<option v-for="option in options" :key="option.value" :value="option.value">
6+
{{ option.label }}
7+
</option>
8+
</select>
9+
</div>
10+
</template>
11+
12+
<script setup lang="ts">
13+
interface Option {
14+
value: string
15+
label: string
16+
}
17+
18+
interface Props {
19+
modelValue: string
20+
label?: string
21+
options: Option[]
22+
}
23+
24+
const props = defineProps<Props>()
25+
26+
const emit = defineEmits<{
27+
'update:modelValue': [value: string]
28+
change: [value: string]
29+
}>()
30+
31+
function handleChange(event: Event) {
32+
const target = event.target as HTMLSelectElement
33+
emit('update:modelValue', target.value)
34+
emit('change', target.value)
35+
}
36+
</script>
37+
38+
<style scoped>
39+
.select-input {
40+
padding: 0.5rem 1rem 0.5rem 0.75rem;
41+
border: 1px solid var(--ui-border);
42+
border-radius: var(--ui-radius);
43+
background: var(--ui-bg);
44+
color: var(--ui-text);
45+
min-width: 120px;
46+
font-size: 0.875rem;
47+
cursor: pointer;
48+
transition: all 0.2s;
49+
appearance: none;
50+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ffffff' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
51+
background-repeat: no-repeat;
52+
background-position: right 0.75rem center;
53+
background-size: 12px;
54+
}
55+
56+
.select-input:hover {
57+
border-color: var(--ui-primary);
58+
}
59+
60+
.select-input:focus {
61+
outline: none;
62+
border-color: var(--ui-primary);
63+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-primary) 20%, transparent);
64+
}
65+
</style>

0 commit comments

Comments
 (0)