diff --git a/src/components/RecordModal/RecordItem.tsx b/src/components/RecordModal/RecordItem.tsx index 78b8a71..0e47811 100644 --- a/src/components/RecordModal/RecordItem.tsx +++ b/src/components/RecordModal/RecordItem.tsx @@ -20,6 +20,7 @@ interface Props { location?: string; createTime?: string | number | Date; user: Pick | null; + highlighted?: boolean; } interface Particle { @@ -29,20 +30,32 @@ interface Particle { rot: number; } -export default function RecordItem({ id, content, images, likeCount, mood, location, createTime, user }: Props) { +export default function RecordItem({ + id, + content, + images, + likeCount, + mood, + location, + createTime, + user, + highlighted, +}: Props) { const imageList: string[] = Array.isArray(images) ? images : JSON.parse((images as string) ?? '[]'); const { count, like } = useDebouncedLike(Number(id), likeCount ?? 0, likeRecordAction); const [showComments, setShowComments] = useState(false); const [commentCount, setCommentCount] = useState(0); const [popping, setPopping] = useState(false); const [countBump, setCountBump] = useState(false); + const [burst, setBurst] = useState(false); const [particles, setParticles] = useState([]); const particleIdRef = useRef(0); + const lastTapRef = useRef(0); useEffect(() => { getRecordCommentListAPI(Number(id), { pageNum: 1, pageSize: 1 }) .then(({ data }) => setCommentCount(data.total ?? 0)) - .catch(() => { }); + .catch(() => {}); }, [id]); const handleLike = () => { @@ -69,29 +82,61 @@ export default function RecordItem({ id, content, images, likeCount, mood, locat }, 650); }; + const handleContentTap = () => { + const now = Date.now(); + if (now - lastTapRef.current < 320) { + handleLike(); + setBurst(true); + window.setTimeout(() => setBurst(false), 560); + lastTapRef.current = 0; + return; + } + lastTapRef.current = now; + }; + return ( -
- {user?.name -
-

+

+ {user?.avatar ? ( + {user.name + ) : ( +
+ {(user?.name ?? '我').slice(0, 1)} +
+ )} +
+

{user?.name} - {mood ? {mood} : null} + {mood ? {mood} : null}

-

+ + {burst && ( + + + + )} {imageList.length > 0 && ( -

+
)} -
+
{location ? (

@@ -99,9 +144,11 @@ export default function RecordItem({ id, content, images, likeCount, mood, locat {location}

) : null} -

{getRelativeTimeLabel(createTime)}

+

+ {getRelativeTimeLabel(createTime)} +

-
+
{showComments && ( -
+
)} diff --git a/src/components/RecordModal/RecordListSkeleton.tsx b/src/components/RecordModal/RecordListSkeleton.tsx index a1bb4ad..0ff6b32 100644 --- a/src/components/RecordModal/RecordListSkeleton.tsx +++ b/src/components/RecordModal/RecordListSkeleton.tsx @@ -2,18 +2,18 @@ import Skeleton from '@/components/Skeleton'; function RecordItemSkeleton() { return ( -
+
-
+
-
- +
+
- - + +
diff --git a/src/components/RecordModal/index.tsx b/src/components/RecordModal/index.tsx index c989689..070bc2d 100644 --- a/src/components/RecordModal/index.tsx +++ b/src/components/RecordModal/index.tsx @@ -1,8 +1,9 @@ 'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { IoCameraOutline, IoCloseOutline } from 'react-icons/io5'; +import { IoCloseOutline } from 'react-icons/io5'; import { Modal } from '@/ThriveUI'; +import PhotoPreview, { type PhotoItem } from '@/ThriveUI/PhotoPreview'; import Empty from '@/components/Empty'; import Show from '@/components/Show'; import { useAppConfig } from '@/components/AppConfigProvider'; @@ -16,22 +17,24 @@ import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; export default function RecordModal() { - const { open, closeModal } = useRecordModalStore(); + const { open, closeModal, focusId, clearFocus } = useRecordModalStore(); const { author, theme } = useAppConfig(); const [records, setRecords] = useState([]); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const [totalPages, setTotalPages] = useState(1); + const [scrolled, setScrolled] = useState(false); + const [preview, setPreview] = useState<{ photos: PhotoItem[]; index: number } | null>(null); const currentPageRef = useRef(1); const listRef = useRef(null); const fetchedRef = useRef(false); + const focusedRef = useRef(false); const coverSrc = theme?.record_cover?.trim() || getStableImage(undefined, theme?.covers, 'record-cover'); const covers = parseThemeCovers(theme?.covers); const bgCover = coverSrc || covers[0] || ''; const recordName = theme?.record_name?.trim() || author?.name || '我'; - // 如果 record_avatar 为空,则使用 author.avatar const recordAvatar = theme?.record_avatar?.trim() || author?.avatar || ''; const recordUser = { name: recordName, avatar: recordAvatar }; const showSkeleton = loading && records.length === 0; @@ -59,6 +62,9 @@ export default function RecordModal() { useEffect(() => { if (!open) { fetchedRef.current = false; + focusedRef.current = false; + setScrolled(false); + setPreview(null); return; } if (fetchedRef.current) return; @@ -82,6 +88,8 @@ export default function RecordModal() { if (!open || !el) return; const onScroll = () => { + setScrolled(el.scrollTop > 8); + if (loading || !hasMore) return; if (el.scrollTop + el.clientHeight >= el.scrollHeight - 80) { const next = currentPageRef.current + 1; @@ -92,15 +100,42 @@ export default function RecordModal() { let timer: ReturnType; const debounced = () => { clearTimeout(timer); - timer = setTimeout(onScroll, 150); + timer = setTimeout(onScroll, 120); }; - el.addEventListener('scroll', debounced); + el.addEventListener('scroll', debounced, { passive: true }); return () => { el.removeEventListener('scroll', debounced); clearTimeout(timer); }; }, [open, loading, hasMore, totalPages, fetchRecords]); + useEffect(() => { + if (!open || !focusId || loading || focusedRef.current || !records.length) return; + + const target = listRef.current?.querySelector(`[data-record-id="${focusId}"]`); + if (!target) return; + + focusedRef.current = true; + requestAnimationFrame(() => { + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + target.classList.add('record-item-flash'); + window.setTimeout(() => { + target.classList.remove('record-item-flash'); + clearFocus(); + }, 1200); + }); + }, [open, focusId, loading, records, clearFocus]); + + const openCoverPreview = () => { + if (!bgCover) return; + setPreview({ photos: [{ id: 'cover', url: bgCover, alt: '封面' }], index: 0 }); + }; + + const openAvatarPreview = () => { + if (!recordAvatar) return; + setPreview({ photos: [{ id: 'avatar', url: recordAvatar, alt: recordName }], index: 0 }); + }; + return ( <>
-
- - - +
+ 闪念
-
-
-
+
+ + +
-
+
{showSkeleton ? ( ) : ( @@ -164,6 +219,7 @@ export default function RecordModal() { location={item.location} createTime={item.createTime as string | number | undefined} user={recordUser} + highlighted={focusId === item.id} /> ))} @@ -172,10 +228,14 @@ export default function RecordModal() {
{loading && records.length > 0 && ( -
正在加载...
+
+ 加载中… +
)} {!hasMore && records.length > 0 && ( -
没有更多了
+
+ — 已经到底啦 — +
)} )} @@ -184,6 +244,13 @@ export default function RecordModal() {
+ setPreview(null)} + /> + {open && } ); diff --git a/src/components/RecordModal/like.scss b/src/components/RecordModal/like.scss index 6740b2c..bee50d8 100644 --- a/src/components/RecordModal/like.scss +++ b/src/components/RecordModal/like.scss @@ -46,6 +46,38 @@ } } +@keyframes record-double-tap-heart { + 0% { + opacity: 0; + transform: scale(0.4); + } + + 25% { + opacity: 1; + transform: scale(1.15); + } + + 55% { + opacity: 1; + transform: scale(1); + } + + 100% { + opacity: 0; + transform: scale(1.2) translateY(-10px); + } +} + +@keyframes record-item-flash { + 0% { + background-color: rgba(83, 157, 253, 0.16); + } + + 100% { + background-color: transparent; + } +} + .record-like-heart.is-popping { animation: record-like-heart-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); color: #fa5151; @@ -58,4 +90,12 @@ .record-like-count-bump { display: inline-block; animation: record-like-count-bump 0.32s ease-out; -} \ No newline at end of file +} + +.record-double-tap-heart { + animation: record-double-tap-heart 0.56s ease-out forwards; +} + +.record-item-flash { + animation: record-item-flash 1.1s ease-out; +} diff --git a/src/components/Sidebar/RecordCarousel/RecordCarouselClient.tsx b/src/components/Sidebar/RecordCarousel/RecordCarouselClient.tsx index a6f2325..15aeb98 100644 --- a/src/components/Sidebar/RecordCarousel/RecordCarouselClient.tsx +++ b/src/components/Sidebar/RecordCarousel/RecordCarouselClient.tsx @@ -1,7 +1,6 @@ 'use client'; import { useEffect, useRef, useState, type MouseEvent } from 'react'; -import { FiChevronRight } from 'react-icons/fi'; import CoverImage from '@/components/CoverImage'; import { useRecordModalStore } from '@/stores'; @@ -76,7 +75,7 @@ export default function RecordCarouselClient({ list }: Props) {