Skip to content

Commit 42f30e2

Browse files
committed
feat: support note header cover image
Signed-off-by: Innei <[email protected]>
1 parent 6e09012 commit 42f30e2

File tree

6 files changed

+108
-6
lines changed

6 files changed

+108
-6
lines changed

src/app/notes/[id]/pageExtra.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const NoteTitle = () => {
5151
<GoToAdminEditingButton
5252
type="notes"
5353
id={id!}
54-
className="absolute -top-6 right-0"
54+
className="absolute right-0 top-0"
5555
/>
5656
</>
5757
)

src/app/notes/[id]/pageImpl.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { XLogInfoForNote } from '~/components/widgets/xlog'
2020
import { LayoutRightSidePortal } from '~/providers/shared/LayoutRightSideProvider'
2121
import { WrappedElementProvider } from '~/providers/shared/WrappedElementProvider'
2222

23+
import { NoteHeadCover } from '../../../components/widgets/note/NoteHeadCover'
2324
import { NoteHideIfSecret } from '../../../components/widgets/note/NoteHideIfSecret'
2425
import { NoteMetaBar } from '../../../components/widgets/note/NoteMetaBar'
2526
import {
@@ -36,6 +37,8 @@ const NotePage = function (props: NoteModel) {
3637
return (
3738
<>
3839
<AckRead id={props.id} type="note" />
40+
41+
{props.meta?.cover && <NoteHeadCover image={props.meta.cover} />}
3942
<NoteHeaderMetaInfoSetting />
4043
<IndentArticleContainer>
4144
<header>

src/app/notes/layout.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ export default async (props: PropsWithChildren) => {
99
<div
1010
className={clsx(
1111
'relative mx-auto grid min-h-[calc(100vh-6.5rem-10rem)] max-w-[60rem]',
12-
'gap-4 md:grid-cols-1 lg:max-w-[calc(60rem+400px)] lg:grid-cols-[1fr_minmax(auto,60rem)_1fr]',
12+
'gap-4 md:grid-cols-1 xl:max-w-[calc(60rem+400px)] xl:grid-cols-[1fr_minmax(auto,60rem)_1fr]',
1313
'mt-12',
1414
'print:!block print:!max-w-full md:mt-24',
1515
)}
1616
>
17-
<div className="relative hidden min-w-0 lg:block" data-hide-print>
17+
<div className="relative hidden min-w-0 xl:block" data-hide-print>
1818
<NoteLeftSidebar />
1919
</div>
2020

2121
{props.children}
2222

23-
<LayoutRightSideProvider className="relative hidden print:!hidden lg:block" />
23+
<LayoutRightSideProvider className="relative hidden print:!hidden xl:block" />
2424
</div>
2525
)
2626
}

src/components/ui/link-card/LinkCard.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,8 @@ const fetchMxSpaceData: FetchObject = {
316316
setFullUrl(`/posts/${cate}/${slug}`)
317317
} else if (type === 'notes') {
318318
const [nid] = rest
319-
const response = await apiClient.note.getNoteById(nid)
320-
data = response
319+
const response = await apiClient.note.getNoteById(+nid)
320+
data = response.data
321321
setFullUrl(`/notes/${nid}`)
322322
}
323323

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use client'
2+
3+
import { useLayoutEffect, useState } from 'react'
4+
import clsx from 'clsx'
5+
6+
import { AutoResizeHeight } from '~/components/widgets/shared/AutoResizeHeight'
7+
8+
function cropImageTo16by9(src: string): Promise<string> {
9+
return new Promise((resolve, reject) => {
10+
const img = new Image()
11+
img.crossOrigin = 'anonymous'
12+
img.onload = () => {
13+
const canvas = document.createElement('canvas')
14+
const ctx = canvas.getContext('2d')!
15+
16+
const aspectRatio = 838 / 224
17+
let cropWidth = img.width
18+
let cropHeight = cropWidth / aspectRatio
19+
20+
if (cropHeight > img.height) {
21+
cropHeight = img.height
22+
cropWidth = cropHeight * aspectRatio
23+
}
24+
25+
const left = (img.width - cropWidth) / 2
26+
const top = (img.height - cropHeight) / 2
27+
28+
// 设置 canvas 尺寸和绘制裁剪的图像
29+
canvas.width = cropWidth
30+
canvas.height = cropHeight
31+
ctx.drawImage(
32+
img,
33+
left,
34+
top,
35+
cropWidth,
36+
cropHeight,
37+
0,
38+
0,
39+
cropWidth,
40+
cropHeight,
41+
)
42+
43+
// 转换 canvas 内容为 blob URL
44+
canvas.toBlob((blob) => {
45+
if (blob) {
46+
const url = URL.createObjectURL(blob)
47+
resolve(url)
48+
} else {
49+
reject('Blob conversion failed')
50+
}
51+
}, 'image/jpeg')
52+
}
53+
img.onerror = reject
54+
55+
// 设置图像源以开始加载
56+
img.src = src
57+
})
58+
}
59+
60+
export const NoteHeadCover = ({ image }: { image: string }) => {
61+
const [imageBlob, setImageBlob] = useState<string | null>(null)
62+
useLayoutEffect(() => {
63+
let isMounted = true
64+
cropImageTo16by9(image).then((b) => {
65+
if (!isMounted) return
66+
setImageBlob(b)
67+
})
68+
return () => {
69+
isMounted = false
70+
}
71+
}, [image])
72+
return (
73+
<>
74+
<AutoResizeHeight>
75+
<div
76+
className={clsx(
77+
'z-1 absolute left-0 right-0 top-0',
78+
imageBlob ? 'h-[224px]' : '0',
79+
)}
80+
>
81+
<div
82+
style={{
83+
backgroundImage: `url(${imageBlob})`,
84+
}}
85+
className="cover-mask-b h-full w-full bg-cover bg-center bg-no-repeat"
86+
/>
87+
</div>
88+
</AutoResizeHeight>
89+
90+
<AutoResizeHeight>
91+
<div className={imageBlob ? 'h-[120px]' : 'h-0'} />
92+
</AutoResizeHeight>
93+
</>
94+
)
95+
}

src/styles/theme.css

+4
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,7 @@
164164
rgb(255, 255, 255) 20px
165165
);
166166
}
167+
168+
.cover-mask-b {
169+
mask-image: linear-gradient(180deg, #fff -17.19%, #00000000 92.43%);
170+
}

0 commit comments

Comments
 (0)