机构端、监管端首页修改
parent
de49125981
commit
553a2a75a7
|
|
@ -1,8 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function ProjectCompletionTable({ data, onView }) {
|
||||
export default function ProjectCompletionTable({ data, loading, onView }) {
|
||||
const list = data && data.list ? data.list : [];
|
||||
|
||||
// 加载占位:固定行数的骨架,保持表格布局
|
||||
const skeletonRows = Array.from({ length: 5 });
|
||||
|
||||
return (
|
||||
<section className="dashboard-block dashboard-project-block">
|
||||
<div className="dashboard-block-head">
|
||||
|
|
@ -26,26 +29,39 @@ export default function ProjectCompletionTable({ data, onView }) {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{list.map((record, index) => (
|
||||
<tr key={record.projectId || index}>
|
||||
<td>{index + 1}</td>
|
||||
<td>{record.projectNo}</td>
|
||||
<td>{record.projectName}</td>
|
||||
<td>{record.customerName}</td>
|
||||
<td>{record.evalTypeName}</td>
|
||||
<td>{record.projectLeaderName}</td>
|
||||
<td>{record.planEndDate}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
type="button"
|
||||
onClick={() => onView && onView(record)}
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{loading
|
||||
? skeletonRows.map((_, index) => (
|
||||
<tr key={`sk-${index}`}>
|
||||
<td><span className="sk-block sk-cell" /></td>
|
||||
<td><span className="sk-block sk-cell" /></td>
|
||||
<td><span className="sk-block sk-cell sk-cell--lg" /></td>
|
||||
<td><span className="sk-block sk-cell" /></td>
|
||||
<td><span className="sk-block sk-cell" /></td>
|
||||
<td><span className="sk-block sk-cell" /></td>
|
||||
<td><span className="sk-block sk-cell" /></td>
|
||||
<td><span className="sk-block sk-cell sk-cell--sm" /></td>
|
||||
</tr>
|
||||
))
|
||||
: list.map((record, index) => (
|
||||
<tr key={record.projectId || index}>
|
||||
<td>{index + 1}</td>
|
||||
<td>{record.projectNo}</td>
|
||||
<td>{record.projectName}</td>
|
||||
<td>{record.customerName}</td>
|
||||
<td>{record.evalTypeName}</td>
|
||||
<td>{record.projectLeaderName}</td>
|
||||
<td>{record.planEndDate}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
type="button"
|
||||
onClick={() => onView && onView(record)}
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -357,6 +357,40 @@
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
/* ============ 骨架占位(加载态) ============ */
|
||||
.sk-block {
|
||||
display: inline-block;
|
||||
background: linear-gradient(90deg, #eef2f7 25%, #e2e8f0 37%, #eef2f7 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: sk-shimmer 1.3s ease-in-out infinite;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes sk-shimmer {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: 0 0; }
|
||||
}
|
||||
|
||||
/* 图表占位块 */
|
||||
.sk-chart {
|
||||
width: 100%;
|
||||
height: 225px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(90deg, #eef2f7 25%, #e2e8f0 37%, #eef2f7 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: sk-shimmer 1.3s ease-in-out infinite;
|
||||
}
|
||||
.sk-chart--donut { height: 185px; }
|
||||
|
||||
/* 表格单元格骨架 */
|
||||
.sk-cell {
|
||||
width: 70%;
|
||||
height: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.sk-cell--lg { width: 85%; }
|
||||
.sk-cell--sm { width: 36px; }
|
||||
|
||||
/* ============ 响应式 ============ */
|
||||
@media (max-width: 1280px) {
|
||||
.dashboard-classic-top,
|
||||
|
|
|
|||
|
|
@ -7,12 +7,6 @@ import './index.css';
|
|||
|
||||
import {
|
||||
STATISTIC_CARDS,
|
||||
INFO_ALERTS,
|
||||
PROJECT_SUMMARY,
|
||||
ENTERPRISE_TYPE_STATS,
|
||||
PROJECT_TYPE_DISTRIBUTION,
|
||||
PROJECT_TOTAL,
|
||||
PROJECT_COMPLETION_LIST,
|
||||
} from './mockData';
|
||||
|
||||
import {
|
||||
|
|
@ -23,65 +17,37 @@ import {
|
|||
fetchProjectExecution,
|
||||
} from '~/api/institution';
|
||||
|
||||
// ==================== UI 展示常量(与原型保持一致,非数据映射) ====================
|
||||
|
||||
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 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 }) {
|
||||
function StatusCard({ item, loading }) {
|
||||
return (
|
||||
<article>
|
||||
<i className={item.colorClass}>{item.char}</i>
|
||||
<div>
|
||||
<span>{item.title}</span>
|
||||
<strong>{item.value}</strong>
|
||||
{loading ? <Skeleton width={42} height={16} /> : <strong>{item.value}</strong>}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function ReminderCard({ item }) {
|
||||
function ReminderCard({ item, loading }) {
|
||||
return (
|
||||
<article className={item.colorClass}>
|
||||
<i>{item.char}</i>
|
||||
<div>
|
||||
<span>{item.title}</span>
|
||||
<strong>{item.value}</strong>
|
||||
{loading ? <Skeleton width={42} height={16} /> : <strong>{item.value}</strong>}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
|
@ -92,6 +58,7 @@ 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);
|
||||
|
|
@ -99,25 +66,14 @@ export default function InstitutionDashboard() {
|
|||
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(() => {});
|
||||
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 个节点 + 已归档 + 项目延期),
|
||||
|
|
@ -144,7 +100,7 @@ export default function InstitutionDashboard() {
|
|||
}, [nodeStatsApi]);
|
||||
|
||||
const projectSummary = useMemo(() => {
|
||||
if (!nodeStatsApi) return PROJECT_SUMMARY;
|
||||
if (!nodeStatsApi) return { total: 0, statutory: 0 };
|
||||
return {
|
||||
total: Number(nodeStatsApi.totalProjects) || 0,
|
||||
statutory: Number(nodeStatsApi.statutoryProjects) || 0,
|
||||
|
|
@ -152,7 +108,13 @@ export default function InstitutionDashboard() {
|
|||
}, [nodeStatsApi]);
|
||||
|
||||
const alertItems = useMemo(() => {
|
||||
if (!noticesApi) return INFO_ALERTS;
|
||||
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' },
|
||||
|
|
@ -160,23 +122,12 @@ export default function InstitutionDashboard() {
|
|||
];
|
||||
}, [noticesApi]);
|
||||
|
||||
const industryStatData = useMemo(() => {
|
||||
return (industryStatApi && industryStatApi.list && industryStatApi.list.length > 0)
|
||||
? industryStatApi
|
||||
: mockIndustryToApiFormat();
|
||||
}, [industryStatApi]);
|
||||
// 接口无数据时使用空结构,不再用 mock 伪造数据
|
||||
const industryStatData = useMemo(() => industryStatApi || { list: [] }, [industryStatApi]);
|
||||
|
||||
const evalTypeRatioData = useMemo(() => {
|
||||
return (evalTypeRatioApi && evalTypeRatioApi.items && evalTypeRatioApi.items.length > 0)
|
||||
? evalTypeRatioApi
|
||||
: mockEvalTypeToApiFormat();
|
||||
}, [evalTypeRatioApi]);
|
||||
const evalTypeRatioData = useMemo(() => evalTypeRatioApi || { totalProjectCount: 0, items: [] }, [evalTypeRatioApi]);
|
||||
|
||||
const projectExecutionData = useMemo(() => {
|
||||
return (projectExecutionApi && projectExecutionApi.list && projectExecutionApi.list.length > 0)
|
||||
? projectExecutionApi
|
||||
: mockExecutionToApiFormat();
|
||||
}, [projectExecutionApi]);
|
||||
const projectExecutionData = useMemo(() => projectExecutionApi || { list: [] }, [projectExecutionApi]);
|
||||
|
||||
const legendColors = useMemo(() => ({
|
||||
project: '#4285f4',
|
||||
|
|
@ -189,20 +140,20 @@ export default function InstitutionDashboard() {
|
|||
<DriverBack />
|
||||
</div>
|
||||
|
||||
<div className="dashboard-classic">
|
||||
<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>
|
||||
<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} />
|
||||
<StatusCard key={item.key} item={item} loading={loading} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -213,7 +164,7 @@ export default function InstitutionDashboard() {
|
|||
</div>
|
||||
<div className="dashboard-reminder-grid">
|
||||
{alertItems.map(item => (
|
||||
<ReminderCard key={item.key} item={item} onClick={handleReminderClick} />
|
||||
<ReminderCard key={item.key} item={item} loading={loading} onClick={handleReminderClick} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -229,20 +180,25 @@ export default function InstitutionDashboard() {
|
|||
<span><i style={{ backgroundColor: legendColors.statutory }} />法定项目</span>
|
||||
</div>
|
||||
</div>
|
||||
<EnterpriseTypeChart data={industryStatData} />
|
||||
{loading
|
||||
? <div className="sk-chart" />
|
||||
: <EnterpriseTypeChart data={industryStatData} />}
|
||||
</section>
|
||||
|
||||
<section className="dashboard-block dashboard-type-block">
|
||||
<div className="dashboard-block-head">
|
||||
<h3>评价类别占比</h3>
|
||||
</div>
|
||||
<ProjectTypeChart data={evalTypeRatioData} />
|
||||
{loading
|
||||
? <div className="sk-chart sk-chart--donut" />
|
||||
: <ProjectTypeChart data={evalTypeRatioData} />}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* ===== 第三行:项目执行情况 ===== */}
|
||||
<ProjectCompletionTable
|
||||
data={projectExecutionData}
|
||||
loading={loading}
|
||||
onView={() => {}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,79 +1,17 @@
|
|||
/**
|
||||
* 机构端首页 - 模拟数据(1:1 还原原型 V1.4)
|
||||
* 机构端首页 - 卡片模板(仅标题/图标/样式,value 统一为 0,由接口数据覆盖)。
|
||||
* 不再保留任何 mock 假数据:接口无数据时展示 0 / 空值。
|
||||
*/
|
||||
// 仅作为卡片模板(标题/图标/样式),value 统一默认 0,由接口数据覆盖。
|
||||
export const STATISTIC_CARDS = [
|
||||
{ key: 'riskAnalysis', title: '待风险分析', value: 3, char: '险', colorClass: 'blue' },
|
||||
{ key: 'contractInput', title: '待合同录入', value: 2, char: '合', colorClass: 'violet' },
|
||||
{ key: 'projectTeam', title: '待项目组成立', value: 2, char: '组', colorClass: 'green' },
|
||||
{ key: 'siteSurvey', title: '待现场踏勘', value: 4, char: '场', colorClass: 'orange' },
|
||||
{ key: 'reportDraft', title: '待报告编制', value: 5, char: '报', colorClass: 'blue' },
|
||||
{ key: 'internalReview', title: '待内部审核', value: 3, char: '内', colorClass: 'violet' },
|
||||
{ key: 'techReview', title: '待技术审核', value: 2, char: '技', colorClass: 'orange' },
|
||||
{ key: 'processReview', title: '待过程控制审核', value: 1, char: '控', colorClass: 'green' },
|
||||
{ key: 'archived', title: '已归档项目', value: 9, char: '档', colorClass: 'cyan' },
|
||||
{ key: 'delayed', title: '项目延期', value: 2, char: '延', colorClass: 'red' },
|
||||
];
|
||||
|
||||
export const INFO_ALERTS = [
|
||||
{ key: 'qualCheck', title: '资质现场审查通知', value: 1, char: '审', colorClass: 'yellow' },
|
||||
{ key: 'supervisionCheck', title: '监督检查通知', value: 3, char: '检', colorClass: 'red' },
|
||||
{ key: 'unreadNotice', title: '未读监管通知', value: 2, char: '未', colorClass: 'blue' },
|
||||
];
|
||||
|
||||
export const PROJECT_SUMMARY = {
|
||||
total: 31,
|
||||
statutory: 24,
|
||||
};
|
||||
|
||||
export const ENTERPRISE_TYPE_STATS = {
|
||||
categories: ['危险化学品', '非煤矿山', '金属冶炼', '工贸', '烟花爆竹', '石油天然气', '其他行业'],
|
||||
projectCount: [24, 20, 15, 18, 12, 14, 10],
|
||||
statutoryCount: [19, 16, 12, 14, 9, 11, 7],
|
||||
};
|
||||
|
||||
export const PROJECT_TYPE_DISTRIBUTION = [
|
||||
{ name: '安全预评价', value: 12, color: '#4285f4' },
|
||||
{ name: '安全验收评价', value: 8, color: '#58b874' },
|
||||
{ name: '安全现状评价', value: 11, color: '#ff9f2f' },
|
||||
];
|
||||
|
||||
export const PROJECT_TOTAL = 31;
|
||||
|
||||
export const PROJECT_COMPLETION_LIST = [
|
||||
{
|
||||
id: 1,
|
||||
projectCode: 'XP-2026-001',
|
||||
projectName: '华安化工园区安全预评价',
|
||||
evalCompany: '重庆华安安全科技有限公司',
|
||||
evalType: '安全预评价',
|
||||
projectLeader: '张建国',
|
||||
endDate: '2026-09-30',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
projectCode: 'XP-2026-002',
|
||||
projectName: '鼎信建设安全验收评价',
|
||||
evalCompany: '重庆鼎信安全咨询有限公司',
|
||||
evalType: '安全验收评价',
|
||||
projectLeader: '李明华',
|
||||
endDate: '2026-08-15',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
projectCode: 'XP-2026-003',
|
||||
projectName: '西南矿业安全现状评价',
|
||||
evalCompany: '重庆西南矿业有限公司',
|
||||
evalType: '安全现状评价',
|
||||
projectLeader: '陈志强',
|
||||
endDate: '2026-10-20',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
projectCode: 'XP-2026-004',
|
||||
projectName: '两江装备制造安全现状评价',
|
||||
evalCompany: '重庆两江装备制造有限公司',
|
||||
evalType: '安全现状评价',
|
||||
projectLeader: '王海峰',
|
||||
endDate: '2026-11-08',
|
||||
},
|
||||
{ key: 'riskAnalysis', title: '待风险分析', value: 0, char: '险', colorClass: 'blue' },
|
||||
{ key: 'contractInput', title: '待合同录入', value: 0, char: '合', colorClass: 'violet' },
|
||||
{ key: 'projectTeam', title: '待项目组成立', value: 0, char: '组', colorClass: 'green' },
|
||||
{ key: 'siteSurvey', title: '待现场踏勘', value: 0, char: '场', colorClass: 'orange' },
|
||||
{ key: 'reportDraft', title: '待报告编制', value: 0, char: '报', colorClass: 'blue' },
|
||||
{ key: 'internalReview', title: '待内部审核', value: 0, char: '内', colorClass: 'violet' },
|
||||
{ key: 'techReview', title: '待技术审核', value: 0, char: '技', colorClass: 'orange' },
|
||||
{ key: 'processReview', title: '待过程控制审核', value: 0, char: '控', colorClass: 'green' },
|
||||
{ key: 'archived', title: '已归档项目', value: 0, char: '档', colorClass: 'cyan' },
|
||||
{ key: 'delayed', title: '项目延期', value: 0, char: '延', colorClass: 'red' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -313,3 +313,37 @@
|
|||
font-size: 12px;
|
||||
color: #d9f2ff;
|
||||
}
|
||||
|
||||
/* ============ 骨架占位(深色大屏) ============ */
|
||||
.sk-block--dark {
|
||||
background: linear-gradient(90deg, rgba(40, 92, 160, .35) 25%, rgba(70, 130, 210, .5) 37%, rgba(40, 92, 160, .35) 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: sk-shimmer-dark 1.3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes sk-shimmer-dark {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: 0 0; }
|
||||
}
|
||||
|
||||
/* 图表占位块 */
|
||||
.sk-chart--dark {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(90deg, rgba(40, 92, 160, .3) 25%, rgba(70, 130, 210, .45) 37%, rgba(40, 92, 160, .3) 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: sk-shimmer-dark 1.3s ease-in-out infinite;
|
||||
}
|
||||
.sk-chart--donut { height: 144px; }
|
||||
.sk-chart--dark.sk-chart--donut { height: 144px; }
|
||||
|
||||
/* 风险/预警小骨架块微调 */
|
||||
.risk-item .sk-block--dark,
|
||||
.review-stat .sk-block--dark,
|
||||
.life-stat .sk-block--dark,
|
||||
.progress-row .sk-block--dark,
|
||||
.map-summary .sk-block--dark {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import DriverBack from '~/pages/Container/Driver/components/DriverBack';
|
||||
import './index.css';
|
||||
import { cockpitMockData } from './mockData';
|
||||
import chongqingGeoJson from './data/chongqingGeoJson.json';
|
||||
import {
|
||||
fetchQualificationOverview,
|
||||
|
|
@ -46,85 +45,60 @@ function formatNow(date = new Date()) {
|
|||
return `${date.getFullYear()}年${pad(date.getMonth() + 1)}月${pad(date.getDate())}日 ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
// 将 mock 数据转为 API 结构,作为初始值
|
||||
function buildInitialFromMock(m) {
|
||||
// 所有数据的真实空值初始结构(无 mock,接口无数据时展示 0 / 空)
|
||||
function buildInitialState() {
|
||||
return {
|
||||
title: m.title,
|
||||
title: '重庆市安全评价在线监管一件事',
|
||||
currentTime: formatNow(),
|
||||
lifecycleStats: {
|
||||
newFilingOrgCount: m.lifecycleStats[0]?.value || 0,
|
||||
currentFilingOrgCount: m.lifecycleStats[1]?.value || 0,
|
||||
logoutOrgCount: m.lifecycleStats[2]?.value || 0,
|
||||
totalFilingOrgCount: m.lifecycleStats[3]?.value || 0,
|
||||
exitedEvaluatorCount: m.lifecycleStats[4]?.value || 0,
|
||||
currentEvaluatorCount: m.lifecycleStats[5]?.value || 0,
|
||||
newFilingOrgCount: 0,
|
||||
currentFilingOrgCount: 0,
|
||||
logoutOrgCount: 0,
|
||||
totalFilingOrgCount: 0,
|
||||
exitedEvaluatorCount: 0,
|
||||
currentEvaluatorCount: 0,
|
||||
},
|
||||
progress: {
|
||||
filingCompleteRate: m.progress[0]?.value || 0,
|
||||
bizActiveRate: m.progress[1]?.value || 0,
|
||||
newGrowthRate: m.progress[2]?.value || 0,
|
||||
filingCompleteRate: 0,
|
||||
bizActiveRate: 0,
|
||||
newGrowthRate: 0,
|
||||
},
|
||||
industryList: m.industry.map((item, i) => ({
|
||||
industryCode: String(i + 1),
|
||||
industryName: item.name,
|
||||
orgCount: item.institution,
|
||||
typeCount: item.rectification,
|
||||
})),
|
||||
riskItems: m.riskItems,
|
||||
riskPie: m.riskPie,
|
||||
kpiItems: m.kpis.map(k => ({
|
||||
code: '',
|
||||
name: k.title,
|
||||
value: parseFloat(k.value),
|
||||
yoy: parseFloat(k.change),
|
||||
})),
|
||||
regionList: m.mapAreaStats
|
||||
.filter(item => item.name !== '重庆市')
|
||||
.map(item => ({
|
||||
districtCode: '',
|
||||
districtName: item.name,
|
||||
evalProjectCount: item.projectCount,
|
||||
filingOrgCount: item.institutionCount,
|
||||
})),
|
||||
industryList: [],
|
||||
riskItems: [],
|
||||
riskPie: [],
|
||||
kpiItems: [],
|
||||
regionList: [],
|
||||
regionSummary: {
|
||||
totalEvalProject: m.mapAreaStats.find(s => s.name === '重庆市')?.projectCount || 0,
|
||||
totalFilingOrg: m.mapAreaStats.find(s => s.name === '重庆市')?.institutionCount || 0,
|
||||
totalEvalProject: 0,
|
||||
totalFilingOrg: 0,
|
||||
},
|
||||
mapScatterPoints: m.mapScatterPoints,
|
||||
processNodes: m.todo.map((item, i) => ({
|
||||
nodeCode: `NODE_${i}`,
|
||||
nodeName: item.label,
|
||||
totalCount: 0,
|
||||
pendingCount: item.value,
|
||||
})),
|
||||
trendBuckets: m.reviewLine.xAxis,
|
||||
trendSeries: [
|
||||
{ evalTypeCode: '', evalTypeName: '项目数', projectCounts: m.reviewLine.project, inspCounts: [] },
|
||||
{ evalTypeCode: '', evalTypeName: '监督检查数', projectCounts: m.reviewLine.supervision, inspCounts: [] },
|
||||
],
|
||||
mapScatterPoints: [],
|
||||
processNodes: [],
|
||||
trendBuckets: [],
|
||||
trendSeries: [],
|
||||
reviewStats: {
|
||||
inspCount: m.reviewProgress[0]?.value || 0,
|
||||
onsiteCheckCount: m.reviewProgress[0]?.value || 0,
|
||||
inspCount: 0,
|
||||
onsiteCheckCount: 0,
|
||||
qualKeepCount: 0,
|
||||
specialCount: 0,
|
||||
checkedOrgCount: m.reviewProgress[4]?.value || 0,
|
||||
foundProblemCount: m.reviewProgress[5]?.value || 0,
|
||||
checkedOrgCount: 0,
|
||||
foundProblemCount: 0,
|
||||
},
|
||||
reviewPieItems: m.reviewPie.map(item => ({
|
||||
code: '',
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
})),
|
||||
// 项目流程 mock 所用(基于 processOverview API nodes 结构)
|
||||
processMockNodes: m.process.map((item, i) => ({
|
||||
nodeCode: `NODE_${i}`,
|
||||
nodeName: item.label,
|
||||
totalCount: item.value,
|
||||
pendingCount: 0,
|
||||
})),
|
||||
reviewPieItems: [],
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 骨架占位(深色大屏) ====================
|
||||
function Skeleton({ width = '100%', height = 14, radius = 4, className = '' }) {
|
||||
return (
|
||||
<span
|
||||
className={`sk-block sk-block--dark ${className}`}
|
||||
style={{ width, height, borderRadius: radius }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 子组件 ====================
|
||||
|
||||
function EChart({ option, className, events, onInstance }) {
|
||||
|
|
@ -192,7 +166,7 @@ function Panel({ title, children, className = '' }) {
|
|||
}
|
||||
|
||||
// ==================== 资质全生命周期管理 ====================
|
||||
function LifecyclePanel({ data }) {
|
||||
function LifecyclePanel({ data, loading }) {
|
||||
const { lifecycleStats, progress, industryList } = data;
|
||||
|
||||
const statItems = useMemo(() => [
|
||||
|
|
@ -239,7 +213,10 @@ function LifecyclePanel({ data }) {
|
|||
{statItems.map((item) => (
|
||||
<div className="life-stat" key={item.label}>
|
||||
<span className="life-stat__icon"><FileProtectOutlined /></span>
|
||||
<div><p>{item.label}</p><strong>{item.value}</strong></div>
|
||||
<div>
|
||||
<p>{item.label}</p>
|
||||
{loading ? <Skeleton width={48} height={18} /> : <strong>{item.value}</strong>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -248,17 +225,17 @@ function LifecyclePanel({ data }) {
|
|||
<div className="progress-row" key={item.label}>
|
||||
<span>{item.label}</span>
|
||||
<div className="progress-track"><i style={{ width: `${item.value}%`, background: item.color }} /></div>
|
||||
<b>{item.value}%</b>
|
||||
{loading ? <Skeleton width={40} height={14} /> : <b>{item.value}%</b>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<EChart className="industry-chart" option={barOption} />
|
||||
{loading ? <div className="sk-chart sk-chart--dark" /> : <EChart className="industry-chart" option={barOption} />}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 风险预警(仍用 mock,无 API) ====================
|
||||
function RiskPanel({ data }) {
|
||||
// ==================== 风险预警(暂无对应 API,接口未接入时展示空值) ====================
|
||||
function RiskPanel({ data, loading }) {
|
||||
const pieOption = useMemo(() => ({
|
||||
color: ['#5f8cff', '#e96fff', '#5fd8ae', '#b6a0ff'],
|
||||
tooltip: { trigger: 'item', backgroundColor: 'rgba(3, 25, 66, .92)', borderColor: '#1aa7ff', textStyle: { color: '#dff7ff' } },
|
||||
|
|
@ -266,28 +243,35 @@ function RiskPanel({ data }) {
|
|||
series: [{ type: 'pie', radius: ['46%', '70%'], center: ['30%', '55%'], avoidLabelOverlap: true, label: { show: false }, data: data.riskPie }],
|
||||
}), [data.riskPie]);
|
||||
|
||||
const riskSkeleton = Array.from({ length: 4 });
|
||||
|
||||
return (
|
||||
<Panel title="风险预警智能研判" className="risk-panel">
|
||||
<h3>资质备案类失信统计</h3>
|
||||
<div className="risk-grid">
|
||||
{data.riskItems.map(item => <div className="risk-item" key={item.label}><span>{item.label}</span><b>{item.value}</b></div>)}
|
||||
{loading
|
||||
? riskSkeleton.map((_, i) => (
|
||||
<div className="risk-item" key={`sk-${i}`}><span><Skeleton width={64} height={12} /></span><Skeleton width={28} height={14} /></div>
|
||||
))
|
||||
: data.riskItems.map(item => <div className="risk-item" key={item.label}><span>{item.label}</span><b>{item.value}</b></div>)}
|
||||
</div>
|
||||
<h3>备案机构违法触发统计</h3>
|
||||
<EChart className="risk-pie" option={pieOption} />
|
||||
{loading ? <div className="sk-chart sk-chart--dark sk-chart--donut" /> : <EChart className="risk-pie" option={pieOption} />}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== KPI 指标卡片 ====================
|
||||
function KpiCards({ data }) {
|
||||
function KpiCards({ data, loading }) {
|
||||
const { kpiItems } = data;
|
||||
const kpiSkeleton = Array.from({ length: 5 });
|
||||
return (
|
||||
<div className="kpi-row">
|
||||
{kpiItems.map(item => (
|
||||
<div className="kpi-card" key={item.name}>
|
||||
<p>{item.name}</p>
|
||||
<strong>{item.value}%</strong>
|
||||
<span>同比 <b>↗ {item.yoy}%</b></span>
|
||||
{(loading ? kpiSkeleton : kpiItems).map((item, index) => (
|
||||
<div className="kpi-card" key={loading ? `sk-${index}` : item.name}>
|
||||
<p>{loading ? <Skeleton width={56} height={12} /> : item.name}</p>
|
||||
<strong>{loading ? <Skeleton width={44} height={20} /> : `${item.value}%`}</strong>
|
||||
<span>{loading ? <Skeleton width={72} height={12} /> : `同比 ↗ ${item.yoy}%`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -295,7 +279,7 @@ function KpiCards({ data }) {
|
|||
}
|
||||
|
||||
// ==================== 地图区域分布 ====================
|
||||
function MapSection({ data }) {
|
||||
function MapSection({ data, loading }) {
|
||||
const [selectedAreaName, setSelectedAreaName] = useState('重庆市');
|
||||
const zoomRef = useRef(1.1);
|
||||
const chartInstanceRef = useRef(null);
|
||||
|
|
@ -443,11 +427,15 @@ function MapSection({ data }) {
|
|||
return (
|
||||
<div className="map-shell">
|
||||
<EChart className="map-chart" option={mapOption} events={mapEvents} onInstance={handleMapInstance} />
|
||||
{mapScatterPoints.map((item, index) => <div className={`map-card map-card--${index + 1}`} key={item.name}><strong>{item.name}</strong><span>正在开展评价项目数:{item.value}</span></div>)}
|
||||
{!loading && mapScatterPoints.map((item, index) => <div className={`map-card map-card--${index + 1}`} key={item.name}><strong>{item.name}</strong><span>正在开展评价项目数:{item.value}</span></div>)}
|
||||
<div className="map-summary">
|
||||
<strong>{selectedArea.name}</strong>
|
||||
<span>项目数:{selectedArea.evalProjectCount}</span>
|
||||
<span>备案机构数:{selectedArea.filingOrgCount}</span>
|
||||
{loading
|
||||
? <><Skeleton width={70} height={14} /><Skeleton width={90} height={14} /></>
|
||||
: <>
|
||||
<span>项目数:{selectedArea.evalProjectCount}</span>
|
||||
<span>备案机构数:{selectedArea.filingOrgCount}</span>
|
||||
</>}
|
||||
</div>
|
||||
<ul className="map-legend"><li>0-10</li><li>11-30</li><li>31-50</li><li>51-100</li></ul>
|
||||
</div>
|
||||
|
|
@ -455,15 +443,21 @@ function MapSection({ data }) {
|
|||
}
|
||||
|
||||
// ==================== 执业全过程管控 ====================
|
||||
function TodoPanel({ data }) {
|
||||
function TodoPanel({ data, loading }) {
|
||||
const { processNodes } = data;
|
||||
const skeletonNodes = Array.from({ length: 9 });
|
||||
return (
|
||||
<Panel title="执业全过程管控" className="todo-panel">
|
||||
<div className="todo-grid">
|
||||
{processNodes.map((item, index) => {
|
||||
const Icon = iconMap[index] || FileDoneOutlined;
|
||||
return <div className="todo-item" key={item.nodeName}><span><Icon /></span><p>{item.nodeName}</p><b>{item.pendingCount}</b></div>;
|
||||
})}
|
||||
{loading
|
||||
? skeletonNodes.map((_, index) => {
|
||||
const Icon = iconMap[index] || FileDoneOutlined;
|
||||
return <div className="todo-item" key={`sk-${index}`}><span><Icon /></span><p><Skeleton width={56} height={12} /></p><Skeleton width={24} height={16} /></div>;
|
||||
})
|
||||
: processNodes.map((item, index) => {
|
||||
const Icon = iconMap[index] || FileDoneOutlined;
|
||||
return <div className="todo-item" key={item.nodeName}><span><Icon /></span><p>{item.nodeName}</p><b>{item.pendingCount}</b></div>;
|
||||
})}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
|
|
@ -476,7 +470,7 @@ const PERIOD_TABS = [
|
|||
{ key: 'month', label: '月' },
|
||||
];
|
||||
|
||||
function ReviewLinePanel({ data, period, onPeriodChange }) {
|
||||
function ReviewLinePanel({ data, period, onPeriodChange, loading }) {
|
||||
const { trendBuckets, trendSeries } = data;
|
||||
|
||||
const lineOption = useMemo(() => {
|
||||
|
|
@ -510,13 +504,13 @@ function ReviewLinePanel({ data, period, onPeriodChange }) {
|
|||
>{tab.label}</span>
|
||||
))}
|
||||
</div>
|
||||
<EChart className="line-chart" option={lineOption} />
|
||||
{loading ? <div className="sk-chart sk-chart--dark" /> : <EChart className="line-chart" option={lineOption} />}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 复盘评估改进提效 ====================
|
||||
function ReviewProgressPanel({ data }) {
|
||||
function ReviewProgressPanel({ data, loading }) {
|
||||
const { reviewStats, reviewPieItems } = data;
|
||||
|
||||
const statItems = useMemo(() => [
|
||||
|
|
@ -538,25 +532,39 @@ function ReviewProgressPanel({ data }) {
|
|||
return (
|
||||
<Panel title="复盘评估改进提效" className="review-panel">
|
||||
<div className="review-stats">
|
||||
{statItems.map(item => <div className="review-stat" key={item.label}><span><FundProjectionScreenOutlined /></span><p>{item.label}:<b>{item.value}</b></p></div>)}
|
||||
{statItems.map(item => (
|
||||
<div className="review-stat" key={item.label}>
|
||||
<span><FundProjectionScreenOutlined /></span>
|
||||
<p>{item.label}:{loading ? <Skeleton width={28} height={13} /> : <b>{item.value}</b>}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<EChart className="review-pie" option={pieOption} />
|
||||
{loading ? <div className="sk-chart sk-chart--dark sk-chart--donut" /> : <EChart className="review-pie" option={pieOption} />}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 项目流程 ====================
|
||||
function ProcessPanel({ data }) {
|
||||
function ProcessPanel({ data, loading }) {
|
||||
const { processNodes } = data;
|
||||
const skeletonNodes = Array.from({ length: 5 });
|
||||
return (
|
||||
<Panel title="项目流程" className="process-panel">
|
||||
<div className="process-grid">
|
||||
{processNodes.map((item, index) => (
|
||||
<div className="process-node" key={item.nodeName}>
|
||||
<strong>{item.totalCount}</strong>
|
||||
<span>{item.nodeName}</span>
|
||||
{index < processNodes.length - 1 && <i />}
|
||||
</div>
|
||||
{loading
|
||||
? skeletonNodes.map((_, index) => (
|
||||
<div className="process-node" key={`sk-${index}`}>
|
||||
<Skeleton width={44} height={18} />
|
||||
<span><Skeleton width={60} height={12} /></span>
|
||||
{index < skeletonNodes.length - 1 && <i />}
|
||||
</div>
|
||||
))
|
||||
: processNodes.map((item, index) => (
|
||||
<div className="process-node" key={item.nodeName}>
|
||||
<strong>{item.totalCount}</strong>
|
||||
<span>{item.nodeName}</span>
|
||||
{index < processNodes.length - 1 && <i />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
|
|
@ -565,7 +573,7 @@ function ProcessPanel({ data }) {
|
|||
|
||||
// ==================== 主组件 ====================
|
||||
export default function SupervisionCockpit() {
|
||||
const [data, setData] = useState(() => buildInitialFromMock(cockpitMockData));
|
||||
const [data, setData] = useState(() => buildInitialState());
|
||||
|
||||
// 取当前年份/月份
|
||||
const currentYear = useMemo(() => new Date().getFullYear(), []);
|
||||
|
|
@ -591,68 +599,76 @@ export default function SupervisionCockpit() {
|
|||
process: false,
|
||||
});
|
||||
|
||||
// 首屏加载态(请求期间展示骨架占位,避免空白)
|
||||
const [loading, setLoading] = useState(true);
|
||||
// 趋势图切换加载态
|
||||
const [trendLoading, setTrendLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// 1. 资质全生命周期管理
|
||||
fetchQualificationOverview(currentYear)
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
lifecycleStats: apiData.lifecycleStats || prev.lifecycleStats,
|
||||
progress: apiData.progress || prev.progress,
|
||||
industryList: apiData.industryList || prev.industryList,
|
||||
}));
|
||||
setApiFlags(prev => ({ ...prev, qualification: true }));
|
||||
})
|
||||
.catch(() => { /* 接口异常,沿用 mock 数据 */ });
|
||||
const tasks = [
|
||||
// 1. 资质全生命周期管理
|
||||
fetchQualificationOverview(currentYear)
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
lifecycleStats: apiData.lifecycleStats || prev.lifecycleStats,
|
||||
progress: apiData.progress || prev.progress,
|
||||
industryList: apiData.industryList || prev.industryList,
|
||||
}));
|
||||
setApiFlags(prev => ({ ...prev, qualification: true }));
|
||||
})
|
||||
.catch(() => {}),
|
||||
|
||||
// 2. 核心 KPI
|
||||
fetchKpi('year', currentYear)
|
||||
.then((apiData) => {
|
||||
if (apiData.items && apiData.items.length > 0) {
|
||||
setData(prev => ({ ...prev, kpiItems: apiData.items }));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
// 2. 核心 KPI
|
||||
fetchKpi('year', currentYear)
|
||||
.then((apiData) => {
|
||||
if (apiData.items && apiData.items.length > 0) {
|
||||
setData(prev => ({ ...prev, kpiItems: apiData.items }));
|
||||
}
|
||||
})
|
||||
.catch(() => {}),
|
||||
|
||||
// 3. 区域地图分布
|
||||
fetchRegionDistribution()
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
regionList: apiData.list || prev.regionList,
|
||||
regionSummary: apiData.summary || prev.regionSummary,
|
||||
}));
|
||||
})
|
||||
.catch(() => {});
|
||||
// 3. 区域地图分布
|
||||
fetchRegionDistribution()
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
regionList: apiData.list || prev.regionList,
|
||||
regionSummary: apiData.summary || prev.regionSummary,
|
||||
}));
|
||||
})
|
||||
.catch(() => {}),
|
||||
|
||||
// 4. 执业全过程管控 + 项目流程
|
||||
fetchProcessOverview()
|
||||
.then((apiData) => {
|
||||
if (apiData.nodes && apiData.nodes.length > 0) {
|
||||
setData(prev => ({ ...prev, processNodes: apiData.nodes }));
|
||||
setApiFlags(prev => ({ ...prev, process: true }));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
// 4. 执业全过程管控 + 项目流程
|
||||
fetchProcessOverview()
|
||||
.then((apiData) => {
|
||||
if (apiData.nodes && apiData.nodes.length > 0) {
|
||||
setData(prev => ({ ...prev, processNodes: apiData.nodes }));
|
||||
setApiFlags(prev => ({ ...prev, process: true }));
|
||||
}
|
||||
})
|
||||
.catch(() => {}),
|
||||
|
||||
// 6. 复盘评估改进提效
|
||||
fetchReviewSummary(currentYear)
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
reviewStats: apiData.stats || prev.reviewStats,
|
||||
reviewPieItems: apiData.pie || prev.reviewPieItems,
|
||||
}));
|
||||
})
|
||||
.catch(() => {});
|
||||
// 6. 复盘评估改进提效
|
||||
fetchReviewSummary(currentYear)
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
reviewStats: apiData.stats || prev.reviewStats,
|
||||
reviewPieItems: apiData.pie || prev.reviewPieItems,
|
||||
}));
|
||||
})
|
||||
.catch(() => {}),
|
||||
|
||||
// 7. 项目实时监控(当前暂无对应面板,仅预取)
|
||||
fetchProjectMonitor(currentMonth)
|
||||
.catch(() => {});
|
||||
// 7. 项目实时监控(当前暂无对应面板,仅预取)
|
||||
fetchProjectMonitor(currentMonth).catch(() => {}),
|
||||
];
|
||||
Promise.all(tasks).finally(() => setLoading(false));
|
||||
}, [currentYear, currentMonth]);
|
||||
|
||||
// 5. 评价类型趋势(随 年/季/月 tab 切换重新查询)
|
||||
useEffect(() => {
|
||||
setTrendLoading(true);
|
||||
fetchEvalTypeTrend(trendPeriod, currentYear)
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
|
|
@ -661,16 +677,17 @@ export default function SupervisionCockpit() {
|
|||
trendSeries: apiData.series || prev.trendSeries,
|
||||
}));
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {})
|
||||
.finally(() => setTrendLoading(false));
|
||||
}, [trendPeriod, currentYear]);
|
||||
|
||||
// 项目流程面板数据:API 有值时用 processNodes,否则用 mock processMockNodes
|
||||
// 项目流程面板数据:无接口数据时展示空
|
||||
const processPanelNodes = useMemo(() => {
|
||||
if (apiFlags.process && data.processNodes && data.processNodes.length > 0) {
|
||||
return data.processNodes;
|
||||
}
|
||||
return data.processMockNodes || data.processNodes || [];
|
||||
}, [apiFlags.process, data.processNodes, data.processMockNodes]);
|
||||
return data.processNodes || [];
|
||||
}, [apiFlags.process, data.processNodes]);
|
||||
|
||||
return (
|
||||
<div className="cockpit-page">
|
||||
|
|
@ -678,22 +695,23 @@ export default function SupervisionCockpit() {
|
|||
<ScreenHeader data={data} />
|
||||
<main className="cockpit-layout">
|
||||
<aside className="cockpit-left">
|
||||
<LifecyclePanel data={data} />
|
||||
<RiskPanel data={data} />
|
||||
<LifecyclePanel data={data} loading={loading} />
|
||||
<RiskPanel data={data} loading={loading} />
|
||||
</aside>
|
||||
<section className="cockpit-center">
|
||||
<KpiCards data={data} />
|
||||
<MapSection data={data} />
|
||||
<ProcessPanel data={{ processNodes: processPanelNodes }} />
|
||||
<KpiCards data={data} loading={loading} />
|
||||
<MapSection data={data} loading={loading} />
|
||||
<ProcessPanel data={{ processNodes: processPanelNodes }} loading={loading} />
|
||||
</section>
|
||||
<aside className="cockpit-right">
|
||||
<TodoPanel data={data} />
|
||||
<TodoPanel data={data} loading={loading} />
|
||||
<ReviewLinePanel
|
||||
data={data}
|
||||
period={trendPeriod}
|
||||
onPeriodChange={setTrendPeriod}
|
||||
loading={trendLoading}
|
||||
/>
|
||||
<ReviewProgressPanel data={data} />
|
||||
<ReviewProgressPanel data={data} loading={loading} />
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue