This commit is contained in:
2026-01-23 13:22:07 +08:00
parent 771db7e59a
commit 50371f9a2e
4 changed files with 135 additions and 15 deletions

View File

@@ -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');
};

View File

@@ -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>

View File

@@ -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="刷新统计数据"
/>

View File

@@ -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>
);