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; 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也用这个编辑器了..... // 我草react-md-editor这么好用无语了早知道v1也用这个编辑器了.....
// 不过居然没有汉化...有点可惜 // 不过居然没有汉化...有点可惜
// 我撤回刚才那句话编辑器有提供中文指令集我是sb。 // 我撤回刚才那句话编辑器有提供中文指令集我是sb。
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import MDEditor from '@uiw/react-md-editor'; import MDEditor from '@uiw/react-md-editor';
// 引入中文指令集 // 引入中文指令集
import { getCommands, getExtraCommands } from '@uiw/react-md-editor/commands-cn'; import { getCommands, getExtraCommands } from '@uiw/react-md-editor/commands-cn';
@@ -13,7 +13,10 @@ import {
makeStyles, makeStyles,
shorthands, shorthands,
tokens, tokens,
Input Input,
useToastController,
Toast,
ToastTitle
} from '@fluentui/react-components'; } from '@fluentui/react-components';
import { import {
NumberSymbol24Regular, NumberSymbol24Regular,
@@ -21,6 +24,7 @@ import {
Save24Regular Save24Regular
} from '@fluentui/react-icons'; } from '@fluentui/react-icons';
import { useLayout } from '../context/LayoutContext'; import { useLayout } from '../context/LayoutContext';
import { saveDraft, getDraft } from '../api';
const useStyles = makeStyles({ const useStyles = makeStyles({
container: { container: {
@@ -114,14 +118,82 @@ const remarkTagPlugin = () => {
const CreatePost: React.FC = () => { const CreatePost: React.FC = () => {
const styles = useStyles(); const styles = useStyles();
const { isDarkMode } = useLayout(); const { isDarkMode, toasterId } = useLayout();
const { dispatchToast } = useToastController(toasterId);
const [value, setValue] = useState<string | undefined>(""); 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 [showTagInput, setShowTagInput] = useState(false);
const [tagInputValue, setTagInputValue] = useState(""); 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 = () => { const handleTagSubmit = () => {
if (!tagInputValue.trim()) { if (!tagInputValue.trim()) {
setShowTagInput(false); setShowTagInput(false);
@@ -146,9 +218,24 @@ const CreatePost: React.FC = () => {
}; };
useEffect(() => { useEffect(() => {
// 模拟自动保存时间显示 if (value === undefined) return;
const now = new Date(); valueRef.current = value ?? "";
setLastSaved(now.toLocaleTimeString('zh-CN', { hour12: false })); 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 ( return (
@@ -188,11 +275,11 @@ const CreatePost: React.FC = () => {
</Button> </Button>
<div className={styles.tagButtonWrapper}> <div className={styles.tagButtonWrapper}>
{showTagInput && ( {showTagInput && (
<div className={styles.tagInputContainer}> <div className={styles.tagInputContainer} ref={tagInputRef}>
<Input <Input
value={tagInputValue} value={tagInputValue}
onChange={(e, data) => setTagInputValue(data.value)} onChange={(_, data) => setTagInputValue(data.value)}
placeholder="输入标签..." placeholder="输入tag..."
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') handleTagSubmit(); if (e.key === 'Enter') handleTagSubmit();
}} }}
@@ -201,6 +288,7 @@ const CreatePost: React.FC = () => {
</div> </div>
)} )}
<Button <Button
ref={tagButtonRef}
appearance="subtle" appearance="subtle"
icon={<NumberSymbol24Regular />} icon={<NumberSymbol24Regular />}
onClick={() => setShowTagInput(!showTagInput)} onClick={() => setShowTagInput(!showTagInput)}
@@ -217,6 +305,15 @@ const CreatePost: React.FC = () => {
<Button <Button
appearance="outline" appearance="outline"
icon={<Save24Regular />} icon={<Save24Regular />}
onClick={() => {
persistDraft();
dispatchToast(
<Toast>
<ToastTitle>稿</ToastTitle>
</Toast>,
{ intent: 'success' }
);
}}
> >
稿 稿
</Button> </Button>

View File

@@ -8,12 +8,16 @@ import {
Spinner, Spinner,
Badge, Badge,
Button, Button,
useToastController,
Toast,
ToastTitle,
} from '@fluentui/react-components'; } from '@fluentui/react-components';
import { import {
CheckmarkCircle20Filled, CheckmarkCircle20Filled,
DismissCircle20Filled, DismissCircle20Filled,
ArrowClockwise20Regular ArrowClockwise20Regular
} from '@fluentui/react-icons'; } from '@fluentui/react-icons';
import { useLayout } from '../context/LayoutContext';
import { testApiStatus, getStatics } from '../api'; import { testApiStatus, getStatics } from '../api';
import type { StaticsData } from '../api'; import type { StaticsData } from '../api';
@@ -71,6 +75,8 @@ const useStyles = makeStyles({
const StatusDisplay: React.FC = () => { const StatusDisplay: React.FC = () => {
const styles = useStyles(); const styles = useStyles();
const { toasterId } = useLayout();
const { dispatchToast } = useToastController(toasterId);
const [isApiOnline, setIsApiOnline] = useState<boolean | null>(null); const [isApiOnline, setIsApiOnline] = useState<boolean | null>(null);
const [statics, setStatics] = useState<StaticsData | null>(null); const [statics, setStatics] = useState<StaticsData | null>(null);
const [isTestLoading, setIsTestLoading] = useState<boolean>(true); const [isTestLoading, setIsTestLoading] = useState<boolean>(true);
@@ -80,23 +86,31 @@ const StatusDisplay: React.FC = () => {
setIsApiOnline(online); setIsApiOnline(online);
setIsTestLoading(false); setIsTestLoading(false);
}, []); }, []);
const refreshStatics = useCallback(async () => { const refreshStatics = useCallback(async (isManual: boolean = false) => {
setIsStaticsLoading(true); setIsStaticsLoading(true);
try { try {
const data = await getStatics(); const data = await getStatics();
setStatics(data); setStatics(data);
if (isManual) {
dispatchToast(
<Toast>
<ToastTitle></ToastTitle>
</Toast>,
{ intent: 'success' }
);
}
} catch (error) { } catch (error) {
console.error('Failed to refresh statics:', error); console.error('Failed to refresh statics:', error);
setStatics(null); // 失败时重置数据 setStatics(null); // 失败时重置数据
} finally { } finally {
setIsStaticsLoading(false); setIsStaticsLoading(false);
} }
}, []); }, [dispatchToast]);
useEffect(() => { useEffect(() => {
// 初始加载 // 初始加载
checkStatus(); checkStatus();
refreshStatics(); refreshStatics(false);
// 每 10 秒测试一次后端 API 状态 // 每 10 秒测试一次后端 API 状态
const testInterval = setInterval(checkStatus, 10000); const testInterval = setInterval(checkStatus, 10000);
@@ -141,7 +155,7 @@ const StatusDisplay: React.FC = () => {
className={styles.refreshButton} className={styles.refreshButton}
appearance="subtle" appearance="subtle"
icon={<ArrowClockwise20Regular fontSize={16} />} icon={<ArrowClockwise20Regular fontSize={16} />}
onClick={refreshStatics} onClick={() => refreshStatics(true)}
disabled={isStaticsLoading} disabled={isStaticsLoading}
title="刷新统计数据" title="刷新统计数据"
/> />

View File

@@ -9,6 +9,7 @@ interface LayoutContextType {
toggleSidebar: () => void; toggleSidebar: () => void;
isDarkMode: boolean; isDarkMode: boolean;
toggleTheme: () => void; toggleTheme: () => void;
toasterId: string;
} }
const LayoutContext = createContext<LayoutContextType | undefined>(undefined); 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); const toggleTheme = () => setIsDarkMode(prev => !prev);
return ( return (
<LayoutContext.Provider value={{ settings, isSidebarCollapsed, toggleSidebar, isDarkMode, toggleTheme }}> <LayoutContext.Provider value={{ settings, isSidebarCollapsed, toggleSidebar, isDarkMode, toggleTheme, toasterId }}>
{children} {children}
</LayoutContext.Provider> </LayoutContext.Provider>
); );