feat: 添加评论时间格式化功能,优化评论项显示,支持显示编辑时间

This commit is contained in:
2025-09-10 12:34:08 +08:00
parent 09c024ccbb
commit c6e89c0b86
5 changed files with 88 additions and 9 deletions

View File

@ -2,7 +2,6 @@ import { useToLogin, useToUserProfile } from "@/hooks/use-route";
import { User } from "@/models/user"; import { User } from "@/models/user";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import NeedLogin from "@/components/common/need-login";
import { toast } from "sonner"; import { toast } from "sonner";
import { getGravatarByUser } from "@/components/common/gravatar"; import { getGravatarByUser } from "@/components/common/gravatar";
import { CircleUser } from "lucide-react"; import { CircleUser } from "lucide-react";
@ -60,7 +59,7 @@ export function CommentInput(
return ( return (
<div className="fade-in-up"> <div className="fade-in-up">
<div className="flex py-4 fade-in"> <div className="flex py-4 fade-in">
<div onClick={user ? () => clickToUserProfile(user.username) : clickToLogin} className="flex-shrink-0 w-10 h-10 fade-in"> <div onClick={user ? () => clickToUserProfile(user.username) : clickToLogin} className="cursor-pointer flex-shrink-0 w-10 h-10 fade-in">
{user ? getGravatarByUser(user) : null} {user ? getGravatarByUser(user) : null}
{!user && <CircleUser className="w-full h-full fade-in" />} {!user && <CircleUser className="w-full h-full fade-in" />}
</div> </div>

View File

@ -1,6 +1,6 @@
import { useToLogin, useToUserProfile } from "@/hooks/use-route"; import { useToLogin, useToUserProfile } from "@/hooks/use-route";
import { User } from "@/models/user"; import { User } from "@/models/user";
import { useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { getGravatarByUser } from "@/components/common/gravatar"; import { getGravatarByUser } from "@/components/common/gravatar";
@ -13,6 +13,7 @@ import { CommentInput } from "./comment-input";
import { createComment, deleteComment, listComments, updateComment } from "@/api/comment"; import { createComment, deleteComment, listComments, updateComment } from "@/api/comment";
import { OrderBy } from "@/models/common"; import { OrderBy } from "@/models/common";
import config from "@/config"; import config from "@/config";
import { formatDateTime } from "@/utils/common/datetime";
export function CommentItem( export function CommentItem(
@ -32,8 +33,10 @@ export function CommentItem(
setActiveInputId: (input: { id: number; type: 'reply' | 'edit' } | null) => void, setActiveInputId: (input: { id: number; type: 'reply' | 'edit' } | null) => void,
} }
) { ) {
const t = useTranslations("Comment") const locale = useLocale();
const commonT = useTranslations('Common') console.log("locale", locale);
const t = useTranslations("Comment");
const commonT = useTranslations("Common");
const clickToUserProfile = useToUserProfile(); const clickToUserProfile = useToUserProfile();
const clickToLogin = useToLogin(); const clickToLogin = useToLogin();
const { confirming, onClick, onBlur } = useDoubleConfirm(); const { confirming, onClick, onBlur } = useDoubleConfirm();
@ -157,7 +160,27 @@ export function CommentItem(
{getGravatarByUser(comment.user)} {getGravatarByUser(comment.user)}
</div> </div>
<div className="flex-1 pl-2 fade-in-up"> <div className="flex-1 pl-2 fade-in-up">
<div onClick={() => clickToUserProfile(comment.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up">{comment.user.nickname}</div> <div className="flex gap-2 md:gap-4 items-center">
<div onClick={() => clickToUserProfile(comment.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up">
{comment.user.nickname}
</div>
<span className="text-xs">{formatDateTime({
dateTimeString: comment.createdAt,
locale,
convertShortAgo: true,
unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") }
})}</span>
{comment.createdAt !== comment.updatedAt &&
<span className="text-xs">{t("edit_at", {
time: formatDateTime({
dateTimeString: comment.updatedAt,
locale,
convertShortAgo: true,
unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") }
})
})}</span>}
</div>
<p className="text-lg text-slate-600 dark:text-slate-400 fade-in"> <p className="text-lg text-slate-600 dark:text-slate-400 fade-in">
{ {
isPrivate && <Lock className="inline w-4 h-4 mr-1 mb-1 text-slate-500 dark:text-slate-400" /> isPrivate && <Lock className="inline w-4 h-4 mr-1 mb-1 text-slate-500 dark:text-slate-400" />
@ -169,7 +192,7 @@ export function CommentItem(
{comment.content} {comment.content}
</p> </p>
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400 flex items-center gap-4 fade-in"> <div className="mt-1 text-xs text-slate-500 dark:text-slate-400 flex items-center gap-4 fade-in">
<span>{new Date(comment.updatedAt).toLocaleString()}</span>
{/* 点赞按钮 */} {/* 点赞按钮 */}
<button <button
title={t(liked ? "unlike" : "like")} title={t(liked ? "unlike" : "like")}

View File

@ -16,7 +16,7 @@ export default getRequestConfig(async () => {
}) })
).then((msgs) => msgs.reduce((acc, msg) => deepmerge(acc, msg), {})); ).then((msgs) => msgs.reduce((acc, msg) => deepmerge(acc, msg), {}));
return { return {
locale: locales[0], locale: locales[locales.length - 1] || 'en',
messages messages
}; };
}); });

View File

@ -14,6 +14,7 @@
"delete_failed": "删除评论失败", "delete_failed": "删除评论失败",
"delete_success": "评论已经删除", "delete_success": "评论已经删除",
"edit": "编辑", "edit": "编辑",
"edit_at": "编辑于{time}",
"edit_failed": "编辑评论失败", "edit_failed": "编辑评论失败",
"edit_success": "评论已更新", "edit_success": "评论已更新",
"expand_replies": "展开 {count} 条", "expand_replies": "展开 {count} 条",
@ -31,7 +32,11 @@
"update": "更新" "update": "更新"
}, },
"Common":{ "Common":{
"login": "登录" "login": "登录",
"daysAgo": "天前",
"hoursAgo": "小时前",
"minutesAgo": "分钟前",
"secondsAgo": "秒前"
}, },
"Login": { "Login": {
"welcome": "欢迎回来", "welcome": "欢迎回来",

View File

@ -0,0 +1,52 @@
function getAgoString(diff: number, unitI18n: { secondsAgo: string; minutesAgo: string; hoursAgo: string; daysAgo: string; }): string {
let value: number, unit: string;
if (diff < 60 * 1000) {
value = Math.floor(diff / 1000);
unit = unitI18n.secondsAgo;
return `${value}${unit}`;
} else if (diff < 60 * 60 * 1000) {
value = Math.floor(diff / (60 * 1000));
unit = unitI18n.minutesAgo;
return `${value}${unit}`;
} else if (diff < 24 * 60 * 60 * 1000) {
value = Math.floor(diff / (60 * 60 * 1000));
unit = unitI18n.hoursAgo;
return `${value}${unit}`;
} else {
value = Math.floor(diff / (24 * 60 * 60 * 1000));
unit = unitI18n.daysAgo;
return `${value}${unit}`;
}
}
export function formatDateTime({
dateTimeString,
locale,
convertShortAgo,
convertShortAgoDuration = 3 * 24 * 60 * 60 * 1000,
unitI18n = { secondsAgo: "s ago", minutesAgo: "m ago", hoursAgo: "h ago", daysAgo: "d ago" }
}: {
dateTimeString: string;
locale: string;
convertShortAgo?: boolean;
convertShortAgoDuration?: number;
unitI18n?: { secondsAgo: string; minutesAgo: string; hoursAgo: string; daysAgo: string; };
}): string {
const date = new Date(dateTimeString);
const now = new Date();
const diff = now.getTime() - date.getTime();
if (convertShortAgo && diff >= 0 && diff < convertShortAgoDuration) {
return getAgoString(diff, unitI18n);
}
return date.toLocaleString(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
}