diff --git a/back/main.py b/back/main.py index 483d9c9..8516ffd 100644 --- a/back/main.py +++ b/back/main.py @@ -581,6 +581,19 @@ def get_post_info(): return jsonify({"code": 1000, "data": data}) except Exception as e: return jsonify({"code": 2003, "data": str(e)}) + +@app.route('/api/hot_topics', methods=['GET']) +def get_hot_topics(): + try: + rows = db.session.query( + Hashtag.name, + db.func.count(Hashtag.name).label('count') + ).group_by(Hashtag.name).order_by(db.func.count(Hashtag.name).desc()).limit(5).all() + + data = [{"name": name, "count": int(count)} for name, count in rows] + return jsonify({"code": 1000, "data": {"list": data}}) + except Exception as e: + return jsonify({"code": 2003, "data": str(e)}) # --- 彩蛋 --- @app.route('/api/teapot', methods=['GET']) diff --git a/front/src/api.ts b/front/src/api.ts index 25373d6..ce6e233 100644 --- a/front/src/api.ts +++ b/front/src/api.ts @@ -55,6 +55,11 @@ export interface StaticsData { images: number; } +export interface HotTopicItem { + name: string; + count: number; +} + export const testApiStatus = async (): Promise => { try { const response = await fetch('/api/test'); @@ -123,6 +128,23 @@ const notifyInvalidIdentity = () => { } }; +export const getHotTopics = async (): Promise => { + try { + const response = await fetch('/api/hot_topics'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const json = await response.json(); + if (json.code === 1000 && json.data && Array.isArray(json.data.list)) { + return json.data.list as HotTopicItem[]; + } + throw new Error('Invalid response code or missing data'); + } catch (error) { + console.error('Failed to fetch hot topics:', error); + throw error; + } +}; + const handlePostApiCode = (json: any) => { if (json && json.code === 2004) { notifyInvalidIdentity(); diff --git a/front/src/components/StatusDisplay.tsx b/front/src/components/Widgets.tsx similarity index 68% rename from front/src/components/StatusDisplay.tsx rename to front/src/components/Widgets.tsx index 92512c8..9870f89 100644 --- a/front/src/components/StatusDisplay.tsx +++ b/front/src/components/Widgets.tsx @@ -12,6 +12,7 @@ import { useToastController, Toast, ToastTitle, + Divider, } from '@fluentui/react-components'; import { CheckmarkCircle20Filled, @@ -19,8 +20,8 @@ import { ArrowClockwise20Regular } from '@fluentui/react-icons'; import { useLayout } from '../context/LayoutContext'; -import { testApiStatus, getStatics } from '../api'; -import type { StaticsData } from '../api'; +import { testApiStatus, getStatics, getHotTopics } from '../api'; +import type { StaticsData, HotTopicItem } from '../api'; const useStyles = makeStyles({ container: { @@ -71,31 +72,55 @@ const useStyles = makeStyles({ headerText: { fontSize: tokens.fontSizeBase300, fontWeight: tokens.fontWeightSemibold, - } + }, + topicList: { + padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`, + display: 'flex', + flexDirection: 'column', + gap: tokens.spacingVerticalS, + }, + topicRow: { + display: 'flex', + flexDirection: 'column', + gap: tokens.spacingVerticalXXS, + padding: `${tokens.spacingVerticalS} 0`, + }, + topicName: { + fontSize: tokens.fontSizeBase300, + color: tokens.colorNeutralForeground1, + }, + topicCount: { + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground3, + }, }); -const StatusDisplay: React.FC = () => { +const Widgets: React.FC = () => { const styles = useStyles(); const { toasterId, refreshTrigger, staticsRefreshTrigger } = useLayout(); const { dispatchToast } = useToastController(toasterId); const [isApiOnline, setIsApiOnline] = useState(null); const [statics, setStatics] = useState(null); + const [topics, setTopics] = useState([]); const [isTestLoading, setIsTestLoading] = useState(true); const [isStaticsLoading, setIsStaticsLoading] = useState(true); + const [isTopicsLoading, setIsTopicsLoading] = useState(true); const checkStatus = useCallback(async () => { const online = await testApiStatus(); setIsApiOnline(online); setIsTestLoading(false); }, []); - const refreshStatics = useCallback(async (isManual: boolean = false) => { + const refreshWidgets = useCallback(async (isManual: boolean = false) => { setIsStaticsLoading(true); + setIsTopicsLoading(true); try { - const data = await getStatics(); - setStatics(data); + const [staticsData, topicsData] = await Promise.all([getStatics(), getHotTopics()]); + setStatics(staticsData); + setTopics(topicsData); if (isManual) { dispatchToast( - 统计数据已刷新 + 小组件数据已刷新 , { intent: 'success' } ); @@ -103,15 +128,17 @@ const StatusDisplay: React.FC = () => { } catch (error) { console.error('Failed to refresh statics:', error); setStatics(null); // 失败时重置数据 + setTopics([]); } finally { setIsStaticsLoading(false); + setIsTopicsLoading(false); } }, [dispatchToast]); useEffect(() => { // 初始加载 checkStatus(); - refreshStatics(false); + refreshWidgets(false); // 每 10 秒测试一次后端 API 状态 const testInterval = setInterval(checkStatus, 10000); @@ -119,20 +146,20 @@ const StatusDisplay: React.FC = () => { return () => { clearInterval(testInterval); }; - }, [checkStatus, refreshStatics]); + }, [checkStatus, refreshWidgets]); // Listen for global refresh trigger useEffect(() => { if (refreshTrigger > 0) { - refreshStatics(false); + refreshWidgets(false); } - }, [refreshTrigger, refreshStatics]); + }, [refreshTrigger, refreshWidgets]); useEffect(() => { if (staticsRefreshTrigger > 0) { - refreshStatics(false); + refreshWidgets(false); } - }, [staticsRefreshTrigger, refreshStatics]); + }, [staticsRefreshTrigger, refreshWidgets]); return (
@@ -164,16 +191,6 @@ const StatusDisplay: React.FC = () => { 统计数据} - action={ -
); }; -export default StatusDisplay; +export default Widgets; diff --git a/front/src/layouts/MainLayout.tsx b/front/src/layouts/MainLayout.tsx index 94ac58e..41f0cf8 100644 --- a/front/src/layouts/MainLayout.tsx +++ b/front/src/layouts/MainLayout.tsx @@ -4,7 +4,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 Widgets from '../components/Widgets'; import { LayoutProvider, useLayout } from '../context/LayoutContext'; const useStyles = makeStyles({ @@ -66,7 +66,7 @@ const LayoutContent = ({ toasterId, overlays }: { toasterId: string; overlays?: