Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit 24aadd8

Browse files
committed
refactor(admin): drop markdown client-side image hash compute (server-side now authoritative)
1 parent 980fe52 commit 24aadd8

2 files changed

Lines changed: 3 additions & 111 deletions

File tree

apps/admin/src/components/drawer/components/image-detail-section.tsx

Lines changed: 3 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ import { ChevronDownIcon, ExternalLinkIcon, Trash2Icon } from 'lucide-vue-next'
33
import { NButton, NColorPicker, NInput, NInputNumber } from 'naive-ui'
44
import { thumbHashToDataURL } from 'thumbhash'
55
import { computed, defineComponent, ref } from 'vue'
6-
import { toast } from 'vue-sonner'
76
import type { Image as ImageModel } from '~/models/base'
87
import type { PropType } from 'vue'
98

10-
import { getDominantColor, getThumbhash } from '~/utils/image'
11-
import { isVideoExt, pickImagesFromMarkdown } from '~/utils/markdown'
9+
import { pickImagesFromMarkdown } from '~/utils/markdown'
1210

1311
export const ImageDetailSection = defineComponent({
1412
props: {
@@ -30,8 +28,6 @@ export const ImageDetailSection = defineComponent({
3028
},
3129
},
3230
setup(props) {
33-
const loading = ref(false)
34-
3531
const originImageMap = computed(() => {
3632
const map = new Map<string, ImageModel>()
3733
props.images.forEach((image) => {
@@ -88,103 +84,17 @@ export const ImageDetailSection = defineComponent({
8884

8985
return nextImages
9086
})
91-
const handleCorrectImageDimensions = async () => {
92-
loading.value = true
93-
94-
const fetchImageTasks = await Promise.allSettled(
95-
images.value.map((item) => {
96-
return new Promise<ImageModel>((resolve, reject) => {
97-
const ext = item.src.split('.').pop()!
98-
const isVideo = isVideoExt(ext)
99-
100-
if (isVideo) {
101-
const video = document.createElement('video')
102-
103-
video.src = item.src
104-
105-
video.addEventListener('loadedmetadata', () => {
106-
resolve({
107-
height: video.videoHeight,
108-
type: ext,
109-
src: item.src,
110-
width: video.videoWidth,
111-
accent: '#fff',
112-
})
113-
})
114-
115-
video.addEventListener('error', (e) => {
116-
reject({
117-
err: e,
118-
src: item.src,
119-
})
120-
})
121-
} else {
122-
const $image = new Image()
123-
$image.src = item.src
124-
$image.crossOrigin = 'Anonymous'
125-
$image.addEventListener('load', () => {
126-
resolve({
127-
width: $image.naturalWidth,
128-
height: $image.naturalHeight,
129-
src: item.src,
130-
type: ext,
131-
accent: getDominantColor($image),
132-
thumbhash: getThumbhash($image),
133-
})
134-
})
135-
$image.onerror = (err) => {
136-
reject({
137-
err,
138-
src: item.src,
139-
})
140-
}
141-
}
142-
})
143-
}),
144-
)
145-
146-
loading.value = false
147-
148-
const nextImageDimensions = [] as ImageModel[]
149-
fetchImageTasks.forEach((task, index) => {
150-
if (task.status === 'fulfilled') {
151-
nextImageDimensions.push(task.value)
152-
} else {
153-
// 保留原始图片信息,避免丢失
154-
const originalImage = images.value[index]
155-
if (originalImage) {
156-
nextImageDimensions.push(originalImage)
157-
}
158-
toast.warning(
159-
`获取图片信息失败:${task.reason.src}: ${task.reason.err}`,
160-
)
161-
}
162-
})
163-
164-
props.onChange(nextImageDimensions)
165-
166-
loading.value = false
167-
}
16887

16988
// 展开状态管理
17089
const expandedIndex = ref<number | null>(null)
17190

17291
return () => (
17392
<div class="flex w-full flex-col">
174-
{/* 头部操作区 */}
93+
{/* 头部 */}
17594
<div class="flex items-center justify-between gap-3">
17695
<span class="text-sm text-neutral-500">
177-
调整 Markdown 中的图片信息
96+
Markdown 中的图片信息(由服务端自动写入)
17897
</span>
179-
<NButton
180-
loading={loading.value}
181-
size="tiny"
182-
onClick={handleCorrectImageDimensions}
183-
type="primary"
184-
tertiary
185-
>
186-
自动修正
187-
</NButton>
18898
</div>
18999

190100
{/* 图片列表 */}

apps/admin/src/utils/image.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
// @see https://stackoverflow.com/questions/2541481/get-average-color-of-image-via-javascript
22

3-
import { rgbaToThumbHash } from 'thumbhash'
4-
53
import { computeImageMeta } from '@haklex/rich-editor/renderers'
64

75
export function getDominantColor(imageObject: HTMLImageElement) {
@@ -28,22 +26,6 @@ export function rgbObjectToHex(rgb: { r: number; g: number; b: number }) {
2826
return rgbToHex(rgb.r, rgb.g, rgb.b)
2927
}
3028

31-
export function getThumbhash(img: HTMLImageElement): string {
32-
const scale = Math.min(100 / img.naturalWidth, 100 / img.naturalHeight, 1)
33-
const sw = Math.max(1, Math.round(img.naturalWidth * scale))
34-
const sh = Math.max(1, Math.round(img.naturalHeight * scale))
35-
const canvas = document.createElement('canvas')
36-
canvas.width = sw
37-
canvas.height = sh
38-
const ctx = canvas.getContext('2d')!
39-
ctx.drawImage(img, 0, 0, sw, sh)
40-
const rgba = ctx.getImageData(0, 0, sw, sh).data
41-
const u8 = rgbaToThumbHash(sw, sh, rgba)
42-
let bin = ''
43-
for (let i = 0; i < u8.length; i++) bin += String.fromCharCode(u8[i])
44-
return btoa(bin)
45-
}
46-
4729
export const encodeImageToThumbhash = async (image: HTMLImageElement) => {
4830
const canvas = document.createElement('canvas')
4931
canvas.width = image.naturalWidth

0 commit comments

Comments
 (0)