|
1 | 1 | <script setup lang="ts"> |
2 | 2 | import { withBase } from 'vitepress' |
3 | | -import mediumZoom, { type Zoom, type ZoomOptions } from 'medium-zoom' |
4 | | -
|
| 3 | +import mediumZoom, { type ZoomOptions } from 'medium-zoom' |
| 4 | +import { |
| 5 | + onMounted, |
| 6 | + onUnmounted, |
| 7 | + ref, |
| 8 | + computed, |
| 9 | + useTemplateRef, |
| 10 | + watch, |
| 11 | + useAttrs, |
| 12 | +} from 'vue' |
| 13 | +import { cn } from '@/lib/utils' |
5 | 14 | import type { DefaultTheme } from 'vitepress/theme-without-fonts' |
6 | | -import { onMounted, onUnmounted, useTemplateRef, watch } from 'vue' |
7 | 15 |
|
8 | 16 | interface Props { |
9 | 17 | image?: DefaultTheme.ThemeableImage |
10 | 18 | alt?: string |
11 | 19 | zoom?: ZoomOptions | false |
| 20 | + class?: string |
12 | 21 | } |
13 | 22 |
|
14 | | -const { |
15 | | - zoom: zoomConfig = { |
16 | | - background: 'transparent', |
17 | | - }, |
18 | | -} = defineProps<Props>() |
| 23 | +const { zoom: zoomConfig = { background: 'transparent' }, image } = |
| 24 | + defineProps<Props>() |
19 | 25 |
|
20 | 26 | defineOptions({ inheritAttrs: false }) |
21 | 27 |
|
22 | 28 | const img = useTemplateRef('img') |
| 29 | +const loadFail = ref(false) |
| 30 | +const attrs = useAttrs() |
| 31 | +// 计算 `imgSrc`,动态监听 `image` 变化 |
| 32 | +const imgSrc = computed(() => |
| 33 | + withBase( |
| 34 | + (typeof image === 'string' ? image : image?.src) || |
| 35 | + attrs?.src || |
| 36 | + 'https://assets.yuanshen.site/images/noImage.png', |
| 37 | + ), |
| 38 | +) |
23 | 39 |
|
24 | | -const initZoom = () => { |
25 | | - if (zoomConfig === false) return |
26 | | - let zoom: Zoom | null = null |
27 | | -
|
28 | | - const getZoom = () => (zoom === null ? mediumZoom(zoomConfig) : zoom) |
| 40 | +// medium-zoom 只创建一次,避免重复初始化 |
| 41 | +const zoom = zoomConfig === false ? null : mediumZoom(zoomConfig) |
29 | 42 |
|
30 | | - onMounted(() => { |
31 | | - if (!img.value) return |
| 43 | +const initZoom = () => { |
| 44 | + if (!zoom || !img.value) return |
| 45 | + zoom.attach(img.value) |
| 46 | +} |
32 | 47 |
|
33 | | - getZoom().attach(img.value) |
| 48 | +onMounted(initZoom) |
| 49 | +onUnmounted(() => zoom?.detach()) |
34 | 50 |
|
35 | | - watch( |
36 | | - () => zoomConfig, |
37 | | - (options) => { |
38 | | - const zoom = getZoom() |
39 | | - zoom.update(zoomConfig || {}) |
40 | | - }, |
41 | | - ) |
42 | | - }) |
| 51 | +// 监听 zoomConfig 的变化,避免 Vue 的 shallow 监听失效 |
| 52 | +watch( |
| 53 | + () => JSON.stringify(zoomConfig), |
| 54 | + () => zoom?.update(zoomConfig || {}), |
| 55 | +) |
43 | 56 |
|
44 | | - onUnmounted(() => { |
45 | | - if (img.value) getZoom().detach() |
46 | | - }) |
| 57 | +const handleLoadError = () => { |
| 58 | + loadFail.value = true |
47 | 59 | } |
48 | | -
|
49 | | -initZoom() |
50 | 60 | </script> |
51 | 61 |
|
52 | 62 | <template> |
53 | | - <template v-if="image"> |
| 63 | + <img |
| 64 | + v-if="!loadFail" |
| 65 | + ref="img" |
| 66 | + v-bind="typeof image === 'string' ? $attrs : { ...image, ...$attrs }" |
| 67 | + :src="imgSrc" |
| 68 | + :alt="alt ?? (typeof image === 'string' ? '' : image?.alt || '')" |
| 69 | + :class="cn('VPImage', $props.class)" |
| 70 | + @error="handleLoadError" |
| 71 | + /> |
| 72 | + <template v-else> |
54 | 73 | <img |
55 | | - v-if="typeof image === 'string' || 'src' in image" |
56 | | - ref="img" |
57 | | - class="VPImage" |
| 74 | + :class="cn('VPImage bg-[var(--vp-c-bg-alt)]', $props.class)" |
| 75 | + :alt="alt ?? (typeof image === 'string' ? '' : image?.alt || '')" |
58 | 76 | v-bind="typeof image === 'string' ? $attrs : { ...image, ...$attrs }" |
59 | | - :src="withBase(typeof image === 'string' ? image : image.src)" |
60 | | - :alt="alt ?? (typeof image === 'string' ? '' : image.alt || '')" |
| 77 | + src="https://assets.yuanshen.site/images/noImage.png" |
61 | 78 | /> |
62 | | - <template v-else> |
63 | | - <Image |
64 | | - class="dark" |
65 | | - :image="image.dark" |
66 | | - :alt="image.alt" |
67 | | - v-bind="$attrs" |
68 | | - /> |
69 | | - <Image |
70 | | - class="light" |
71 | | - :image="image.light" |
72 | | - :alt="image.alt" |
73 | | - v-bind="$attrs" |
74 | | - /> |
75 | | - </template> |
76 | | - </template> |
77 | | - <template v-else> |
78 | | - <img ref="img" class="VPImage" v-bind="$attrs" /> |
79 | 79 | </template> |
80 | 80 | </template> |
81 | 81 |
|
82 | 82 | <style scoped> |
83 | 83 | html:not(.dark) .VPImage.dark { |
84 | 84 | display: none; |
85 | 85 | } |
86 | | -
|
87 | 86 | .dark .VPImage.light { |
88 | 87 | display: none; |
89 | 88 | } |
|
0 commit comments