1321
This commit is contained in:
@@ -81,3 +81,11 @@ export const getStatics = async (): Promise<StaticsData> => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const saveDraft = (content: string): void => {
|
||||
localStorage.setItem('draft', content);
|
||||
};
|
||||
|
||||
export const getDraft = (): string | null => {
|
||||
return localStorage.getItem('draft');
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 我草,react-md-editor这么好用,无语了,早知道v1也用这个编辑器了.....
|
||||
// 不过居然没有汉化...有点可惜
|
||||
// 我撤回刚才那句话,编辑器有提供中文指令集,我是sb。
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
// 引入中文指令集
|
||||
import { getCommands, getExtraCommands } from '@uiw/react-md-editor/commands-cn';
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
makeStyles,
|
||||
shorthands,
|
||||
tokens,
|
||||
Input
|
||||
Input,
|
||||
useToastController,
|
||||
Toast,
|
||||
ToastTitle
|
||||
} from '@fluentui/react-components';
|
||||
import {
|
||||
NumberSymbol24Regular,
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
Save24Regular
|
||||
} from '@fluentui/react-icons';
|
||||
import { useLayout } from '../context/LayoutContext';
|
||||
import { saveDraft, getDraft } from '../api';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
@@ -114,14 +118,82 @@ const remarkTagPlugin = () => {
|
||||
|
||||
const CreatePost: React.FC = () => {
|
||||
const styles = useStyles();
|
||||
const { isDarkMode } = useLayout();
|
||||
const { isDarkMode, toasterId } = useLayout();
|
||||
const { dispatchToast } = useToastController(toasterId);
|
||||
const [value, setValue] = useState<string | undefined>("");
|
||||
const [lastSaved, setLastSaved] = useState<string>("");
|
||||
const [lastSaved, setLastSaved] = useState<string>(() => new Date().toLocaleTimeString('zh-CN', { hour12: false }));
|
||||
|
||||
// 标签输入相关状态
|
||||
const [showTagInput, setShowTagInput] = useState(false);
|
||||
const [tagInputValue, setTagInputValue] = useState("");
|
||||
|
||||
const tagInputRef = useRef<HTMLDivElement>(null);
|
||||
const tagButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const valueRef = useRef<string>("");
|
||||
const autoSaveIntervalRef = useRef<number | null>(null);
|
||||
const lastInputAtRef = useRef<number | null>(null);
|
||||
const activeElapsedRef = useRef<number>(0);
|
||||
const hasStartedAutoSaveRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
showTagInput &&
|
||||
tagInputRef.current &&
|
||||
!tagInputRef.current.contains(event.target as Node) &&
|
||||
tagButtonRef.current &&
|
||||
!tagButtonRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowTagInput(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [showTagInput]);
|
||||
|
||||
useEffect(() => {
|
||||
const draft = getDraft();
|
||||
if (draft) {
|
||||
setValue(draft);
|
||||
dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>读取草稿成功!</ToastTitle>
|
||||
</Toast>,
|
||||
{ intent: 'success' }
|
||||
);
|
||||
}
|
||||
}, [dispatchToast]);
|
||||
|
||||
const formatTime = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
|
||||
const persistDraft = useCallback(() => {
|
||||
saveDraft(valueRef.current);
|
||||
setLastSaved(formatTime());
|
||||
}, []);
|
||||
|
||||
const startAutoSaveInterval = useCallback(() => {
|
||||
if (autoSaveIntervalRef.current !== null) return;
|
||||
autoSaveIntervalRef.current = window.setInterval(() => {
|
||||
const lastInputAt = lastInputAtRef.current;
|
||||
if (!lastInputAt) return;
|
||||
if (Date.now() - lastInputAt <= 3000) {
|
||||
activeElapsedRef.current += 1000;
|
||||
if (activeElapsedRef.current >= 30000) {
|
||||
if (autoSaveIntervalRef.current !== null) {
|
||||
window.clearInterval(autoSaveIntervalRef.current);
|
||||
}
|
||||
autoSaveIntervalRef.current = null;
|
||||
activeElapsedRef.current = 0;
|
||||
hasStartedAutoSaveRef.current = false;
|
||||
persistDraft();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}, [persistDraft]);
|
||||
|
||||
const handleTagSubmit = () => {
|
||||
if (!tagInputValue.trim()) {
|
||||
setShowTagInput(false);
|
||||
@@ -146,9 +218,24 @@ const CreatePost: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟自动保存时间显示
|
||||
const now = new Date();
|
||||
setLastSaved(now.toLocaleTimeString('zh-CN', { hour12: false }));
|
||||
if (value === undefined) return;
|
||||
valueRef.current = value ?? "";
|
||||
if (value.length === 0 && !hasStartedAutoSaveRef.current) return;
|
||||
|
||||
lastInputAtRef.current = Date.now();
|
||||
if (!hasStartedAutoSaveRef.current) {
|
||||
hasStartedAutoSaveRef.current = true;
|
||||
persistDraft();
|
||||
}
|
||||
startAutoSaveInterval();
|
||||
}, [value, persistDraft, startAutoSaveInterval]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (autoSaveIntervalRef.current !== null) {
|
||||
window.clearInterval(autoSaveIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -188,11 +275,11 @@ const CreatePost: React.FC = () => {
|
||||
</Button>
|
||||
<div className={styles.tagButtonWrapper}>
|
||||
{showTagInput && (
|
||||
<div className={styles.tagInputContainer}>
|
||||
<div className={styles.tagInputContainer} ref={tagInputRef}>
|
||||
<Input
|
||||
value={tagInputValue}
|
||||
onChange={(e, data) => setTagInputValue(data.value)}
|
||||
placeholder="输入标签..."
|
||||
onChange={(_, data) => setTagInputValue(data.value)}
|
||||
placeholder="输入tag..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleTagSubmit();
|
||||
}}
|
||||
@@ -201,6 +288,7 @@ const CreatePost: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
ref={tagButtonRef}
|
||||
appearance="subtle"
|
||||
icon={<NumberSymbol24Regular />}
|
||||
onClick={() => setShowTagInput(!showTagInput)}
|
||||
@@ -217,6 +305,15 @@ const CreatePost: React.FC = () => {
|
||||
<Button
|
||||
appearance="outline"
|
||||
icon={<Save24Regular />}
|
||||
onClick={() => {
|
||||
persistDraft();
|
||||
dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>保存草稿成功!</ToastTitle>
|
||||
</Toast>,
|
||||
{ intent: 'success' }
|
||||
);
|
||||
}}
|
||||
>
|
||||
保存草稿
|
||||
</Button>
|
||||
|
||||
@@ -8,12 +8,16 @@ import {
|
||||
Spinner,
|
||||
Badge,
|
||||
Button,
|
||||
useToastController,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
} from '@fluentui/react-components';
|
||||
import {
|
||||
CheckmarkCircle20Filled,
|
||||
DismissCircle20Filled,
|
||||
ArrowClockwise20Regular
|
||||
} from '@fluentui/react-icons';
|
||||
import { useLayout } from '../context/LayoutContext';
|
||||
import { testApiStatus, getStatics } from '../api';
|
||||
import type { StaticsData } from '../api';
|
||||
|
||||
@@ -71,6 +75,8 @@ const useStyles = makeStyles({
|
||||
|
||||
const StatusDisplay: React.FC = () => {
|
||||
const styles = useStyles();
|
||||
const { toasterId } = useLayout();
|
||||
const { dispatchToast } = useToastController(toasterId);
|
||||
const [isApiOnline, setIsApiOnline] = useState<boolean | null>(null);
|
||||
const [statics, setStatics] = useState<StaticsData | null>(null);
|
||||
const [isTestLoading, setIsTestLoading] = useState<boolean>(true);
|
||||
@@ -80,23 +86,31 @@ const StatusDisplay: React.FC = () => {
|
||||
setIsApiOnline(online);
|
||||
setIsTestLoading(false);
|
||||
}, []);
|
||||
const refreshStatics = useCallback(async () => {
|
||||
const refreshStatics = useCallback(async (isManual: boolean = false) => {
|
||||
setIsStaticsLoading(true);
|
||||
try {
|
||||
const data = await getStatics();
|
||||
setStatics(data);
|
||||
if (isManual) {
|
||||
dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>统计数据已刷新</ToastTitle>
|
||||
</Toast>,
|
||||
{ intent: 'success' }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh statics:', error);
|
||||
setStatics(null); // 失败时重置数据
|
||||
} finally {
|
||||
setIsStaticsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [dispatchToast]);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始加载
|
||||
checkStatus();
|
||||
refreshStatics();
|
||||
refreshStatics(false);
|
||||
|
||||
// 每 10 秒测试一次后端 API 状态
|
||||
const testInterval = setInterval(checkStatus, 10000);
|
||||
@@ -141,7 +155,7 @@ const StatusDisplay: React.FC = () => {
|
||||
className={styles.refreshButton}
|
||||
appearance="subtle"
|
||||
icon={<ArrowClockwise20Regular fontSize={16} />}
|
||||
onClick={refreshStatics}
|
||||
onClick={() => refreshStatics(true)}
|
||||
disabled={isStaticsLoading}
|
||||
title="刷新统计数据"
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ interface LayoutContextType {
|
||||
toggleSidebar: () => void;
|
||||
isDarkMode: boolean;
|
||||
toggleTheme: () => void;
|
||||
toasterId: string;
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
|
||||
@@ -48,7 +49,7 @@ export const LayoutProvider: React.FC<{ children: React.ReactNode; toasterId: st
|
||||
const toggleTheme = () => setIsDarkMode(prev => !prev);
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={{ settings, isSidebarCollapsed, toggleSidebar, isDarkMode, toggleTheme }}>
|
||||
<LayoutContext.Provider value={{ settings, isSidebarCollapsed, toggleSidebar, isDarkMode, toggleTheme, toasterId }}>
|
||||
{children}
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user