添加动图支持,优化图片和视频上传逻辑,调整超时设置,增强评论时间格式化
This commit is contained in:
@@ -18,7 +18,7 @@ import { useNavigation } from "./hooks/useNavigation";
|
||||
import { usePlayerState } from "./hooks/usePlayerState";
|
||||
import { useVideoPlayer } from "./hooks/useVideoPlayer";
|
||||
|
||||
const SEGMENT_MS = 5000;
|
||||
const SEGMENT_MS = 4000;
|
||||
|
||||
interface AwemeDetailClientProps {
|
||||
data: AwemeData;
|
||||
@@ -51,6 +51,7 @@ export default function AwemeDetailClient({ data, neighbors }: AwemeDetailClient
|
||||
audioRef,
|
||||
scrollerRef,
|
||||
setProgress: playerState.setProgress,
|
||||
segmentMs: SEGMENT_MS,
|
||||
});
|
||||
|
||||
// 视频播放器 hooks
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ThumbsUp } from "lucide-react";
|
||||
import type { Comment, User } from "../types";
|
||||
import { formatRelativeTime } from "../utils";
|
||||
import { formatRelativeTime, formatAbsoluteUTC } from "../utils";
|
||||
import { CommentText } from "./CommentText";
|
||||
|
||||
interface CommentListProps {
|
||||
@@ -20,7 +20,7 @@ export function CommentList({ author, createdAt, comments }: CommentListProps) {
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-white/95 text-sm sm:text-base">{author.nickname}</div>
|
||||
<div className="text-xs text-white/50" title={new Date(createdAt).toLocaleString()}>
|
||||
<div className="text-xs text-white/50" title={formatAbsoluteUTC(createdAt)}>
|
||||
发布于 {formatRelativeTime(createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,16 +19,17 @@ export const ImageCarousel = forwardRef<HTMLDivElement, ImageCarouselProps>(
|
||||
key={img.id}
|
||||
className="relative h-full min-w-full snap-center flex items-center justify-center bg-black/70 cursor-pointer"
|
||||
onClick={onTogglePlay}
|
||||
>
|
||||
<img
|
||||
src={img.url}
|
||||
alt={`image-${i + 1}`}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
style={{
|
||||
width: img.width ? `${img.width}px` : undefined,
|
||||
height: img.height ? `${img.height}px` : undefined,
|
||||
}}
|
||||
/>
|
||||
>{
|
||||
img.animated ? <video src={img.animated} autoPlay muted playsInline className="max-w-full max-h-full object-contain" /> : <img
|
||||
src={img.url}
|
||||
alt={`image-${i + 1}`}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
style={{
|
||||
width: img.width ? `${img.width}px` : undefined,
|
||||
height: img.height ? `${img.height}px` : undefined,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { RefObject } from "react";
|
||||
import type { LoopMode, ObjectFit, User } from "../types";
|
||||
import { formatRelativeTime, formatTime } from "../utils";
|
||||
import { formatRelativeTime, formatTime, formatAbsoluteUTC } from "../utils";
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
import { SegmentedProgressBar } from "./SegmentedProgressBar";
|
||||
|
||||
@@ -90,7 +90,7 @@ export function MediaControls({
|
||||
<span className="text-[13px] leading-tight text-white/95 drop-shadow">·</span>
|
||||
<span
|
||||
className="text-[11px] leading-tight text-white/95 drop-shadow"
|
||||
title={new Date(createdAt).toLocaleString()}
|
||||
title={formatAbsoluteUTC(createdAt)}
|
||||
>
|
||||
{formatRelativeTime(createdAt)}
|
||||
</span>
|
||||
|
||||
@@ -3,8 +3,6 @@ import type { RefObject } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { ImageData, LoopMode, Neighbors } from "../types";
|
||||
|
||||
const SEGMENT_MS = 5000;
|
||||
|
||||
interface UseImageCarouselProps {
|
||||
images: ImageData["images"];
|
||||
isPlaying: boolean;
|
||||
@@ -14,6 +12,8 @@ interface UseImageCarouselProps {
|
||||
audioRef: RefObject<HTMLAudioElement | null>;
|
||||
scrollerRef: RefObject<HTMLDivElement | null>;
|
||||
setProgress: (progress: number) => void;
|
||||
/** 单张图片显示时长(毫秒),默认 5000ms */
|
||||
segmentMs?: number;
|
||||
}
|
||||
|
||||
export function useImageCarousel({
|
||||
@@ -25,6 +25,7 @@ export function useImageCarousel({
|
||||
audioRef,
|
||||
scrollerRef,
|
||||
setProgress,
|
||||
segmentMs = 5000,
|
||||
}: UseImageCarouselProps) {
|
||||
const router = useRouter();
|
||||
const [idx, setIdx] = useState(0);
|
||||
@@ -67,8 +68,8 @@ export function useImageCarousel({
|
||||
let localIdx = idxRef.current;
|
||||
|
||||
let elapsed = ts - start;
|
||||
while (elapsed >= SEGMENT_MS) {
|
||||
elapsed -= SEGMENT_MS;
|
||||
while (elapsed >= segmentMs) {
|
||||
elapsed -= segmentMs;
|
||||
|
||||
if (localIdx >= images.length - 1) {
|
||||
if (loopMode === "sequential" && neighbors?.next) {
|
||||
@@ -89,7 +90,7 @@ export function useImageCarousel({
|
||||
if (el) el.scrollTo({ left: localIdx * el.clientWidth, behavior: "smooth" });
|
||||
}
|
||||
|
||||
const localSeg = Math.max(0, Math.min(1, elapsed / SEGMENT_MS));
|
||||
const localSeg = Math.max(0, Math.min(1, elapsed / segmentMs));
|
||||
setSegProgress(localSeg);
|
||||
setProgress((localIdx + localSeg) / images.length);
|
||||
|
||||
@@ -101,7 +102,7 @@ export function useImageCarousel({
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
};
|
||||
}, [images?.length, isPlaying, loopMode, neighbors?.next, router, scrollerRef, setProgress]);
|
||||
}, [images?.length, isPlaying, loopMode, neighbors?.next, router, scrollerRef, setProgress, segmentMs]);
|
||||
|
||||
return {
|
||||
idx,
|
||||
|
||||
@@ -83,7 +83,7 @@ export default async function AwemeDetail({ params }: { params: Promise<{ awemeI
|
||||
aweme_id: post!.aweme_id,
|
||||
desc: post!.desc,
|
||||
created_at: post!.created_at,
|
||||
images: post!.images.map((i) => ({ id: i.id, url: i.url, width: i.width ?? undefined, height: i.height ?? undefined })),
|
||||
images: post!.images,
|
||||
music_url: post!.music_url,
|
||||
author: { nickname: post!.author.nickname, avatar_url: post!.author.avatar_url },
|
||||
comments: post!.comments.map((c) => ({
|
||||
|
||||
@@ -26,7 +26,7 @@ export type ImageData = {
|
||||
aweme_id: string;
|
||||
desc: string;
|
||||
created_at: string | Date;
|
||||
images: { id: string; url: string; width?: number; height?: number }[];
|
||||
images: { id: string; url: string; width?: number; height?: number, animated?: string }[];
|
||||
music_url?: string | null;
|
||||
author: User;
|
||||
comments: Comment[];
|
||||
|
||||
@@ -70,3 +70,16 @@ export function saveToStorage(key: string, value: string | number): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(key, value.toString());
|
||||
}
|
||||
|
||||
// 稳定的绝对时间格式(与 locale 无关),用于避免 SSR/CSR 水合不一致
|
||||
// 输出示例:2025-10-20 08:23:05 UTC
|
||||
export function formatAbsoluteUTC(date: string | Date): string {
|
||||
const d = new Date(date);
|
||||
const Y = d.getUTCFullYear();
|
||||
const M = String(d.getUTCMonth() + 1).padStart(2, "0");
|
||||
const D = String(d.getUTCDate()).padStart(2, "0");
|
||||
const h = String(d.getUTCHours()).padStart(2, "0");
|
||||
const m = String(d.getUTCMinutes()).padStart(2, "0");
|
||||
const s = String(d.getUTCSeconds()).padStart(2, "0");
|
||||
return `${Y}-${M}-${D} ${h}:${m}:${s} UTC`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user