diff --git a/apps/admin/src/components/BackTop.tsx b/apps/admin/src/components/BackTop.tsx index 59ace44..4360523 100644 --- a/apps/admin/src/components/BackTop.tsx +++ b/apps/admin/src/components/BackTop.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { Button, Tooltip } from 'antd'; import { VerticalAlignTopOutlined } from '@ant-design/icons'; +import { useEventCallback, useEventListener } from 'usehooks-ts'; /** * 全局「回到顶部」浮动按钮:长列表滚动超过 400px 后出现。 @@ -8,13 +9,13 @@ import { VerticalAlignTopOutlined } from '@ant-design/icons'; */ export const BackTop: React.FC<{ threshold?: number }> = ({ threshold = 400 }) => { const [visible, setVisible] = useState(false); + const updateVisible = useEventCallback(() => setVisible(window.scrollY > threshold)); useEffect(() => { - const onScroll = () => setVisible(window.scrollY > threshold); - onScroll(); - window.addEventListener('scroll', onScroll, { passive: true }); - return () => window.removeEventListener('scroll', onScroll); - }, [threshold]); + updateVisible(); + }, [threshold, updateVisible]); + + useEventListener('scroll', updateVisible, undefined, { passive: true }); if (!visible) return null; diff --git a/apps/admin/src/components/EditableCell/index.tsx b/apps/admin/src/components/EditableCell/index.tsx index 5e0f12f..32bfe8c 100644 --- a/apps/admin/src/components/EditableCell/index.tsx +++ b/apps/admin/src/components/EditableCell/index.tsx @@ -3,6 +3,7 @@ import { DatePicker, Input, InputNumber, Select, Spin, Tooltip } from 'antd'; import dayjs, { type Dayjs } from 'dayjs'; import equal from 'fast-deep-equal'; import { usePermission } from '../../hooks/usePermission'; +import { useTimeout } from 'usehooks-ts'; import { useEditableCellStore } from '../../store/editableCell/editableCellStore'; import { message } from '../../ui/app-message'; import './style.css'; @@ -102,16 +103,10 @@ const EditableCell = ({ ); // 保存成功后短暂显示「撤销」入口:记录保存前的序列化旧值 const [undoMeta, setUndoMeta] = useState<{ serializedPrevious: unknown } | null>(null); - const undoTimerRef = useRef(undefined); + // 撤销入口 6 秒后自动消失;useTimeout 在 undoMeta 置空/组件卸载时自动清理 + useTimeout(() => setUndoMeta(null), undoMeta ? 6_000 : null); const enabled = !disabled && (!permission || hasPermission(permission)); - useEffect( - () => () => { - window.clearTimeout(undoTimerRef.current); - }, - [], - ); - const original = useMemo( () => serializeEditableValue( @@ -146,10 +141,8 @@ const EditableCell = ({ await onSave(parseValue ? parseValue(serialized) : (serialized as Value)); useEditableCellStore.getState().clearIfActive(idRef.current); setEditing(false); - // 提供 6 秒内的撤销入口(把旧值再保存一次) + // 提供 6 秒内的撤销入口(把旧值再保存一次);useTimeout 负责到时自动清除 setUndoMeta({ serializedPrevious: previousValue }); - window.clearTimeout(undoTimerRef.current); - undoTimerRef.current = window.setTimeout(() => setUndoMeta(null), 6_000); return true; } catch (error) { message.error(getErrorMessage(error, '保存失败')); @@ -210,7 +203,6 @@ const EditableCell = ({ } useEditableCellStore.getState().setActiveCell({ id: idRef.current, save }); // 重新进入编辑时清掉上一次的撤销入口 - window.clearTimeout(undoTimerRef.current); setUndoMeta(null); setDraft(normalizeEditableValue(formatValue ? formatValue(value) : value, editor)); setEditing(true); @@ -218,7 +210,6 @@ const EditableCell = ({ const handleUndo = async () => { if (!undoMeta) return; - window.clearTimeout(undoTimerRef.current); setUndoMeta(null); try { await onSave( diff --git a/apps/admin/src/pages/Notifications/index.tsx b/apps/admin/src/pages/Notifications/index.tsx index ab85cf4..7de21e0 100644 --- a/apps/admin/src/pages/Notifications/index.tsx +++ b/apps/admin/src/pages/Notifications/index.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; +import dayjs from 'dayjs'; import { validateResponse } from '../../utils/validate'; import { notificationsSchema } from '../../api/schemas'; import { List, Typography, Menu, Layout, Button, Spin, Space, Grid, Select } from 'antd'; @@ -43,15 +44,11 @@ const typeMap: Record = { }; function timeAgo(dateStr: string): string { - const diff = Date.now() - new Date(dateStr).getTime(); - const mins = Math.floor(diff / 60000); - if (mins < 1) return '刚刚'; - if (mins < 60) return `${mins}分钟前`; - const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours}小时前`; - const days = Math.floor(hours / 24); - if (days < 7) return `${days}天前`; - return new Date(dateStr).toLocaleDateString('zh-CN'); + const diff = Date.now() - dayjs(dateStr).valueOf(); + if (diff < 60_000) return '刚刚'; + // 7 天内用相对时间(dayjs relativeTime 已全局配置),更早显示具体日期 + if (diff < 7 * 86_400_000) return dayjs(dateStr).fromNow(); + return dayjs(dateStr).format('YYYY/M/D'); } const FILTER_ITEMS: Array<{ key: string; icon: React.ReactNode; label: string }> = [