Skip to content

Commit 9e6cb36

Browse files
committed
feat: image init
Signed-off-by: Innei <[email protected]>
1 parent fc6f268 commit 9e6cb36

File tree

13 files changed

+580
-64
lines changed

13 files changed

+580
-64
lines changed

src/app/notes/Paper.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const Paper: Component = ({ children }) => {
66
className={clsx(
77
'relative bg-base-100 md:col-start-1 lg:col-auto',
88
'-m-4 p-[2rem_1rem] lg:m-0 lg:p-[30px_45px]',
9-
'rounded-[0_6px_6px_0] border border-[#bbb3]',
9+
'rounded-[0_6px_6px_0] border border-[#bbb3] shadow-sm dark:shadow-[#333]',
1010
'note-layout-main',
1111
)}
1212
>

src/app/notes/[id]/page.module.css

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.with-indent {
2+
:global {
3+
ul .indent,
4+
.paragraph .indent {
5+
border-bottom: 1px solid #00b8bb41;
6+
}
7+
8+
.paragraph:not(:nth-child(1)) > span.indent {
9+
&:nth-child(1) {
10+
margin-left: 2rem;
11+
}
12+
}
13+
14+
main {
15+
> p:first-child {
16+
margin-bottom: 2rem;
17+
}
18+
19+
.paragraph:first-child::first-letter {
20+
float: left;
21+
font-size: 2.4em;
22+
margin: 0 0.2em 0 0;
23+
}
24+
}
25+
}
26+
}
27+
28+
.with-serif {
29+
:global {
30+
main {
31+
@apply font-serif;
32+
33+
font-size: 16px;
34+
}
35+
}
36+
}

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

+19-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import { useEffect } from 'react'
44
import { Balancer } from 'react-wrap-balancer'
5+
import clsx from 'clsx'
56
import dayjs from 'dayjs'
67
import { useParams } from 'next/navigation'
8+
import type { MarkdownToJSX } from '~/components/ui/markdown'
79

810
import { PageDataHolder } from '~/components/common/PageHolder'
911
import { useSetHeaderMetaInfo } from '~/components/layout/header/internal/hooks'
@@ -16,6 +18,8 @@ import { ArticleElementProvider } from '~/providers/article/article-element-prov
1618
import { useSetCurrentNoteId } from '~/providers/note/current-note-id-provider'
1719
import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider'
1820

21+
import styles from './page.module.css'
22+
1923
const PageImpl = () => {
2024
const { id } = useParams() as { id: string }
2125
const { data } = useNoteByNidQuery(id)
@@ -52,7 +56,9 @@ const PageImpl = () => {
5256
.format('YYYY 年 M 月 D 日 dddd')
5357

5458
return (
55-
<article className="prose">
59+
<article
60+
className={clsx('prose', styles['with-indent'], styles['with-serif'])}
61+
>
5662
<header>
5763
<h1 className="mt-8 text-left font-bold text-base-content/95">
5864
<Balancer>{note.title}</Balancer>
@@ -66,7 +72,7 @@ const PageImpl = () => {
6672
</header>
6773

6874
<ArticleElementProvider>
69-
<Markdown value={note.text} className="text-[1.05rem]" />
75+
<Markdown as="main" renderers={Markdownrenderers} value={note.text} />
7076

7177
<NoteLayoutRightSidePortal>
7278
<Toc className="sticky top-[120px] ml-4 mt-[120px]" />
@@ -77,6 +83,17 @@ const PageImpl = () => {
7783
)
7884
}
7985

86+
const Markdownrenderers: { [name: string]: Partial<MarkdownToJSX.Rule> } = {
87+
text: {
88+
react(node, _, state) {
89+
return (
90+
<span className="indent" key={state?.key}>
91+
{node.content}
92+
</span>
93+
)
94+
},
95+
},
96+
}
8097
export default PageDataHolder(PageImpl, () => {
8198
const { id } = useParams() as { id: string }
8299
return useNoteByNidQuery(id)

src/components/common/Lazyload.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react'
2+
import { useInView } from 'react-intersection-observer'
3+
import type { FC, PropsWithChildren } from 'react'
4+
import type { IntersectionOptions } from 'react-intersection-observer'
5+
6+
export type LazyLoadProps = {
7+
offset?: number
8+
placeholder?: React.ReactNode
9+
} & IntersectionOptions
10+
export const LazyLoad: FC<PropsWithChildren & LazyLoadProps> = (props) => {
11+
const { placeholder = null, offset = 0, ...rest } = props
12+
const { ref, inView } = useInView({
13+
triggerOnce: true,
14+
rootMargin: `${offset || 0}px`,
15+
...rest,
16+
})
17+
return (
18+
<>
19+
<span data-testid="lazyload-indicator" ref={ref} />
20+
{!inView ? placeholder : props.children}
21+
</>
22+
)
23+
}

src/components/ui/image/LazyImage.tsx

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import React, {
2+
forwardRef,
3+
memo,
4+
useCallback,
5+
useEffect,
6+
useImperativeHandle,
7+
useMemo,
8+
useRef,
9+
useState,
10+
} from 'react'
11+
import { clsx } from 'clsx'
12+
import mediumZoom from 'medium-zoom'
13+
import type {
14+
CSSProperties,
15+
DetailedHTMLProps,
16+
FC,
17+
ImgHTMLAttributes,
18+
} from 'react'
19+
20+
import styles from './index.module.css'
21+
import { useCalculateNaturalSize } from './use-calculate-size'
22+
23+
interface ImageProps {
24+
defaultImage?: string
25+
src: string
26+
alt?: string
27+
height?: number | string
28+
width?: number | string
29+
backgroundColor?: string
30+
popup?: boolean
31+
overflowHidden?: boolean
32+
getParentElWidth?: ((parentElementWidth: number) => number) | number
33+
showErrorMessage?: boolean
34+
}
35+
36+
const Image: FC<
37+
{
38+
popup?: boolean
39+
height?: number | string
40+
width?: number | string
41+
loaderFn: () => void
42+
loaded: boolean
43+
} & Pick<
44+
DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
45+
'src' | 'alt'
46+
>
47+
> = memo(({ src, alt, height, width, popup = false, loaded, loaderFn }) => {
48+
const imageRef = useRef<HTMLImageElement>(null)
49+
50+
useEffect(() => {
51+
if (!popup) {
52+
return
53+
}
54+
const $image = imageRef.current
55+
if ($image) {
56+
const zoom = mediumZoom($image, {
57+
background: 'var(--light-bg)',
58+
})
59+
60+
return () => {
61+
zoom.detach(zoom.getImages())
62+
}
63+
}
64+
}, [popup])
65+
66+
useEffect(() => {
67+
loaderFn()
68+
}, [loaderFn])
69+
70+
return (
71+
<>
72+
<div
73+
className={clsx(
74+
styles['lazyload-image'],
75+
!loaded && styles['image-hide'],
76+
)}
77+
data-status={loaded ? 'loaded' : 'loading'}
78+
onAnimationEnd={onImageAnimationEnd}
79+
>
80+
<img
81+
src={src}
82+
alt={alt}
83+
ref={imageRef}
84+
loading="lazy"
85+
style={{ width, height }}
86+
/>
87+
</div>
88+
</>
89+
)
90+
})
91+
92+
const onImageAnimationEnd: React.AnimationEventHandler<HTMLDivElement> = (
93+
e,
94+
) => {
95+
;(e.target as HTMLElement).dataset.animated = '1'
96+
}
97+
98+
export type ImageLazyRef = { status: 'loading' | 'loaded' }
99+
100+
export const LazyImage = memo(
101+
forwardRef<
102+
ImageLazyRef,
103+
ImageProps &
104+
DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>
105+
>((props, ref) => {
106+
const {
107+
defaultImage,
108+
src,
109+
alt,
110+
height,
111+
width,
112+
backgroundColor = 'rgb(111,111,111)',
113+
popup = false,
114+
style,
115+
overflowHidden = false,
116+
getParentElWidth = (w) => w,
117+
showErrorMessage,
118+
...rest
119+
} = props
120+
useImperativeHandle(ref, () => {
121+
return {
122+
status: loaded ? 'loaded' : ('loading' as any),
123+
}
124+
})
125+
const realImageRef = useRef<HTMLImageElement>(null)
126+
const placeholderRef = useRef<HTMLDivElement>(null)
127+
128+
const wrapRef = useRef<HTMLDivElement>(null)
129+
const [calculatedSize, calculateDimensions] = useCalculateNaturalSize()
130+
131+
const [loaded, setLoad] = useState(false)
132+
const loaderFn = useCallback(() => {
133+
if (!src || loaded) {
134+
return
135+
}
136+
137+
const image = new window.Image()
138+
image.src = src as string
139+
// FIXME
140+
const parentElement = wrapRef.current?.parentElement?.parentElement
141+
142+
if (!height && !width) {
143+
calculateDimensions(
144+
image,
145+
typeof getParentElWidth == 'function'
146+
? getParentElWidth(
147+
parentElement
148+
? parseFloat(getComputedStyle(parentElement).width)
149+
: 0,
150+
)
151+
: getParentElWidth,
152+
)
153+
}
154+
155+
image.onload = () => {
156+
setLoad(true)
157+
try {
158+
if (placeholderRef && placeholderRef.current) {
159+
placeholderRef.current.classList.add('hide')
160+
}
161+
162+
// eslint-disable-next-line no-empty
163+
} catch {}
164+
}
165+
if (showErrorMessage) {
166+
image.onerror = () => {
167+
try {
168+
if (placeholderRef && placeholderRef.current) {
169+
placeholderRef.current.innerHTML = `<p style="color:${
170+
isDarkColorHex(backgroundColor) ? '#eee' : '#333'
171+
};z-index:2"><span>图片加载失败!</span><br/>
172+
<a style="margin: 0 12px;word-break:break-all;white-space:pre-wrap;display:inline-block;" href="${escapeHTMLTag(
173+
image.src,
174+
)}" target="_blank">${escapeHTMLTag(image.src)}</a></p>`
175+
}
176+
// eslint-disable-next-line no-empty
177+
} catch {}
178+
}
179+
}
180+
}, [
181+
src,
182+
loaded,
183+
height,
184+
width,
185+
calculateDimensions,
186+
getParentElWidth,
187+
backgroundColor,
188+
showErrorMessage,
189+
])
190+
const memoPlaceholderImage = useMemo(
191+
() => (
192+
<PlaceholderImage
193+
height={height}
194+
width={width}
195+
backgroundColor={backgroundColor}
196+
ref={placeholderRef}
197+
/>
198+
),
199+
[backgroundColor, height, width],
200+
)
201+
202+
const imageWrapperStyle = useMemo<CSSProperties>(
203+
() => ({
204+
height: loaded ? undefined : height || calculatedSize.height,
205+
width: loaded ? undefined : width || calculatedSize.width,
206+
207+
...(overflowHidden ? { overflow: 'hidden', borderRadius: '3px' } : {}),
208+
}),
209+
[
210+
calculatedSize.height,
211+
calculatedSize.width,
212+
height,
213+
loaded,
214+
overflowHidden,
215+
width,
216+
],
217+
)
218+
return (
219+
<figure style={style} className="inline-block">
220+
{defaultImage ? (
221+
<img src={defaultImage} alt={alt} {...rest} ref={realImageRef} />
222+
) : (
223+
<div
224+
className={clsx(
225+
'relative m-auto inline-block min-h-[1px] max-w-full transition-none',
226+
rest.className,
227+
)}
228+
style={imageWrapperStyle}
229+
ref={wrapRef}
230+
data-info={JSON.stringify({ height, width, calculatedSize })}
231+
data-src={src}
232+
>
233+
<LazyLoad offset={100} placeholder={memoPlaceholderImage}>
234+
<Image
235+
src={src}
236+
alt={alt}
237+
height={height || calculatedSize.height}
238+
width={width || calculatedSize.width}
239+
popup={popup}
240+
loaded={loaded}
241+
loaderFn={loaderFn}
242+
/>
243+
{!loaded && memoPlaceholderImage}
244+
</LazyLoad>
245+
</div>
246+
)}
247+
{alt && <figcaption className={styles['img-alt']}>{alt}</figcaption>}
248+
</figure>
249+
)
250+
}),
251+
)
252+
253+
const PlaceholderImage = memo(
254+
forwardRef<
255+
HTMLDivElement,
256+
{ ref: any; className?: string } & Partial<ImageProps>
257+
>((props, ref) => {
258+
const { backgroundColor, height, width } = props
259+
return (
260+
<div
261+
className={clsx(styles['placeholder-image'], props.className)}
262+
ref={ref}
263+
style={{
264+
height,
265+
width,
266+
color: backgroundColor,
267+
}}
268+
/>
269+
)
270+
}),
271+
)

0 commit comments

Comments
 (0)