✨ 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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user