mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 19:16:24 +00:00
feat: 添加评论时间格式化功能,优化评论项显示,支持显示编辑时间
This commit is contained in:
@ -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>
|
||||||
|
@ -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")}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -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": "欢迎回来",
|
||||||
|
52
web/src/utils/common/datetime.ts
Normal file
52
web/src/utils/common/datetime.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
Reference in New Issue
Block a user