safety-eval-website/src/pages/Container/Institution/index.js

209 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import DriverBack from '~/pages/Container/Driver/components/DriverBack';
import { tools } from "@cqsjjb/jjb-common-lib";
import EnterpriseTypeChart from './components/EnterpriseTypeChart';
import ProjectTypeChart from './components/ProjectTypeChart';
import ProjectCompletionTable from './components/ProjectCompletionTable';
import './index.css';
import {
STATISTIC_CARDS,
} from './mockData';
import {
fetchProjectNodeStats,
fetchNotices,
fetchIndustryStat,
fetchEvalTypeRatio,
fetchProjectExecution,
} from '~/api/institution';
// ==================== 骨架占位 ====================
function Skeleton({ width = '100%', height = 14, radius = 4, className = '' }) {
return (
<span
className={`sk-block ${className}`}
style={{ width, height, borderRadius: radius }}
aria-hidden="true"
/>
);
}
// ==================== 子组件 ====================
function StatusCard({ item, loading }) {
return (
<article>
<i className={item.colorClass}>{item.char}</i>
<div>
<span>{item.title}</span>
{loading ? <Skeleton width={42} height={16} /> : <strong>{item.value}</strong>}
</div>
</article>
);
}
function ReminderCard({ item, loading }) {
return (
<article className={item.colorClass}>
<i>{item.char}</i>
<div>
<span>{item.title}</span>
{loading ? <Skeleton width={42} height={16} /> : <strong>{item.value}</strong>}
</div>
</article>
);
}
// ==================== 主组件 ====================
export default function InstitutionDashboard() {
const handleReminderClick = useCallback(() => {}, []);
// 各 API 的原始返回数据null 表示接口未成功,走 mock
const [loading, setLoading] = useState(true);
const [nodeStatsApi, setNodeStatsApi] = useState(null);
const [noticesApi, setNoticesApi] = useState(null);
const [industryStatApi, setIndustryStatApi] = useState(null);
const [evalTypeRatioApi, setEvalTypeRatioApi] = useState(null);
const [projectExecutionApi, setProjectExecutionApi] = useState(null);
useEffect(() => {
const tasks = [
fetchProjectNodeStats().then(setNodeStatsApi).catch(() => {}),
fetchNotices().then(setNoticesApi).catch(() => {}),
fetchIndustryStat().then(setIndustryStatApi).catch(() => {}),
fetchEvalTypeRatio().then(setEvalTypeRatioApi).catch(() => {}),
fetchProjectExecution().then(setProjectExecutionApi).catch(() => {}),
];
Promise.all(tasks).finally(() => setLoading(false));
}, []);
// 衍生展示数据始终渲染固定卡片集合8 个节点 + 已归档 + 项目延期),
// 仅用 API 数据覆盖对应数值,保证无论接口返回多少节点,卡片项数都不减少。
const statusCards = useMemo(() => {
if (!nodeStatsApi) return STATISTIC_CARDS;
const { nodeStats = [], archivedProjectCount = 0, delayedProjectCount = 0 } = nodeStatsApi || {};
const nodeByName = new Map();
(nodeStats || []).forEach((n) => {
if (n && n.nodeName) nodeByName.set(n.nodeName, n);
});
return STATISTIC_CARDS.map((card) => {
let value = card.value;
if (card.key === 'archived') {
value = Number(archivedProjectCount) || 0;
} else if (card.key === 'delayed') {
value = Number(delayedProjectCount) || 0;
} else {
const node = nodeByName.get(card.title);
if (node) value = Number(node.pendingCount) || 0;
}
return { ...card, value };
});
}, [nodeStatsApi]);
const projectSummary = useMemo(() => {
if (!nodeStatsApi) return { total: 0, statutory: 0 };
return {
total: Number(nodeStatsApi.totalProjects) || 0,
statutory: Number(nodeStatsApi.statutoryProjects) || 0,
};
}, [nodeStatsApi]);
const alertItems = useMemo(() => {
if (!noticesApi) {
return [
{ key: 'qualCheck', title: '资质现场审查通知', value: 0, char: '审', colorClass: 'yellow' },
{ key: 'supervisionCheck', title: '监督检查通知', value: 0, char: '检', colorClass: 'red' },
{ key: 'unreadNotice', title: '未读监管通知', value: 0, char: '未', colorClass: 'blue' },
];
}
return [
{ key: 'qualCheck', title: '资质现场审查通知', value: Number(noticesApi.qualOnSiteReviewCount) || 0, char: '审', colorClass: 'yellow' },
{ key: 'supervisionCheck', title: '监督检查通知', value: Number(noticesApi.inspectionCount) || 0, char: '检', colorClass: 'red' },
{ key: 'unreadNotice', title: '未读监管通知', value: Number(noticesApi.unreadRegulatoryCount) || 0, char: '未', colorClass: 'blue' },
];
}, [noticesApi]);
// 接口无数据时使用空结构,不再用 mock 伪造数据
const industryStatData = useMemo(() => industryStatApi || { list: [] }, [industryStatApi]);
const evalTypeRatioData = useMemo(() => evalTypeRatioApi || { totalProjectCount: 0, items: [] }, [evalTypeRatioApi]);
const projectExecutionData = useMemo(() => projectExecutionApi || { list: [] }, [projectExecutionApi]);
const legendColors = useMemo(() => ({
project: '#4285f4',
statutory: '#ffab31',
}), []);
return (
<div className="institution-dashboard">
<div className="institution-dashboard__toolbar">
{!tools.inIframe() && <DriverBack />}
</div>
<div className="dashboard-classic">
{/* ===== 第一行:项目节点统计 + 通知提醒 ===== */}
<div className="dashboard-classic-top">
<section className="dashboard-block dashboard-status-block">
<div className="dashboard-block-head">
<h3>当前项目节点统计</h3>
<div>
<span>项目总数{loading ? <Skeleton width={28} height={14} /> : <b>{projectSummary.total}</b>}</span>
<span>法定项目{loading ? <Skeleton width={28} height={14} /> : <b>{projectSummary.statutory}</b>}</span>
</div>
</div>
<div className="dashboard-status-grid">
{statusCards.map(item => (
<StatusCard key={item.key} item={item} loading={loading} />
))}
</div>
</section>
<section className="dashboard-block dashboard-reminder-block">
<div className="dashboard-block-head">
<h3>通知提醒</h3>
</div>
<div className="dashboard-reminder-grid">
{alertItems.map(item => (
<ReminderCard key={item.key} item={item} loading={loading} onClick={handleReminderClick} />
))}
</div>
</section>
</div>
{/* ===== 第二行:服务行业项目统计 + 评价类别占比 ===== */}
<div className="dashboard-classic-middle">
<section className="dashboard-block dashboard-industry-block">
<div className="dashboard-block-head">
<h3>服务行业项目统计</h3>
<div className="chart-legend">
<span><i style={{ backgroundColor: legendColors.project }} />项目数</span>
<span><i style={{ backgroundColor: legendColors.statutory }} />法定项目</span>
</div>
</div>
{loading
? <div className="sk-chart" />
: <EnterpriseTypeChart data={industryStatData} />}
</section>
<section className="dashboard-block dashboard-type-block">
<div className="dashboard-block-head">
<h3>评价类别占比</h3>
</div>
{loading
? <div className="sk-chart sk-chart--donut" />
: <ProjectTypeChart data={evalTypeRatioData} />}
</section>
</div>
{/* ===== 第三行:项目执行情况 ===== */}
<ProjectCompletionTable
data={projectExecutionData}
loading={loading}
onView={() => {}}
/>
</div>
</div>
);
}