实现投稿ui

This commit is contained in:
2026-01-23 11:16:07 +08:00
parent 2bb3db5966
commit 5932adcd9a
8 changed files with 839 additions and 16 deletions

View File

@@ -1,10 +1,10 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { MainLayout } from './layouts/MainLayout';
import About from './components/About';
import CreatePost from './components/CreatePost';
import './App.css';
const Home = () => <h1>Home Page</h1>;
const CreatePost = () => <h1>Create Post</h1>;
const NotFound = () => <h1>404 Not Found</h1>;
function App() {

View File

@@ -48,3 +48,36 @@ export const getAbout = async (): Promise<string> => {
throw error;
}
};
export interface StaticsData {
posts: number;
comments: number;
images: number;
}
export const testApiStatus = async (): Promise<boolean> => {
try {
const response = await fetch('/api/test');
return response.ok;
} catch (error) {
return false;
}
};
export const getStatics = async (): Promise<StaticsData> => {
try {
const response = await fetch('/api/statics');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
if (json.code === 1000) {
return json.data;
} else {
throw new Error('Invalid response code');
}
} catch (error) {
console.error('Failed to fetch statics:', error);
throw error;
}
};

View File

@@ -0,0 +1,126 @@
// 我草react-md-editor这么好用无语了早知道v1也用这个编辑器了.....
// 不过居然没有汉化...有点可惜
// 我撤回刚才那句话编辑器有提供中文指令集我是sb。
import React, { useState, useEffect } from 'react';
import MDEditor from '@uiw/react-md-editor';
// 引入中文指令集
import { getCommands, getExtraCommands } from '@uiw/react-md-editor/commands-cn';
// 导入编辑器样式
import '@uiw/react-md-editor/markdown-editor.css';
import {
Button,
Text,
makeStyles,
shorthands,
tokens
} from '@fluentui/react-components';
import {
NumberSymbol24Regular,
Send24Regular,
Save24Regular
} from '@fluentui/react-icons';
import { useLayout } from '../context/LayoutContext';
const useStyles = makeStyles({
container: {
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
maxWidth: '1000px',
margin: '0 auto',
...shorthands.padding('20px'),
boxSizing: 'border-box',
},
editorWrapper: {
flexGrow: 1,
minHeight: '400px',
marginBottom: '20px',
'& .w-md-editor': {
height: '100% !important',
boxShadow: tokens.shadow16,
borderRadius: tokens.borderRadiusMedium,
}
},
footer: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
footerLeft: {
display: 'flex',
gap: '12px',
alignItems: 'center',
},
footerRight: {
display: 'flex',
gap: '12px',
alignItems: 'center',
},
autoSaveText: {
color: tokens.colorNeutralForeground4,
fontSize: tokens.fontSizeBase200,
}
});
const CreatePost: React.FC = () => {
const styles = useStyles();
const { isDarkMode } = useLayout();
const [value, setValue] = useState<string | undefined>("");
const [lastSaved, setLastSaved] = useState<string>("");
useEffect(() => {
// 模拟自动保存时间显示
const now = new Date();
setLastSaved(now.toLocaleTimeString('zh-CN', { hour12: false }));
}, []);
return (
<div className={styles.container}>
<div className={styles.editorWrapper} data-color-mode={isDarkMode ? "dark" : "light"}>
<MDEditor
value={value}
onChange={setValue}
preview="live"
height="100%"
textareaProps={{
placeholder: "请在此输入投稿内容...",
}}
commands={getCommands()}
extraCommands={getExtraCommands()}
/>
</div>
<div className={styles.footer}>
<div className={styles.footerLeft}>
<Button
appearance="primary"
icon={<Send24Regular />}
>
</Button>
<Button
appearance="subtle"
icon={<NumberSymbol24Regular />}
>
</Button>
</div>
<div className={styles.footerRight}>
<Text className={styles.autoSaveText}>
{lastSaved}
</Text>
<Button
appearance="outline"
icon={<Save24Regular />}
>
稿
</Button>
</div>
</div>
</div>
);
};
export default CreatePost;

View File

@@ -0,0 +1,177 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
makeStyles,
tokens,
Card,
CardHeader,
Text,
Spinner,
Badge,
Button,
} from '@fluentui/react-components';
import {
CheckmarkCircle20Filled,
DismissCircle20Filled,
ArrowClockwise20Regular
} from '@fluentui/react-icons';
import { testApiStatus, getStatics } from '../api';
import type { StaticsData } from '../api';
const useStyles = makeStyles({
container: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalS,
width: '100%',
},
card: {
width: '100%',
},
statusContainer: {
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
paddingBottom: tokens.spacingVerticalM,
},
statusText: {
display: 'flex',
alignItems: 'center',
gap: tokens.spacingHorizontalSNudge,
},
online: {
color: tokens.colorStatusSuccessForeground1,
fontSize: tokens.fontSizeBase200,
},
offline: {
color: tokens.colorStatusDangerForeground1,
fontSize: tokens.fontSizeBase200,
},
statsContainer: {
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
paddingBottom: tokens.spacingVerticalM,
display: 'flex',
flexDirection: 'column',
gap: '4px',
},
statRow: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
refreshButton: {
minWidth: 'auto',
padding: '2px',
},
labelText: {
fontSize: tokens.fontSizeBase200,
},
headerText: {
fontSize: tokens.fontSizeBase300,
fontWeight: tokens.fontWeightSemibold,
}
});
const StatusDisplay: React.FC = () => {
const styles = useStyles();
const [isApiOnline, setIsApiOnline] = useState<boolean | null>(null);
const [statics, setStatics] = useState<StaticsData | null>(null);
const [isTestLoading, setIsTestLoading] = useState<boolean>(true);
const [isStaticsLoading, setIsStaticsLoading] = useState<boolean>(true);
const checkStatus = useCallback(async () => {
const online = await testApiStatus();
setIsApiOnline(online);
setIsTestLoading(false);
}, []);
const refreshStatics = useCallback(async () => {
setIsStaticsLoading(true);
try {
const data = await getStatics();
setStatics(data);
} catch (error) {
console.error('Failed to refresh statics:', error);
setStatics(null); // 失败时重置数据
} finally {
setIsStaticsLoading(false);
}
}, []);
useEffect(() => {
// 初始加载
checkStatus();
refreshStatics();
// 每 10 秒测试一次后端 API 状态
const testInterval = setInterval(checkStatus, 10000);
return () => {
clearInterval(testInterval);
};
}, [checkStatus, refreshStatics]);
return (
<div className={styles.container}>
<Card className={styles.card}>
<CardHeader
header={<Text className={styles.headerText}></Text>}
/>
<div className={styles.statusContainer}>
{isTestLoading && isApiOnline === null ? (
<Spinner size="tiny" label="检查中..." />
) : (
<div className={styles.statusText}>
{isApiOnline ? (
<>
<CheckmarkCircle20Filled fontSize={16} className={styles.online} />
<Text className={styles.online}>线</Text>
</>
) : (
<>
<DismissCircle20Filled fontSize={16} className={styles.offline} />
<Text className={styles.offline}>线</Text>
</>
)}
</div>
)}
</div>
</Card>
<Card className={styles.card}>
<CardHeader
header={<Text className={styles.headerText}></Text>}
action={
<Button
className={styles.refreshButton}
appearance="subtle"
icon={<ArrowClockwise20Regular fontSize={16} />}
onClick={refreshStatics}
disabled={isStaticsLoading}
title="刷新统计数据"
/>
}
/>
<div className={styles.statsContainer}>
{isStaticsLoading && !statics ? (
<Spinner size="tiny" label="加载中..." />
) : statics ? (
<>
<div className={styles.statRow}>
<Text className={styles.labelText}>稿:</Text>
<Badge size="small" appearance="outline" color="brand">{statics.posts}</Badge>
</div>
<div className={styles.statRow}>
<Text className={styles.labelText}>:</Text>
<Badge size="small" appearance="outline" color="brand">{statics.comments}</Badge>
</div>
<div className={styles.statRow}>
<Text className={styles.labelText}>:</Text>
<Badge size="small" appearance="outline" color="brand">{statics.images}</Badge>
</div>
</>
) : (
<Text className={styles.labelText} italic></Text>
)}
</div>
</Card>
</div>
);
};
export default StatusDisplay;

View File

@@ -3,6 +3,7 @@ import { Outlet } from 'react-router-dom';
import Header from './components/Header';
import Sidebar from './components/Sidebar';
import Footer from './components/Footer';
import StatusDisplay from '../components/StatusDisplay';
import { LayoutProvider, useLayout } from '../context/LayoutContext';
const useStyles = makeStyles({
@@ -21,12 +22,13 @@ const useStyles = makeStyles({
container: {
display: 'flex',
flex: '1 1 auto',
overflow: 'hidden',
height: 'calc(100vh - 64px)', // 减去 Header 的高度
position: 'relative',
},
content: {
flex: '1 1 auto',
padding: '20px',
paddingBottom: '64px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
@@ -36,6 +38,17 @@ const useStyles = makeStyles({
minHeight: 0,
boxSizing: 'border-box',
},
rightPanel: {
width: '240px',
flexShrink: 0,
borderLeft: `1px solid ${tokens.colorNeutralStroke1}`,
padding: '20px',
backgroundColor: tokens.colorNeutralBackground1,
overflowY: 'auto',
'@media (max-width: 768px)': {
display: 'none',
},
}
});
const LayoutContent = ({ toasterId }: { toasterId: string }) => {
@@ -51,6 +64,9 @@ const LayoutContent = ({ toasterId }: { toasterId: string }) => {
<main className={styles.content}>
<Outlet />
</main>
<aside className={styles.rightPanel}>
<StatusDisplay />
</aside>
</div>
<Footer />
<Toaster toasterId={toasterId} position="top-end" />