@@ -1,9 +1,16 @@
|
||||
"use client";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import HoverVideo from "./HoverVideo";
|
||||
import { ThumbsUp } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { FeedItem, FeedResponse } from "@/app/types/feed";
|
||||
import HoverVideo from "./HoverVideo";
|
||||
|
||||
const COLUMN_KEYS = ["feed-col-0", "feed-col-1", "feed-col-2", "feed-col-3"];
|
||||
|
||||
function getAspectRatio(item: FeedItem) {
|
||||
return item.width && item.height ? `${item.width}/${item.height}` : undefined;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
initialItems: FeedItem[];
|
||||
@@ -58,7 +65,7 @@ export default function FeedMasonry({
|
||||
mediaH = (9 / 16) * colWidth; // 常见视频比例
|
||||
}
|
||||
// 文本 + 作者栏的高度粗估
|
||||
const textH = 48; // 标题+标签区域
|
||||
const textH = item.content_tags?.length ? 72 : 48; // 标题+标签区域
|
||||
const authorH = 48; // 作者行
|
||||
const gap = 16; // 卡片内边距/间隙
|
||||
return mediaH + textH + authorH + gap;
|
||||
@@ -92,8 +99,7 @@ export default function FeedMasonry({
|
||||
}
|
||||
setColumns(newCols);
|
||||
setColHeights(newHeights);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [columnCount]);
|
||||
}, [columnCount, estimateItemHeight, initialItems]);
|
||||
|
||||
const fetchMore = useCallback(async () => {
|
||||
if (loading || ended) return;
|
||||
@@ -136,7 +142,15 @@ export default function FeedMasonry({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [cursor, ended, loading, columnCount, colHeights, estimateItemHeight]);
|
||||
}, [
|
||||
cursor,
|
||||
ended,
|
||||
loading,
|
||||
columnCount,
|
||||
colHeights,
|
||||
estimateItemHeight,
|
||||
fetchUrl,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = sentinelRef.current;
|
||||
@@ -168,29 +182,43 @@ export default function FeedMasonry({
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
aspectRatio:
|
||||
`${item.width && item.height ? `${item.width}/${item.height}` : ""}` as any,
|
||||
aspectRatio: getAspectRatio(item),
|
||||
}}
|
||||
>
|
||||
{item.type === "video" ? (
|
||||
<HoverVideo
|
||||
videoUrl={(item as any).video_url}
|
||||
videoUrl={item.video_url}
|
||||
coverUrl={item.cover_url}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
loading="lazy"
|
||||
<Image
|
||||
fill
|
||||
src={item.cover_url || "/placeholder.svg"}
|
||||
alt={item.desc?.slice(0, 20) || "image"}
|
||||
unoptimized
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/50 via-black/0 to-black/0 opacity-70" />
|
||||
<div className="absolute left-3 bottom-3 right-3 flex items-end justify-between gap-3">
|
||||
<p className="text-white/95 text-sm leading-tight line-clamp-2 drop-shadow">
|
||||
{item.desc}
|
||||
</p>
|
||||
<div className="min-w-0">
|
||||
<p className="text-white/95 text-sm leading-tight line-clamp-2 drop-shadow">
|
||||
{item.desc}
|
||||
</p>
|
||||
{!!item.content_tags?.length && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{item.content_tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full bg-blue-500/80 px-2 py-0.5 text-xs text-white shadow"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="shrink-0 inline-flex items-center gap-2 rounded-full bg-white/85 px-2 py-1 text-xs text-zinc-800">
|
||||
{item.type === "video" ? "视频" : "图文"}
|
||||
</span>
|
||||
@@ -206,9 +234,12 @@ export default function FeedMasonry({
|
||||
>
|
||||
<div className="size-6 rounded-full overflow-hidden bg-zinc-200 shrink-0">
|
||||
{item.author.avatar_url ? (
|
||||
<img
|
||||
<Image
|
||||
src={item.author.avatar_url}
|
||||
alt="avatar"
|
||||
width={24}
|
||||
height={24}
|
||||
unoptimized
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
@@ -221,9 +252,12 @@ export default function FeedMasonry({
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<div className="size-6 rounded-full overflow-hidden bg-zinc-200 shrink-0">
|
||||
{item.author.avatar_url ? (
|
||||
<img
|
||||
<Image
|
||||
src={item.author.avatar_url}
|
||||
alt="avatar"
|
||||
width={24}
|
||||
height={24}
|
||||
unoptimized
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
@@ -254,7 +288,7 @@ export default function FeedMasonry({
|
||||
}}
|
||||
>
|
||||
{columns.map((col, idx) => (
|
||||
<div key={idx} className="flex flex-col">
|
||||
<div key={COLUMN_KEYS[idx]} className="flex flex-col">
|
||||
{col.map((item) => renderCard(item))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Search } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function SearchBox() {
|
||||
export default function SearchBox({ tag }: { tag?: string | null }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
@@ -12,7 +12,9 @@ export default function SearchBox() {
|
||||
e.preventDefault();
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return;
|
||||
router.push(`/search?q=${encodeURIComponent(trimmed)}`);
|
||||
const params = new URLSearchParams({ q: trimmed });
|
||||
if (tag) params.set("tag", tag);
|
||||
router.push(`/search?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user