2026-07-31 15:17:21 +08:00
|
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
2026-07-09 10:05:52 +08:00
|
|
|
|
import DriverBack from '~/pages/Container/Driver/components/DriverBack';
|
|
|
|
|
|
import EnterpriseTypeChart from './components/EnterpriseTypeChart';
|
|
|
|
|
|
import ProjectTypeChart from './components/ProjectTypeChart';
|
|
|
|
|
|
import ProjectCompletionTable from './components/ProjectCompletionTable';
|
|
|
|
|
|
import './index.css';
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
STATISTIC_CARDS,
|
|
|
|
|
|
INFO_ALERTS,
|
|
|
|
|
|
PROJECT_SUMMARY,
|
|
|
|
|
|
ENTERPRISE_TYPE_STATS,
|
|
|
|
|
|
PROJECT_TYPE_DISTRIBUTION,
|
|
|
|
|
|
PROJECT_TOTAL,
|
2026-07-31 15:17:21 +08:00
|
|
|
|
PROJECT_COMPLETION_LIST,
|
2026-07-09 10:05:52 +08:00
|
|
|
|
} from './mockData';
|
|
|
|
|
|
|
2026-07-31 15:17:21 +08:00
|
|
|
|
import {
|
|
|
|
|
|
fetchProjectNodeStats,
|
|
|
|
|
|
fetchNotices,
|
|
|
|
|
|
fetchIndustryStat,
|
|
|
|
|
|
fetchEvalTypeRatio,
|
|
|
|
|
|
fetchProjectExecution,
|
|
|
|
|
|
} from '~/api/institution';
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== UI 展示常量(与原型保持一致,非数据映射) ====================
|
|
|
|
|
|
const NODE_CHARS = ['险', '合', '组', '场', '报', '内', '技', '控'];
|
|
|
|
|
|
const NODE_COLORS = ['blue', 'violet', 'green', 'orange', 'blue', 'violet', 'orange', 'green'];
|
|
|
|
|
|
|
|
|
|
|
|
function mockIndustryToApiFormat() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
list: ENTERPRISE_TYPE_STATS.categories.map((name, i) => ({
|
|
|
|
|
|
industryCode: '',
|
|
|
|
|
|
industryName: name,
|
|
|
|
|
|
projectCount: ENTERPRISE_TYPE_STATS.projectCount[i],
|
|
|
|
|
|
statutoryProjectCount: ENTERPRISE_TYPE_STATS.statutoryCount[i],
|
|
|
|
|
|
})),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mockEvalTypeToApiFormat() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
totalProjectCount: PROJECT_TOTAL,
|
|
|
|
|
|
items: PROJECT_TYPE_DISTRIBUTION.map(item => ({
|
|
|
|
|
|
evalTypeCode: '',
|
|
|
|
|
|
evalTypeName: item.name,
|
|
|
|
|
|
count: item.value,
|
|
|
|
|
|
})),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mockExecutionToApiFormat() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
list: PROJECT_COMPLETION_LIST.map(item => ({
|
|
|
|
|
|
projectId: item.id,
|
|
|
|
|
|
projectNo: item.projectCode,
|
|
|
|
|
|
projectName: item.projectName,
|
|
|
|
|
|
customerName: item.evalCompany,
|
|
|
|
|
|
evalTypeCode: '',
|
|
|
|
|
|
evalTypeName: item.evalType,
|
|
|
|
|
|
projectLeaderName: item.projectLeader,
|
|
|
|
|
|
planEndDate: item.endDate,
|
|
|
|
|
|
})),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 子组件 ====================
|
|
|
|
|
|
function StatusCard({ item }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<article>
|
|
|
|
|
|
<i className={item.colorClass}>{item.char}</i>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>{item.title}</span>
|
|
|
|
|
|
<strong>{item.value}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ReminderCard({ item }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<article className={item.colorClass}>
|
|
|
|
|
|
<i>{item.char}</i>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>{item.title}</span>
|
|
|
|
|
|
<strong>{item.value}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 主组件 ====================
|
2026-07-09 10:05:52 +08:00
|
|
|
|
export default function InstitutionDashboard() {
|
2026-07-31 15:17:21 +08:00
|
|
|
|
const handleReminderClick = useCallback(() => {}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 各 API 的原始返回数据(null 表示接口未成功,走 mock)
|
|
|
|
|
|
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(() => {
|
|
|
|
|
|
fetchProjectNodeStats()
|
|
|
|
|
|
.then(setNodeStatsApi)
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
|
|
|
|
|
|
fetchNotices()
|
|
|
|
|
|
.then(setNoticesApi)
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
|
|
|
|
|
|
fetchIndustryStat()
|
|
|
|
|
|
.then(setIndustryStatApi)
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
|
|
|
|
|
|
fetchEvalTypeRatio()
|
|
|
|
|
|
.then(setEvalTypeRatioApi)
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
|
|
|
|
|
|
fetchProjectExecution()
|
|
|
|
|
|
.then(setProjectExecutionApi)
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 衍生展示数据:API 成功则用 API 字段直接构建,失败则回退 mock
|
|
|
|
|
|
const statusCards = useMemo(() => {
|
|
|
|
|
|
if (!nodeStatsApi) return STATISTIC_CARDS;
|
|
|
|
|
|
const { nodeStats, archivedProjectCount, delayedProjectCount } = nodeStatsApi;
|
|
|
|
|
|
const nodes = (nodeStats || []).map((node, i) => ({
|
|
|
|
|
|
key: node.nodeCode || `NODE_${i}`,
|
|
|
|
|
|
title: node.nodeName,
|
|
|
|
|
|
value: Number(node.pendingCount) || 0,
|
|
|
|
|
|
char: NODE_CHARS[i] || node.nodeName.charAt(1) || '-',
|
|
|
|
|
|
colorClass: NODE_COLORS[i] || 'blue',
|
|
|
|
|
|
}));
|
|
|
|
|
|
nodes.push(
|
|
|
|
|
|
{ key: 'archived', title: '已归档项目', value: Number(archivedProjectCount) || 0, char: '档', colorClass: 'cyan' },
|
|
|
|
|
|
{ key: 'delayed', title: '项目延期', value: Number(delayedProjectCount) || 0, char: '延', colorClass: 'red' },
|
|
|
|
|
|
);
|
|
|
|
|
|
return nodes;
|
|
|
|
|
|
}, [nodeStatsApi]);
|
|
|
|
|
|
|
|
|
|
|
|
const projectSummary = useMemo(() => {
|
|
|
|
|
|
if (!nodeStatsApi) return PROJECT_SUMMARY;
|
|
|
|
|
|
return {
|
|
|
|
|
|
total: Number(nodeStatsApi.totalProjects) || 0,
|
|
|
|
|
|
statutory: Number(nodeStatsApi.statutoryProjects) || 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [nodeStatsApi]);
|
|
|
|
|
|
|
|
|
|
|
|
const alertItems = useMemo(() => {
|
|
|
|
|
|
if (!noticesApi) return INFO_ALERTS;
|
|
|
|
|
|
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]);
|
|
|
|
|
|
|
|
|
|
|
|
const industryStatData = useMemo(() => {
|
|
|
|
|
|
return (industryStatApi && industryStatApi.list && industryStatApi.list.length > 0)
|
|
|
|
|
|
? industryStatApi
|
|
|
|
|
|
: mockIndustryToApiFormat();
|
|
|
|
|
|
}, [industryStatApi]);
|
|
|
|
|
|
|
|
|
|
|
|
const evalTypeRatioData = useMemo(() => {
|
|
|
|
|
|
return (evalTypeRatioApi && evalTypeRatioApi.items && evalTypeRatioApi.items.length > 0)
|
|
|
|
|
|
? evalTypeRatioApi
|
|
|
|
|
|
: mockEvalTypeToApiFormat();
|
|
|
|
|
|
}, [evalTypeRatioApi]);
|
|
|
|
|
|
|
|
|
|
|
|
const projectExecutionData = useMemo(() => {
|
|
|
|
|
|
return (projectExecutionApi && projectExecutionApi.list && projectExecutionApi.list.length > 0)
|
|
|
|
|
|
? projectExecutionApi
|
|
|
|
|
|
: mockExecutionToApiFormat();
|
|
|
|
|
|
}, [projectExecutionApi]);
|
|
|
|
|
|
|
|
|
|
|
|
const legendColors = useMemo(() => ({
|
|
|
|
|
|
project: '#4285f4',
|
|
|
|
|
|
statutory: '#ffab31',
|
|
|
|
|
|
}), []);
|
|
|
|
|
|
|
2026-07-09 10:05:52 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="institution-dashboard">
|
|
|
|
|
|
<div className="institution-dashboard__toolbar">
|
|
|
|
|
|
<DriverBack />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-31 15:17:21 +08:00
|
|
|
|
<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>项目总数:<b>{projectSummary.total}</b></span>
|
|
|
|
|
|
<span>法定项目:<b>{projectSummary.statutory}</b></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="dashboard-status-grid">
|
|
|
|
|
|
{statusCards.map(item => (
|
|
|
|
|
|
<StatusCard key={item.key} item={item} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</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} 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>
|
|
|
|
|
|
<EnterpriseTypeChart data={industryStatData} />
|
|
|
|
|
|
</section>
|
2026-07-09 10:05:52 +08:00
|
|
|
|
|
2026-07-31 15:17:21 +08:00
|
|
|
|
<section className="dashboard-block dashboard-type-block">
|
|
|
|
|
|
<div className="dashboard-block-head">
|
|
|
|
|
|
<h3>评价类别占比</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ProjectTypeChart data={evalTypeRatioData} />
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ===== 第三行:项目执行情况 ===== */}
|
|
|
|
|
|
<ProjectCompletionTable
|
|
|
|
|
|
data={projectExecutionData}
|
|
|
|
|
|
onView={() => {}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-07-09 10:05:52 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|