feat: Add console announcements and FAQ management system

- Add SettingsAnnouncements component with full CRUD operations for system announcements
  * Support multiple announcement types (default, ongoing, success, warning, error)
  * Include publish date, content, type classification and additional notes
  * Implement batch operations and pagination for better data management
  * Add real-time preview with relative time display and date formatting

- Add SettingsFAQ component for comprehensive FAQ management
  * Support question-answer pairs with rich text content
  * Include full editing, deletion and creation capabilities
  * Implement batch delete operations and paginated display
  * Add validation for complete Q&A information

- Integrate announcement and FAQ modules into DashboardSetting
  * Add unified configuration interface in admin console
  * Implement auto-refresh functionality for real-time updates
  * Add loading states and error handling for better UX

- Enhance backend API support in controller and setting modules
  * Add validation functions for console settings
  * Include time and sorting utilities for announcement management
  * Extend API endpoints for announcement and FAQ data persistence

- Improve frontend infrastructure
  * Add new translation keys for internationalization support
  * Update utility functions for date/time formatting
  * Enhance CSS styles for better component presentation
  * Add icons and visual improvements for announcements and FAQ sections

This implementation provides administrators with comprehensive tools to manage
system-wide announcements and user FAQ content through an intuitive console interface.
This commit is contained in:
Apple\Apple
2025-06-10 20:10:07 +08:00
parent 56188c33b5
commit 26b70d6a25
11 changed files with 1487 additions and 36 deletions

View File

@@ -1,7 +1,7 @@
import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { useNavigate } from 'react-router-dom';
import { Wallet, Activity, Zap, Gauge, PieChart, Server } from 'lucide-react';
import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react';
import {
Card,
@@ -13,7 +13,9 @@ import {
Tabs,
TabPane,
Empty,
Tag
Tag,
Timeline,
Collapse
} from '@douyinfe/semi-ui';
import {
IconRefresh,
@@ -26,7 +28,9 @@ import {
IconPulse,
IconStopwatchStroked,
IconTypograph,
IconPieChart2Stroked
IconPieChart2Stroked,
IconPlus,
IconMinus
} from '@douyinfe/semi-icons';
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
import { VChart } from '@visactor/react-vchart';
@@ -43,7 +47,8 @@ import {
renderQuota,
modelToColor,
copy,
showSuccess
showSuccess,
getRelativeTime
} from '../../helpers';
import { UserContext } from '../../context/User/index.js';
import { StatusContext } from '../../context/Status/index.js';
@@ -179,7 +184,7 @@ const Detail = (props) => {
const [times, setTimes] = useState(0);
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
const [lineData, setLineData] = useState([]);
const [apiInfoData, setApiInfoData] = useState([]);
const [modelColors, setModelColors] = useState({});
const [activeChartTab, setActiveChartTab] = useState('1');
const [showApiScrollHint, setShowApiScrollHint] = useState(false);
@@ -578,6 +583,37 @@ const Detail = (props) => {
checkApiScrollable();
};
const checkCardScrollable = (ref, setHintFunction) => {
if (ref.current) {
const element = ref.current;
const isScrollable = element.scrollHeight > element.clientHeight;
const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - 5;
setHintFunction(isScrollable && !isAtBottom);
}
};
const handleCardScroll = (ref, setHintFunction) => {
checkCardScrollable(ref, setHintFunction);
};
// ========== Additional Refs for new cards ==========
const announcementScrollRef = useRef(null);
const faqScrollRef = useRef(null);
// ========== Additional State for scroll hints ==========
const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false);
const [showFaqScrollHint, setShowFaqScrollHint] = useState(false);
// ========== Effects for scroll detection ==========
useEffect(() => {
const timer = setTimeout(() => {
checkApiScrollable();
checkCardScrollable(announcementScrollRef, setShowAnnouncementScrollHint);
checkCardScrollable(faqScrollRef, setShowFaqScrollHint);
}, 100);
return () => clearTimeout(timer);
}, []);
const getUserData = async () => {
let res = await API.get(`/api/user/self`);
const { success, message, data } = res.data;
@@ -775,6 +811,32 @@ const Detail = (props) => {
generateChartTimePoints, updateChartSpec, updateMapValue, t
]);
// ========== Status Data Management ==========
const announcementLegendData = useMemo(() => [
{ color: 'grey', label: t('默认'), type: 'default' },
{ color: 'blue', label: t('进行中'), type: 'ongoing' },
{ color: 'green', label: t('成功'), type: 'success' },
{ color: 'orange', label: t('警告'), type: 'warning' },
{ color: 'red', label: t('异常'), type: 'error' }
], [t]);
const apiInfoData = useMemo(() => {
return statusState?.status?.api_info || [];
}, [statusState?.status?.api_info]);
const announcementData = useMemo(() => {
const announcements = statusState?.status?.announcements || [];
// 处理后台配置的公告数据,自动生成相对时间
return announcements.map(item => ({
...item,
time: getRelativeTime(item.publishDate)
}));
}, [statusState?.status?.announcements]);
const faqData = useMemo(() => {
return statusState?.status?.faq || [];
}, [statusState?.status?.faq]);
// ========== Hooks - Effects ==========
useEffect(() => {
getUserData();
@@ -787,19 +849,6 @@ const Detail = (props) => {
}
}, []);
useEffect(() => {
if (statusState?.status?.api_info) {
setApiInfoData(statusState.status.api_info);
}
}, [statusState?.status?.api_info]);
useEffect(() => {
const timer = setTimeout(() => {
checkApiScrollable();
}, 100);
return () => clearTimeout(timer);
}, []);
return (
<div className="bg-gray-50 h-full">
<div className="flex items-center justify-between mb-4">
@@ -975,10 +1024,10 @@ const Detail = (props) => {
</div>
}
>
<div className="api-info-container">
<div className="card-content-container">
<div
ref={apiScrollRef}
className="space-y-3 max-h-96 overflow-y-auto api-info-scroll"
className="space-y-3 max-h-96 overflow-y-auto card-content-scroll"
onScroll={handleApiScroll}
>
{apiInfoData.length > 0 ? (
@@ -1023,7 +1072,7 @@ const Detail = (props) => {
<Empty
image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
title={t('暂无API信息配置')}
title={t('暂无API信息')}
description={t('请联系管理员在系统设置中配置API信息')}
style={{ padding: '12px' }}
/>
@@ -1031,7 +1080,7 @@ const Detail = (props) => {
)}
</div>
<div
className="api-info-fade-indicator"
className="card-content-fade-indicator"
style={{ opacity: showApiScrollHint ? 1 : 0 }}
/>
</div>
@@ -1039,6 +1088,129 @@ const Detail = (props) => {
)}
</div>
</div>
{/* 系统公告和常见问答卡片 */}
{!statusState?.status?.self_use_mode_enabled && (
<div className="mb-4">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
{/* 公告卡片 */}
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-2"
title={
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
<div className="flex items-center gap-2">
<Bell size={16} />
{t('系统公告')}
<Tag size="small" color="grey" shape="circle">
{t('显示最新20条')}
</Tag>
</div>
{/* 图例 */}
<div className="flex flex-wrap gap-3 text-xs">
{announcementLegendData.map((legend, index) => (
<div key={index} className="flex items-center gap-1">
<div
className="w-2 h-2 rounded-full"
style={{
backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
legend.color === 'blue' ? '#3b82f6' :
legend.color === 'green' ? '#10b981' :
legend.color === 'orange' ? '#f59e0b' :
legend.color === 'red' ? '#ef4444' : '#8b9aa7'
}}
/>
<span className="text-gray-600">{legend.label}</span>
</div>
))}
</div>
</div>
}
>
<div className="card-content-container">
<div
ref={announcementScrollRef}
className="p-2 max-h-96 overflow-y-auto card-content-scroll"
onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)}
>
{announcementData.length > 0 ? (
<Timeline
mode="alternate"
dataSource={announcementData}
/>
) : (
<div className="flex justify-center items-center py-8">
<Empty
image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
title={t('暂无系统公告')}
description={t('请联系管理员在系统设置中配置公告信息')}
style={{ padding: '12px' }}
/>
</div>
)}
</div>
<div
className="card-content-fade-indicator"
style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
/>
</div>
</Card>
{/* 常见问答卡片 */}
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-2"
title={
<div className={FLEX_CENTER_GAP2}>
<HelpCircle size={16} />
{t('常见问答')}
</div>
}
>
<div className="card-content-container">
<div
ref={faqScrollRef}
className="p-2 max-h-96 overflow-y-auto card-content-scroll"
onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
>
{faqData.length > 0 ? (
<Collapse
accordion
expandIcon={<IconPlus />}
collapseIcon={<IconMinus />}
>
{faqData.map((item, index) => (
<Collapse.Panel
key={index}
header={item.title}
itemKey={index.toString()}
>
<p>{item.content}</p>
</Collapse.Panel>
))}
</Collapse>
) : (
<div className="flex justify-center items-center py-8">
<Empty
image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
title={t('暂无常见问答')}
description={t('请联系管理员在系统设置中配置常见问答')}
style={{ padding: '12px' }}
/>
</div>
)}
</div>
<div
className="card-content-fade-indicator"
style={{ opacity: showFaqScrollHint ? 1 : 0 }}
/>
</div>
</Card>
</div>
</div>
)}
</Spin>
</div>
);