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