Files
v2/front/src/components/StatusDisplay.tsx
2026-01-23 14:38:47 +08:00

200 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// v1的时候就连统计信息也5秒获取一次不妥所以改成逻辑触发刷新
import React, { useState, useEffect, useCallback } from 'react';
import {
makeStyles,
tokens,
Card,
CardHeader,
Text,
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';
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 { toasterId, refreshTrigger } = 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);
const [isStaticsLoading, setIsStaticsLoading] = useState<boolean>(true);
const checkStatus = useCallback(async () => {
const online = await testApiStatus();
setIsApiOnline(online);
setIsTestLoading(false);
}, []);
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(false);
// 每 10 秒测试一次后端 API 状态
const testInterval = setInterval(checkStatus, 10000);
return () => {
clearInterval(testInterval);
};
}, [checkStatus, refreshStatics]);
// Listen for global refresh trigger
useEffect(() => {
if (refreshTrigger > 0) {
refreshStatics(false);
}
}, [refreshTrigger, 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(true)}
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;