TimeAgo.tsx 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. interface TimeAgoProps {
  4. date: string | Date | null | undefined;
  5. className?: string;
  6. }
  7. export default function TimeAgo({ date, className = '' }: TimeAgoProps) {
  8. const [timeAgo, setTimeAgo] = useState<string>('-');
  9. useEffect(() => {
  10. if (!date) return;
  11. const calculateTimeAgo = () => {
  12. let dateObj: Date;
  13. // === 核心修复逻辑 ===
  14. if (typeof date === 'string') {
  15. // 1. 兼容空格格式
  16. let normalizedDate = date.replace(' ', 'T');
  17. // 2. 如果没有时区标识,强制追加 'Z',将其视为 UTC 时间
  18. if (!normalizedDate.endsWith('Z') && !normalizedDate.includes('+')) {
  19. normalizedDate += 'Z';
  20. }
  21. dateObj = new Date(normalizedDate);
  22. } else {
  23. dateObj = new Date(date);
  24. }
  25. // 检查日期有效性
  26. if (isNaN(dateObj.getTime())) {
  27. setTimeAgo('-');
  28. return;
  29. }
  30. const past = dateObj.getTime();
  31. const now = new Date().getTime();
  32. const diffInSeconds = Math.floor((now - past) / 1000);
  33. // 处理未来时间(可能是本地时间偏差或服务器时间超前)
  34. if (diffInSeconds < 0) {
  35. setTimeAgo('刚刚');
  36. return;
  37. }
  38. if (diffInSeconds < 60) {
  39. setTimeAgo(`${diffInSeconds}秒前`);
  40. } else if (diffInSeconds < 3600) {
  41. setTimeAgo(`${Math.floor(diffInSeconds / 60)}分钟前`);
  42. } else if (diffInSeconds < 86400) {
  43. setTimeAgo(`${Math.floor(diffInSeconds / 3600)}小时前`);
  44. } else if (diffInSeconds < 2592000) { // 30天
  45. setTimeAgo(`${Math.floor(diffInSeconds / 86400)}天前`);
  46. } else if (diffInSeconds < 31536000) { // 365天
  47. setTimeAgo(`${Math.floor(diffInSeconds / 2592000)}个月前`);
  48. } else {
  49. setTimeAgo(`${Math.floor(diffInSeconds / 31536000)}年前`);
  50. }
  51. };
  52. calculateTimeAgo();
  53. // 每分钟更新一次文本
  54. const interval = setInterval(calculateTimeAgo, 60000);
  55. return () => clearInterval(interval);
  56. }, [date]);
  57. if (!date) return <span className="text-gray-300">-</span>;
  58. // 使用 title 属性显示完整的本地时间,方便鼠标悬停查看
  59. // 注意:这里仅用于 title 属性的展示,不需要像 LocalTime 那样处理 SSR 水合问题
  60. return (
  61. <span className={className} title={new Date(date).toLocaleString()}>
  62. {timeAgo}
  63. </span>
  64. );
  65. }