@antdv-next/docs-plugins 0.0.1 → 0.0.2
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.
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CSSProperties } from 'vue'
|
|
3
|
+
import type { DemoExtraFile, DemoModule, DemoSourceData } from '../../demo/types'
|
|
4
|
+
import { CheckOutlined, CodeOutlined, CopyOutlined, EditOutlined, ThunderboltOutlined } from '@antdv-next/icons'
|
|
5
|
+
import { aquaBlue, atomDark } from '@codesandbox/sandpack-themes'
|
|
6
|
+
import { useClipboard, useDebounceFn } from '@vueuse/core'
|
|
7
|
+
import { Alert, Flex, Skeleton, Spin, Tabs, Tooltip } from 'antdv-next'
|
|
8
|
+
import { createStyles } from 'antdv-style'
|
|
9
|
+
import { SandpackProvider } from 'sandpack-vue3'
|
|
10
|
+
import { loadDemo } from 'virtual:demos'
|
|
11
|
+
import { computed, defineAsyncComponent, markRaw, nextTick, onBeforeUnmount, shallowRef, watch } from 'vue'
|
|
12
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
13
|
+
import { getDemoId } from '../../demo/get-demo-id'
|
|
14
|
+
import CodeEditorBridge from './code-editor-bridge.vue'
|
|
15
|
+
import { compileSfcSource } from './compile-sfc'
|
|
16
|
+
import { resolveLabel, useDemoContext } from './context'
|
|
17
|
+
import ExpandIcon from './expand-icon.vue'
|
|
18
|
+
import ExternalLinkIcon from './external-link-icon.vue'
|
|
19
|
+
|
|
20
|
+
defineOptions({
|
|
21
|
+
name: 'DemoPanel',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const props = withDefaults(defineProps<{
|
|
25
|
+
src: string
|
|
26
|
+
/** 使用 iframe 模式渲染独立示例页(需要站点提供 iframeRenderer) */
|
|
27
|
+
iframe?: string
|
|
28
|
+
/** 紧凑模式:预览区无内边距 */
|
|
29
|
+
compact?: boolean
|
|
30
|
+
/** 'grey' 时预览区使用灰色背景 */
|
|
31
|
+
background?: string
|
|
32
|
+
simplify?: boolean
|
|
33
|
+
/** Debug demos are shown in development only and hidden in the production docs build. */
|
|
34
|
+
debug?: boolean
|
|
35
|
+
}>(), {
|
|
36
|
+
iframe: undefined,
|
|
37
|
+
compact: false,
|
|
38
|
+
background: '',
|
|
39
|
+
simplify: false,
|
|
40
|
+
debug: false,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const TabPane = Tabs.TabPane
|
|
44
|
+
|
|
45
|
+
const context = useDemoContext()
|
|
46
|
+
|
|
47
|
+
const useStyles = createStyles(({ token }) => ({
|
|
48
|
+
root: {
|
|
49
|
+
'breakInside': 'avoid',
|
|
50
|
+
'display': 'flow-root',
|
|
51
|
+
'overflow': 'hidden',
|
|
52
|
+
'position': 'relative',
|
|
53
|
+
'boxSizing': 'border-box',
|
|
54
|
+
'border': `1px solid ${token.colorSplit}`,
|
|
55
|
+
'borderRadius': token.borderRadiusLG,
|
|
56
|
+
'background': token.colorBgContainer,
|
|
57
|
+
'transition': 'border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease',
|
|
58
|
+
'margin': '16px 0',
|
|
59
|
+
'&.border-primary': {
|
|
60
|
+
borderColor: token.colorPrimary,
|
|
61
|
+
boxShadow: `0 0 0 3px color-mix(in srgb, ${token.colorPrimary} 12%, transparent)`,
|
|
62
|
+
},
|
|
63
|
+
'&.ant-doc-demo-box-debug': {
|
|
64
|
+
borderColor: '#d3adf7',
|
|
65
|
+
},
|
|
66
|
+
'& .ant-doc-demo-box-demo': {
|
|
67
|
+
padding: '24px',
|
|
68
|
+
borderBottom: `1px solid ${token.colorSplit}`,
|
|
69
|
+
borderRadius: '8px 8px 0 0',
|
|
70
|
+
background: token.colorBgContainer,
|
|
71
|
+
},
|
|
72
|
+
'& .ant-doc-demo-box-skeleton': {
|
|
73
|
+
minHeight: 160,
|
|
74
|
+
},
|
|
75
|
+
'&.ant-doc-demo-box-simplify': {
|
|
76
|
+
borderRadius: 0,
|
|
77
|
+
background: 'transparent',
|
|
78
|
+
},
|
|
79
|
+
'&.ant-doc-demo-box-simplify .ant-doc-demo-box-demo': {
|
|
80
|
+
padding: 0,
|
|
81
|
+
borderBottom: 0,
|
|
82
|
+
background: 'transparent',
|
|
83
|
+
},
|
|
84
|
+
'& .ant-doc-demo-box-meta.markdown': {
|
|
85
|
+
position: 'relative',
|
|
86
|
+
width: '100%',
|
|
87
|
+
fontSize: 14,
|
|
88
|
+
borderRadius: '0 0 6px 6px',
|
|
89
|
+
transition: 'background-color 0.4s',
|
|
90
|
+
},
|
|
91
|
+
'& .ant-doc-demo-box-meta-description': {
|
|
92
|
+
padding: '18px 12px 24px',
|
|
93
|
+
},
|
|
94
|
+
'& .ant-doc-demo-box-meta-description p': {
|
|
95
|
+
margin: 0,
|
|
96
|
+
},
|
|
97
|
+
'& .ant-doc-demo-box-title': {
|
|
98
|
+
position: 'absolute',
|
|
99
|
+
top: -16,
|
|
100
|
+
marginLeft: 16,
|
|
101
|
+
padding: '1px 8px',
|
|
102
|
+
borderRadius: '6px 6px 0 0',
|
|
103
|
+
backgroundColor: token.colorBgContainer,
|
|
104
|
+
transition: 'background-color 0.4s',
|
|
105
|
+
},
|
|
106
|
+
'& .ant-doc-demo-box-title a': {
|
|
107
|
+
color: token.colorText,
|
|
108
|
+
textDecoration: 'none',
|
|
109
|
+
fontSize: 16,
|
|
110
|
+
fontWeight: 500,
|
|
111
|
+
},
|
|
112
|
+
'& .ant-doc-demo-box-actions': {
|
|
113
|
+
display: 'flex',
|
|
114
|
+
justifyContent: 'center',
|
|
115
|
+
padding: '12px 0',
|
|
116
|
+
borderTop: `1px dashed ${token.colorSplit}`,
|
|
117
|
+
opacity: 0.7,
|
|
118
|
+
transition: 'opacity 0.3s',
|
|
119
|
+
},
|
|
120
|
+
'&:hover .ant-doc-demo-box-actions': {
|
|
121
|
+
opacity: 1,
|
|
122
|
+
},
|
|
123
|
+
'& .ant-doc-demo-box-code-action': {
|
|
124
|
+
display: 'inline-flex',
|
|
125
|
+
alignItems: 'center',
|
|
126
|
+
justifyContent: 'center',
|
|
127
|
+
width: 16,
|
|
128
|
+
height: 16,
|
|
129
|
+
border: 0,
|
|
130
|
+
padding: 0,
|
|
131
|
+
background: 'transparent',
|
|
132
|
+
color: token.colorTextSecondary,
|
|
133
|
+
cursor: 'pointer',
|
|
134
|
+
transition: 'color 0.24s ease',
|
|
135
|
+
},
|
|
136
|
+
'& .ant-doc-demo-box-code-action:hover': {
|
|
137
|
+
color: token.colorPrimary,
|
|
138
|
+
},
|
|
139
|
+
'& .ant-doc-demo-box-edit-icon': {
|
|
140
|
+
marginInlineStart: 4,
|
|
141
|
+
color: token.colorTextTertiary,
|
|
142
|
+
},
|
|
143
|
+
'& .ant-doc-demo-box-code': {
|
|
144
|
+
'position': 'relative',
|
|
145
|
+
'lineHeight': 2,
|
|
146
|
+
'padding': `${token.paddingSM}px ${token.padding}px`,
|
|
147
|
+
// 与 antdv-next 原 code-demo 一致:去除 sandpack 默认表面样式并隐藏 Run 按钮
|
|
148
|
+
'& .sp-wrapper': {
|
|
149
|
+
background: 'transparent !important',
|
|
150
|
+
},
|
|
151
|
+
'& .sp-layout': {
|
|
152
|
+
background: 'transparent !important',
|
|
153
|
+
border: 'none !important',
|
|
154
|
+
},
|
|
155
|
+
'& .cm-editor': {
|
|
156
|
+
'background': 'transparent',
|
|
157
|
+
'fontSize': 14,
|
|
158
|
+
'& .cm-content': {
|
|
159
|
+
lineHeight: 2,
|
|
160
|
+
},
|
|
161
|
+
'& .cm-activeLine, & .cm-activeLineGutter': {
|
|
162
|
+
background: 'transparent',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
'& .cm-gutters': {
|
|
166
|
+
background: 'transparent',
|
|
167
|
+
border: 'none',
|
|
168
|
+
},
|
|
169
|
+
'& .sp-stack': {
|
|
170
|
+
height: 'auto !important',
|
|
171
|
+
background: 'transparent',
|
|
172
|
+
},
|
|
173
|
+
'& [class*="sp-code-editor"]': {
|
|
174
|
+
background: 'transparent !important',
|
|
175
|
+
},
|
|
176
|
+
// 隐藏 sandpack 内置 Run 按钮与只读徽标
|
|
177
|
+
'& .sp-button, & .sp-read-only': {
|
|
178
|
+
display: 'none',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
'& .ant-doc-demo-box-code-loading': {
|
|
182
|
+
display: 'flex',
|
|
183
|
+
justifyContent: 'center',
|
|
184
|
+
paddingBlock: token.paddingLG,
|
|
185
|
+
},
|
|
186
|
+
'& .ant-doc-demo-box-compile-error': {
|
|
187
|
+
margin: 0,
|
|
188
|
+
padding: '8px 16px',
|
|
189
|
+
background: token.colorErrorBg,
|
|
190
|
+
color: token.colorError,
|
|
191
|
+
fontSize: 12,
|
|
192
|
+
lineHeight: 1.6,
|
|
193
|
+
whiteSpace: 'pre-wrap',
|
|
194
|
+
},
|
|
195
|
+
'& .ant-doc-demo-box-code-tabs': {
|
|
196
|
+
borderTop: `1px dashed ${token.colorSplit}`,
|
|
197
|
+
},
|
|
198
|
+
'& .ant-doc-demo-box-code-tabs .ant-tabs-nav': {
|
|
199
|
+
marginBottom: 0,
|
|
200
|
+
},
|
|
201
|
+
'& .ant-doc-demo-box-code-tabs .ant-tabs-tab': {
|
|
202
|
+
fontSize: 12,
|
|
203
|
+
},
|
|
204
|
+
'& .ant-doc-demo-box-code-copy': {
|
|
205
|
+
position: 'absolute',
|
|
206
|
+
top: 10,
|
|
207
|
+
right: 10,
|
|
208
|
+
zIndex: 1,
|
|
209
|
+
display: 'inline-flex',
|
|
210
|
+
alignItems: 'center',
|
|
211
|
+
justifyContent: 'center',
|
|
212
|
+
width: 24,
|
|
213
|
+
height: 24,
|
|
214
|
+
border: 0,
|
|
215
|
+
padding: 0,
|
|
216
|
+
background: 'transparent',
|
|
217
|
+
color: token.colorIcon,
|
|
218
|
+
cursor: 'pointer',
|
|
219
|
+
},
|
|
220
|
+
'& .ant-doc-demo-box-code-copied': {
|
|
221
|
+
color: token.colorSuccess,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
}))
|
|
225
|
+
|
|
226
|
+
function t(key: Parameters<typeof resolveLabel>[1]) {
|
|
227
|
+
return resolveLabel(context, key)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { styles } = useStyles()
|
|
231
|
+
|
|
232
|
+
// Debug demos are visible while developing but stripped from the production docs.
|
|
233
|
+
const hidden = computed(() => Boolean(props.debug) && import.meta.env.PROD)
|
|
234
|
+
|
|
235
|
+
// demo 模块经 virtual:demos 注册表懒加载(代码分割,按需拉取)
|
|
236
|
+
const demo = shallowRef<DemoModule | undefined>(undefined)
|
|
237
|
+
let demoLoadToken = 0
|
|
238
|
+
|
|
239
|
+
watch(() => props.src, (newSrc) => {
|
|
240
|
+
const token = ++demoLoadToken
|
|
241
|
+
demo.value = undefined
|
|
242
|
+
if (!newSrc)
|
|
243
|
+
return
|
|
244
|
+
loadDemo(newSrc).then((mod) => {
|
|
245
|
+
if (token === demoLoadToken && mod)
|
|
246
|
+
demo.value = mod
|
|
247
|
+
}).catch(() => {})
|
|
248
|
+
}, { immediate: true })
|
|
249
|
+
|
|
250
|
+
// 按需加载的源码数据
|
|
251
|
+
const sourceData = shallowRef<DemoSourceData | null>(null)
|
|
252
|
+
const sourceLoading = shallowRef(false)
|
|
253
|
+
const sourceLoadError = shallowRef<Error | null>(null)
|
|
254
|
+
let sourceLoadPromise: Promise<void> | null = null
|
|
255
|
+
let sourceAbortController: AbortController | null = null
|
|
256
|
+
// HMR 发生在面板收起期间时标记过期,下次展开时重新加载
|
|
257
|
+
let sourceStale = false
|
|
258
|
+
|
|
259
|
+
function releaseSource() {
|
|
260
|
+
sourceAbortController?.abort()
|
|
261
|
+
sourceAbortController = null
|
|
262
|
+
sourceData.value = null
|
|
263
|
+
sourceLoadError.value = null
|
|
264
|
+
sourceLoadPromise = null
|
|
265
|
+
sourceLoading.value = false
|
|
266
|
+
sourceStale = false
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function ensureSourceLoaded() {
|
|
270
|
+
// 如果源码已过期(HMR 发生在收起期间),先释放旧数据
|
|
271
|
+
if (sourceStale)
|
|
272
|
+
releaseSource()
|
|
273
|
+
if (sourceData.value || !demo.value)
|
|
274
|
+
return
|
|
275
|
+
if (sourceLoadPromise)
|
|
276
|
+
return sourceLoadPromise
|
|
277
|
+
|
|
278
|
+
const currentDemo = demo.value
|
|
279
|
+
const abortController = new AbortController()
|
|
280
|
+
sourceAbortController = abortController
|
|
281
|
+
sourceLoading.value = true
|
|
282
|
+
sourceLoadError.value = null
|
|
283
|
+
|
|
284
|
+
const request = currentDemo
|
|
285
|
+
.loadSource(abortController.signal)
|
|
286
|
+
.then((data) => {
|
|
287
|
+
if (demo.value === currentDemo && !abortController.signal.aborted)
|
|
288
|
+
sourceData.value = data
|
|
289
|
+
})
|
|
290
|
+
.catch((error) => {
|
|
291
|
+
if (abortController.signal.aborted)
|
|
292
|
+
return
|
|
293
|
+
const loadError
|
|
294
|
+
= error instanceof Error ? error : new Error(String(error))
|
|
295
|
+
if (demo.value === currentDemo)
|
|
296
|
+
sourceLoadError.value = loadError
|
|
297
|
+
throw loadError
|
|
298
|
+
})
|
|
299
|
+
.finally(() => {
|
|
300
|
+
if (sourceLoadPromise === request) {
|
|
301
|
+
sourceAbortController = null
|
|
302
|
+
sourceLoadPromise = null
|
|
303
|
+
sourceLoading.value = false
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
sourceLoadPromise = request
|
|
308
|
+
return request
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const route = useRoute()
|
|
312
|
+
const router = useRouter()
|
|
313
|
+
|
|
314
|
+
const hasJsSource = computed(() => {
|
|
315
|
+
const jsSource = sourceData.value?.jsSource?.trim()
|
|
316
|
+
return Boolean(jsSource)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const extraFiles = computed<DemoExtraFile[]>(() => sourceData.value?.extraFiles ?? [])
|
|
320
|
+
|
|
321
|
+
// 与线上一致:仅当存在 JS 版本或伴生文件时才显示代码页签栏
|
|
322
|
+
const hasCodeTabs = computed(() => hasJsSource.value || extraFiles.value.length > 0)
|
|
323
|
+
|
|
324
|
+
const codeTabKeys = computed(() => {
|
|
325
|
+
const keys: string[] = ['ts']
|
|
326
|
+
if (hasJsSource.value)
|
|
327
|
+
keys.push('js')
|
|
328
|
+
for (const file of extraFiles.value)
|
|
329
|
+
keys.push(file.name)
|
|
330
|
+
return keys
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// ts/js 切换:每个 Demo 实例维护独立的偏好,初始值取自站点提供的持久化
|
|
334
|
+
// (不通过 computed 依赖全局状态,避免一个 demo 切换导致所有 demo 同步切换)
|
|
335
|
+
const localTsJs = shallowRef<'ts' | 'js'>(context.preferredCodeType?.get() ?? 'ts')
|
|
336
|
+
const localCodeKey = shallowRef<string | null>(null)
|
|
337
|
+
|
|
338
|
+
const activeCodeType = computed<string>({
|
|
339
|
+
get() {
|
|
340
|
+
if (localCodeKey.value && codeTabKeys.value.includes(localCodeKey.value))
|
|
341
|
+
return localCodeKey.value
|
|
342
|
+
const preferred = localTsJs.value
|
|
343
|
+
if (codeTabKeys.value.includes(preferred))
|
|
344
|
+
return preferred
|
|
345
|
+
return 'ts'
|
|
346
|
+
},
|
|
347
|
+
set(value) {
|
|
348
|
+
if (value === 'ts' || value === 'js') {
|
|
349
|
+
localTsJs.value = value
|
|
350
|
+
context.preferredCodeType?.set(value)
|
|
351
|
+
localCodeKey.value = null
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
localCodeKey.value = value
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const activeExtraFile = computed(() =>
|
|
360
|
+
extraFiles.value.find(file => file.name === activeCodeType.value),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
/** 将 demo 相对导入路径映射为 sandpack 虚拟文件路径 */
|
|
364
|
+
function extraFileToSandpackPath(name: string) {
|
|
365
|
+
return `/src/${name.replace(/^(\.\.?\/)+/, '')}`
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** 代码 tab 显示名(去掉 ./ 前缀) */
|
|
369
|
+
function displayFileName(name: string) {
|
|
370
|
+
return name.replace(/^(\.\.?\/)+/, '')
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const mainSourceCode = computed(() => {
|
|
374
|
+
if (activeCodeType.value === 'js')
|
|
375
|
+
return sourceData.value?.jsSource ?? sourceData.value?.source ?? ''
|
|
376
|
+
return sourceData.value?.source ?? ''
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// 编辑器当前展示的源码(主 demo 或伴生文件)
|
|
380
|
+
const activeSourceCode = computed(() => {
|
|
381
|
+
if (activeExtraFile.value)
|
|
382
|
+
return activeExtraFile.value.code
|
|
383
|
+
return mainSourceCode.value
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
const description = computed(() => {
|
|
387
|
+
const locales = demo.value?.locales ?? {}
|
|
388
|
+
const locale = context.locale?.() ?? 'zh-CN'
|
|
389
|
+
const localeData = locales[locale] || locales['zh-CN'] || locales['en-US'] || Object.values(locales)[0] || {}
|
|
390
|
+
return localeData?.html ?? ''
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
const component = computed(() => typeof demo.value?.component === 'function' ? defineAsyncComponent(demo.value.component) : demo.value?.component)
|
|
394
|
+
const id = computed(() => {
|
|
395
|
+
if (!props.src)
|
|
396
|
+
return ''
|
|
397
|
+
return getDemoId(props.src)
|
|
398
|
+
})
|
|
399
|
+
const showCode = shallowRef(false)
|
|
400
|
+
const liveComponent = shallowRef<any>(null)
|
|
401
|
+
const compileError = shallowRef<string | null>(null)
|
|
402
|
+
const currentCode = shallowRef<string | null>(null)
|
|
403
|
+
const editorBridgeRef = shallowRef<{ resetCode: (code: string) => void }>()
|
|
404
|
+
|
|
405
|
+
function handleShowCode() {
|
|
406
|
+
showCode.value = !showCode.value
|
|
407
|
+
if (!showCode.value) {
|
|
408
|
+
// 收起时只重置实时编辑状态,保留源码以便快速重新展开
|
|
409
|
+
liveComponent.value = null
|
|
410
|
+
compileError.value = null
|
|
411
|
+
currentCode.value = null
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
// 展开时按需加载源码(ensureSourceLoaded 内部会处理过期重载)
|
|
415
|
+
void ensureSourceLoaded().catch(() => {})
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 切换 demo 时释放旧源码并重置多文件 tab
|
|
420
|
+
watch(demo, () => {
|
|
421
|
+
localCodeKey.value = null
|
|
422
|
+
releaseSource()
|
|
423
|
+
}, { flush: 'sync' })
|
|
424
|
+
|
|
425
|
+
// 展开代码面板时触发加载(收起时不释放,保留源码)
|
|
426
|
+
watch([showCode, demo], ([visible, currentDemo]) => {
|
|
427
|
+
if (!visible)
|
|
428
|
+
return
|
|
429
|
+
if (currentDemo)
|
|
430
|
+
void ensureSourceLoaded().catch(() => {})
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// HMR 触发的 sourceVersion 变化:展开时立即重载,收起时标记过期
|
|
434
|
+
watch(
|
|
435
|
+
() => demo.value?.sourceVersion,
|
|
436
|
+
(version, previousVersion) => {
|
|
437
|
+
if (version === previousVersion)
|
|
438
|
+
return
|
|
439
|
+
// 如果用户正在编辑,暂缓重载,避免覆盖编辑内容
|
|
440
|
+
if (currentCode.value !== null || liveComponent.value !== null) {
|
|
441
|
+
sourceStale = true
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
if (showCode.value) {
|
|
445
|
+
releaseSource()
|
|
446
|
+
void ensureSourceLoaded().catch(() => {})
|
|
447
|
+
}
|
|
448
|
+
else if (sourceData.value) {
|
|
449
|
+
sourceStale = true
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
onBeforeUnmount(releaseSource)
|
|
455
|
+
|
|
456
|
+
// Reset live component and editor code when tab changes
|
|
457
|
+
watch(activeCodeType, () => {
|
|
458
|
+
liveComponent.value = null
|
|
459
|
+
compileError.value = null
|
|
460
|
+
currentCode.value = null
|
|
461
|
+
// 等 sandpack 完成 activeFile 切换后再重置内容,避免写入错误的文件
|
|
462
|
+
nextTick(() => {
|
|
463
|
+
editorBridgeRef.value?.resetCode(activeSourceCode.value)
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
const debouncedCompile = useDebounceFn(async (newCode: string) => {
|
|
468
|
+
// 伴生文件 tab 不参与主 demo 的实时编译
|
|
469
|
+
if (activeExtraFile.value)
|
|
470
|
+
return
|
|
471
|
+
// Code matches original source (e.g. after tab switch reset), skip compilation
|
|
472
|
+
if (newCode === mainSourceCode.value) {
|
|
473
|
+
liveComponent.value = null
|
|
474
|
+
compileError.value = null
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
const { component: comp, error } = await compileSfcSource(newCode, context.modules)
|
|
478
|
+
if (comp) {
|
|
479
|
+
liveComponent.value = markRaw(comp)
|
|
480
|
+
compileError.value = null
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
compileError.value = error
|
|
484
|
+
}
|
|
485
|
+
}, 300)
|
|
486
|
+
|
|
487
|
+
function handleCodeChange(newCode: string) {
|
|
488
|
+
currentCode.value = newCode
|
|
489
|
+
debouncedCompile(newCode)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const sandpackTheme = computed(() => context.isDark?.() ? atomDark : aquaBlue)
|
|
493
|
+
|
|
494
|
+
const sandpackFiles = computed(() => {
|
|
495
|
+
const files: Record<string, string> = {
|
|
496
|
+
'/src/App.vue': mainSourceCode.value,
|
|
497
|
+
}
|
|
498
|
+
for (const file of extraFiles.value) {
|
|
499
|
+
files[extraFileToSandpackPath(file.name)] = file.code
|
|
500
|
+
}
|
|
501
|
+
return files
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
// 当前激活的 sandpack 文件(多文件 tab 时切换)
|
|
505
|
+
const sandpackActiveFile = computed(() => {
|
|
506
|
+
if (activeExtraFile.value)
|
|
507
|
+
return extraFileToSandpackPath(activeExtraFile.value.name)
|
|
508
|
+
return '/src/App.vue'
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
const sandpackOptions = computed(() => ({
|
|
512
|
+
autorun: false,
|
|
513
|
+
activeFile: sandpackActiveFile.value,
|
|
514
|
+
}))
|
|
515
|
+
|
|
516
|
+
const active = computed(() => route.hash === `#${id.value}`)
|
|
517
|
+
function handleScroll(e: Event) {
|
|
518
|
+
e.preventDefault()
|
|
519
|
+
e.stopPropagation()
|
|
520
|
+
router.push({
|
|
521
|
+
path: route.path,
|
|
522
|
+
hash: `#${id.value}`,
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const titleRef = shallowRef<HTMLElement>()
|
|
527
|
+
|
|
528
|
+
async function handleStackBlitz() {
|
|
529
|
+
if (!context.openStackBlitz)
|
|
530
|
+
return
|
|
531
|
+
try {
|
|
532
|
+
await ensureSourceLoaded()
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
showCode.value = true
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
if (mainSourceCode.value) {
|
|
539
|
+
context.openStackBlitz({
|
|
540
|
+
title: titleRef.value?.textContent || 'Demo',
|
|
541
|
+
code: mainSourceCode.value,
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function handleOpenPlayground() {
|
|
547
|
+
if (!context.openPlayground)
|
|
548
|
+
return
|
|
549
|
+
// 与线上一致:面板未展开过时先加载源码,失败则展开面板展示错误
|
|
550
|
+
try {
|
|
551
|
+
await ensureSourceLoaded()
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
showCode.value = true
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
if (mainSourceCode.value) {
|
|
558
|
+
context.openPlayground(mainSourceCode.value)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const demoStyle = computed(() => {
|
|
563
|
+
const style: CSSProperties = {}
|
|
564
|
+
if (props.compact) {
|
|
565
|
+
style.padding = '0px'
|
|
566
|
+
style.overflow = 'hidden'
|
|
567
|
+
}
|
|
568
|
+
if (props.background === 'grey') {
|
|
569
|
+
style.backgroundColor = 'var(--ant-color-bg-layout)'
|
|
570
|
+
}
|
|
571
|
+
return style
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
const copySource = computed(() => currentCode.value ?? activeSourceCode.value)
|
|
575
|
+
|
|
576
|
+
const { copied, copy } = useClipboard({
|
|
577
|
+
source: copySource,
|
|
578
|
+
legacy: true,
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
// 与线上一致:操作栏复制按钮在面板未展开时也可用(先加载源码再复制)
|
|
582
|
+
async function handleCopy() {
|
|
583
|
+
try {
|
|
584
|
+
await ensureSourceLoaded()
|
|
585
|
+
await copy()
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
showCode.value = true
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const cls = computed(() => {
|
|
593
|
+
const cls: string[] = []
|
|
594
|
+
if (active.value) {
|
|
595
|
+
cls.push('border-primary')
|
|
596
|
+
}
|
|
597
|
+
if (props.simplify) {
|
|
598
|
+
cls.push('ant-doc-demo-box-simplify')
|
|
599
|
+
}
|
|
600
|
+
if (props.debug) {
|
|
601
|
+
cls.push('ant-doc-demo-box-debug')
|
|
602
|
+
}
|
|
603
|
+
return cls
|
|
604
|
+
})
|
|
605
|
+
</script>
|
|
606
|
+
|
|
607
|
+
<template>
|
|
608
|
+
<section v-if="!hidden" :id="id" class="ant-doc-demo-box" :class="[styles.root, cls]">
|
|
609
|
+
<template v-if="simplify">
|
|
610
|
+
<section class="ant-doc-demo-box-demo vp-raw" :style="demoStyle">
|
|
611
|
+
<Skeleton v-if="!demo" active :paragraph="{ rows: 5 }" />
|
|
612
|
+
<component :is="liveComponent || component" v-else-if="liveComponent || demo?.component" />
|
|
613
|
+
</section>
|
|
614
|
+
</template>
|
|
615
|
+
<template v-else>
|
|
616
|
+
<!-- Preview area: always visible, shows live-compiled or original component -->
|
|
617
|
+
<section v-if="!iframe" class="ant-doc-demo-box-demo vp-raw" :style="demoStyle">
|
|
618
|
+
<Skeleton v-if="!demo" active :paragraph="{ rows: 5 }" />
|
|
619
|
+
<Suspense v-else>
|
|
620
|
+
<component :is="liveComponent || component" v-if="liveComponent || demo?.component" />
|
|
621
|
+
<template #fallback>
|
|
622
|
+
<Skeleton active :paragraph="{ rows: 5 }" />
|
|
623
|
+
</template>
|
|
624
|
+
</Suspense>
|
|
625
|
+
</section>
|
|
626
|
+
<template v-else-if="context.iframeRenderer">
|
|
627
|
+
<component :is="context.iframeRenderer(id, props.iframe)" />
|
|
628
|
+
</template>
|
|
629
|
+
<!-- Compile error hint -->
|
|
630
|
+
<div v-if="compileError && showCode" class="ant-doc-demo-box-compile-error">
|
|
631
|
+
<pre>{{ compileError }}</pre>
|
|
632
|
+
</div>
|
|
633
|
+
<!-- Meta: title, description, actions -->
|
|
634
|
+
<section class="ant-doc-demo-box-meta markdown">
|
|
635
|
+
<div class="ant-doc-demo-box-title">
|
|
636
|
+
<a ref="titleRef" :href="`#${id}`" @click="handleScroll">
|
|
637
|
+
<slot />
|
|
638
|
+
</a>
|
|
639
|
+
<a
|
|
640
|
+
v-if="context.editUrl"
|
|
641
|
+
class="ant-doc-demo-box-edit-icon"
|
|
642
|
+
:href="context.editUrl(id)"
|
|
643
|
+
target="_blank"
|
|
644
|
+
rel="noopener"
|
|
645
|
+
>
|
|
646
|
+
<EditOutlined />
|
|
647
|
+
</a>
|
|
648
|
+
</div>
|
|
649
|
+
<div v-if="description" class="ant-doc-demo-box-meta-description">
|
|
650
|
+
<div v-html="description" />
|
|
651
|
+
</div>
|
|
652
|
+
<Flex class="ant-doc-demo-box-actions" wrap gap="middle">
|
|
653
|
+
<div v-if="context.copyCode !== false" class="ant-doc-demo-box-code-action" @click="handleCopy">
|
|
654
|
+
<Tooltip :title="t(`action.${copied ? 'copied' : 'copy'}`)">
|
|
655
|
+
<CheckOutlined v-if="copied" />
|
|
656
|
+
<CopyOutlined v-else />
|
|
657
|
+
</Tooltip>
|
|
658
|
+
</div>
|
|
659
|
+
<a v-if="context.openStackBlitz" class="ant-doc-demo-box-code-action" @click="handleStackBlitz">
|
|
660
|
+
<Tooltip :title="t('action.stackblitz')">
|
|
661
|
+
<ThunderboltOutlined />
|
|
662
|
+
</Tooltip>
|
|
663
|
+
</a>
|
|
664
|
+
<a
|
|
665
|
+
v-if="context.demoPageUrl"
|
|
666
|
+
class="ant-doc-demo-box-code-action"
|
|
667
|
+
:href="context.demoPageUrl(id)"
|
|
668
|
+
target="_blank"
|
|
669
|
+
rel="noopener"
|
|
670
|
+
>
|
|
671
|
+
<Tooltip :title="t('action.externalLink')">
|
|
672
|
+
<ExternalLinkIcon />
|
|
673
|
+
</Tooltip>
|
|
674
|
+
</a>
|
|
675
|
+
<div v-if="context.openPlayground" class="ant-doc-demo-box-code-action" @click="handleOpenPlayground">
|
|
676
|
+
<Tooltip :title="t('action.openPlayground')">
|
|
677
|
+
<CodeOutlined />
|
|
678
|
+
</Tooltip>
|
|
679
|
+
</div>
|
|
680
|
+
<div class="ant-doc-demo-box-expand-icon ant-doc-demo-box-code-action" @click="handleShowCode">
|
|
681
|
+
<Tooltip :title="t(`action.${showCode ? 'expandedCode' : 'expandCode'}`)">
|
|
682
|
+
<ExpandIcon :expanded="showCode" />
|
|
683
|
+
</Tooltip>
|
|
684
|
+
</div>
|
|
685
|
+
</Flex>
|
|
686
|
+
</section>
|
|
687
|
+
<!-- Code editor (only when expanded) -->
|
|
688
|
+
<template v-if="showCode">
|
|
689
|
+
<!-- 加载中 -->
|
|
690
|
+
<div v-if="sourceLoading" class="ant-doc-demo-box-code-loading">
|
|
691
|
+
<Spin />
|
|
692
|
+
</div>
|
|
693
|
+
<!-- 加载失败 -->
|
|
694
|
+
<div v-else-if="sourceLoadError" class="ant-doc-demo-box-code">
|
|
695
|
+
<Alert type="error" :message="t('action.loadError')" />
|
|
696
|
+
</div>
|
|
697
|
+
<!-- 正常展示 -->
|
|
698
|
+
<template v-else>
|
|
699
|
+
<div v-if="hasCodeTabs" class="ant-doc-demo-box-code-tabs">
|
|
700
|
+
<Tabs
|
|
701
|
+
v-model:active-key="activeCodeType"
|
|
702
|
+
centered
|
|
703
|
+
size="small"
|
|
704
|
+
>
|
|
705
|
+
<TabPane key="ts" :tab="t('type.typescript')" />
|
|
706
|
+
<TabPane v-if="hasJsSource" key="js" :tab="t('type.javascript')" />
|
|
707
|
+
<TabPane
|
|
708
|
+
v-for="file in extraFiles"
|
|
709
|
+
:key="file.name"
|
|
710
|
+
:tab="displayFileName(file.name)"
|
|
711
|
+
/>
|
|
712
|
+
</Tabs>
|
|
713
|
+
</div>
|
|
714
|
+
<div class="ant-doc-demo-box-code">
|
|
715
|
+
<Tooltip :title="t(`action.${copied ? 'copied' : 'copy'}`)">
|
|
716
|
+
<div class="ant-doc-demo-box-code-copy" :class="copied ? 'ant-doc-demo-box-code-copied' : ''" @click="copy()">
|
|
717
|
+
<CopyOutlined v-if="!copied" />
|
|
718
|
+
<CheckOutlined v-else />
|
|
719
|
+
</div>
|
|
720
|
+
</Tooltip>
|
|
721
|
+
<SandpackProvider
|
|
722
|
+
template="vite-vue-ts"
|
|
723
|
+
:files="sandpackFiles"
|
|
724
|
+
:theme="sandpackTheme"
|
|
725
|
+
:options="sandpackOptions"
|
|
726
|
+
>
|
|
727
|
+
<CodeEditorBridge
|
|
728
|
+
ref="editorBridgeRef"
|
|
729
|
+
@update:code="handleCodeChange"
|
|
730
|
+
/>
|
|
731
|
+
</SandpackProvider>
|
|
732
|
+
</div>
|
|
733
|
+
</template>
|
|
734
|
+
</template>
|
|
735
|
+
</template>
|
|
736
|
+
</section>
|
|
737
|
+
</template>
|