209 lines
7.9 KiB
JavaScript
209 lines
7.9 KiB
JavaScript
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>
|
||
);
|
||
}
|