From 9f4b78a5ad6387d49dad39fbf3ba2121cb60e0a3 Mon Sep 17 00:00:00 2001 From: huwei <3313749341@qq.com> Date: Wed, 5 Aug 2026 14:10:10 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/institution/index.js | 44 ++ src/api/supervision/index.js | 54 ++ src/assets/images/map_bi/back2.png | Bin 0 -> 1166 bytes .../Driver/components/DriverBack/index.js | 15 + .../Driver/components/DriverBack/index.less | 23 + src/pages/Container/Driver/index.js | 2 +- .../Container/Institution/Dashboard/index.js | 3 - .../components/EnterpriseTypeChart.jsx | 76 +-- .../Institution/components/InfoAlerts.jsx | 39 +- .../components/ProjectCompletionTable.jsx | 93 ++- .../components/ProjectTypeChart.jsx | 79 ++- .../Institution/components/StatisticCards.jsx | 36 +- src/pages/Container/Institution/index.css | 548 +++++++++++------- src/pages/Container/Institution/index.js | 230 ++++++-- src/pages/Container/Institution/mockData.js | 95 +-- src/pages/Container/Layout/index.jsx | 3 +- .../Container/Supervision/Cockpit/index.css | 50 +- .../Container/Supervision/Cockpit/index.js | 526 ++++++++++++++--- src/utils/backToBase.js | 15 + src/utils/handleApiResponse.js | 10 + src/utils/resolveDriverAuthParams.js | 22 + 21 files changed, 1358 insertions(+), 605 deletions(-) create mode 100644 src/api/institution/index.js create mode 100644 src/api/supervision/index.js create mode 100644 src/assets/images/map_bi/back2.png create mode 100644 src/pages/Container/Driver/components/DriverBack/index.js create mode 100644 src/pages/Container/Driver/components/DriverBack/index.less delete mode 100644 src/pages/Container/Institution/Dashboard/index.js create mode 100644 src/utils/backToBase.js create mode 100644 src/utils/handleApiResponse.js create mode 100644 src/utils/resolveDriverAuthParams.js diff --git a/src/api/institution/index.js b/src/api/institution/index.js new file mode 100644 index 0000000..f9dec7b --- /dev/null +++ b/src/api/institution/index.js @@ -0,0 +1,44 @@ +import { apiGet } from "../../utils/enterpriseInfo/http"; + +/** + * 机构端首页(驾驶舱)数据接口。 + * + * 统一走项目 http 层(@cqsjjb/jjb-common-lib),自动附带 API_HOST、token 与机构上下文请求头。 + * 页面以 `await fetchXxx()` 直调并取业务 data,故此处统一解包: + * 失败时抛错,由页面 catch 兜底展示空态。 + */ +function unwrap(res) { + if (!res) { + throw new Error("请求失败"); + } + if (res.success === false) { + throw new Error(res.message || "请求失败"); + } + // 兼容 { code, data } 与已被 http 层解包的裸数据两种形态 + return Object.prototype.hasOwnProperty.call(res, "data") ? res.data : res; +} + +// 当前项目节点统计 +export async function fetchProjectNodeStats() { + return unwrap(await apiGet("/safetyEval/institution/dashboard/project-node-stats")); +} + +// 通知提醒 +export async function fetchNotices() { + return unwrap(await apiGet("/safetyEval/institution/dashboard/notices")); +} + +// 服务行业项目统计 +export async function fetchIndustryStat() { + return unwrap(await apiGet("/safetyEval/institution/dashboard/industry-stat")); +} + +// 评价类别占比 +export async function fetchEvalTypeRatio() { + return unwrap(await apiGet("/safetyEval/institution/dashboard/eval-type-ratio")); +} + +// 项目执行情况(limit 默认 10) +export async function fetchProjectExecution(limit = 10) { + return unwrap(await apiGet("/safetyEval/institution/dashboard/project-execution", { limit })); +} diff --git a/src/api/supervision/index.js b/src/api/supervision/index.js new file mode 100644 index 0000000..c6fbbee --- /dev/null +++ b/src/api/supervision/index.js @@ -0,0 +1,54 @@ +import { apiGet } from "../../utils/enterpriseInfo/http"; + +/** + * 监管端驾驶舱数据接口。 + * + * 统一走项目 http 层(@cqsjjb/jjb-common-lib),自动附带 API_HOST、token 与机构上下文请求头。 + * 页面以 `await fetchXxx()` 直调并取业务 data,故此处统一解包: + * 失败时抛错,由页面 catch 兜底展示空态。 + */ +function unwrap(res) { + if (!res) { + throw new Error("请求失败"); + } + if (res.success === false) { + throw new Error(res.message || "请求失败"); + } + // 兼容 { code, data } 与已被 http 层解包的裸数据两种形态 + return Object.prototype.hasOwnProperty.call(res, "data") ? res.data : res; +} + +// 资质全生命周期管理 +export async function fetchQualificationOverview(year) { + return unwrap(await apiGet("/safetyEval/regulator/cockpit/qualification-overview", { year })); +} + +// 核心KPI指标 +export async function fetchKpi(cycle, year) { + return unwrap(await apiGet("/safetyEval/regulator/cockpit/kpi", { cycle, year })); +} + +// 区域地图分布 +export async function fetchRegionDistribution() { + return unwrap(await apiGet("/safetyEval/regulator/cockpit/region-distribution")); +} + +// 执业全过程管控 + 项目流程 +export async function fetchProcessOverview() { + return unwrap(await apiGet("/safetyEval/regulator/cockpit/process-overview")); +} + +// 评价类型趋势 +export async function fetchEvalTypeTrend(period, year) { + return unwrap(await apiGet("/safetyEval/regulator/cockpit/eval-type-trend", { period, year })); +} + +// 复盘评估改进提效 +export async function fetchReviewSummary(year) { + return unwrap(await apiGet("/safetyEval/regulator/cockpit/review-summary", { year })); +} + +// 项目实时监控 +export async function fetchProjectMonitor(month) { + return unwrap(await apiGet("/safetyEval/regulator/cockpit/project-monitor", { month })); +} diff --git a/src/assets/images/map_bi/back2.png b/src/assets/images/map_bi/back2.png new file mode 100644 index 0000000000000000000000000000000000000000..406138d26d4e86c93e29557d8f0c8504d8ea8fd5 GIT binary patch literal 1166 zcmeAS@N?(olHy`uVBq!ia0vp@K+MO%1|+}KPrC%9BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFso&TM3hAM`dB6B=jtVb)aX^@765fKFxc2v6eK2Rr0UTq__OB&@Hb09I0xZL0)vRD^GUf^&XRs)DJWnQpS7iK&9QrJkXg zv5BRnj)IYap@qJIg}$M&uA!NgiGh`okpdJb0c|TvNwW%aaf8|g1^l#~=$>Fbx5 zm+O@q>*W`v>l<2HTIw4Z=^Gj80#)c1SLT%@R_NvxE5l51Ni9w;$}A|!%+FH*nV6WA zUs__T1av9H3%LbwWAlok!2}F2{ffi_eM3D1ke6TzeSPsO&CP|YE-nd5MYtEM!Nnn! z1*!T$sm1xFMajU3OH&3}Rbb^@l$uzQUlfv`p92fUfQw&4cPq!R;0UoO<dK}gn%gs#DphZAP1iKQ}ci+z6hAK&;J#B4$Kz+ zJY5_^DsCl(*p&4$awoVv{_p6;%?NBeK&n z`l#iNfS4mq?#kYUcYoI>@JcWo^{@J4bH+hhXkP7qOWqX^^_lq&w8-#Jx2yZP z``O3S4w8O-zm!TVL!D+lm{{@Q>+ksNd3$<-9z4p?4h;`DCc5+gMU{W@Oj$Aizl1K4 zF?X+Cx@?isE*s%#PnZt<|F*yCd-3bd<`WNCa7})a;?A42WXtgm<(q%p(>EtfOmX?n eyeqANftlgh$B5nlUME3N3E}DL=d#Wzp$PynJ&{iU literal 0 HcmV?d00001 diff --git a/src/pages/Container/Driver/components/DriverBack/index.js b/src/pages/Container/Driver/components/DriverBack/index.js new file mode 100644 index 0000000..ac25eb7 --- /dev/null +++ b/src/pages/Container/Driver/components/DriverBack/index.js @@ -0,0 +1,15 @@ +import backImg from "~/assets/images/map_bi/back2.png"; +import { backToBase } from "~/utils/backToBase"; +import "./index.less"; + +export default function DriverBack({ theme = "light", className = "" }) { + return ( +
+ + 返回 +
+ ); +} diff --git a/src/pages/Container/Driver/components/DriverBack/index.less b/src/pages/Container/Driver/components/DriverBack/index.less new file mode 100644 index 0000000..d181dda --- /dev/null +++ b/src/pages/Container/Driver/components/DriverBack/index.less @@ -0,0 +1,23 @@ +.driver-back { + display: inline-flex; + align-items: center; + gap: 5px; + cursor: pointer; + user-select: none; + font-size: 14px; + line-height: 1; + white-space: nowrap; + + img { + width: 20px; + height: 20px; + } + + &--light { + color: #1677ff; + } + + &--dark { + color: #7ec8ff; + } +} diff --git a/src/pages/Container/Driver/index.js b/src/pages/Container/Driver/index.js index 98683f4..9490a75 100644 --- a/src/pages/Container/Driver/index.js +++ b/src/pages/Container/Driver/index.js @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { Button, Skeleton, Tabs } from "antd"; import { NS_DRIVER } from "~/enumerate/namespace"; -import Institution from "~/pages/Container/Institution/Dashboard"; +import Institution from "~/pages/Container/Institution"; import Supervision from "~/pages/Container/Supervision/Dashboard"; import Cockpit from "~/pages/Container/Supervision/Cockpit"; diff --git a/src/pages/Container/Institution/Dashboard/index.js b/src/pages/Container/Institution/Dashboard/index.js deleted file mode 100644 index 345484a..0000000 --- a/src/pages/Container/Institution/Dashboard/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import InstitutionDashboard from '../Index'; - -export default InstitutionDashboard; diff --git a/src/pages/Container/Institution/components/EnterpriseTypeChart.jsx b/src/pages/Container/Institution/components/EnterpriseTypeChart.jsx index 41c931e..1d43a17 100644 --- a/src/pages/Container/Institution/components/EnterpriseTypeChart.jsx +++ b/src/pages/Container/Institution/components/EnterpriseTypeChart.jsx @@ -1,11 +1,10 @@ import React, { useEffect, useMemo, useRef } from 'react'; import * as echarts from 'echarts'; -import { Card } from 'antd'; -const BLUE = '#5b8ff9'; -const ORANGE = '#ffb33e'; +const BLUE = '#4285f4'; +const ORANGE = '#ffab31'; -function EChart({ option, className }) { +export default function EnterpriseTypeChart({ data }) { const chartRef = useRef(null); const instanceRef = useRef(null); @@ -16,7 +15,6 @@ function EChart({ option, className }) { const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null; if (observer) observer.observe(chartRef.current); window.addEventListener('resize', resize); - return () => { window.removeEventListener('resize', resize); if (observer) observer.disconnect(); @@ -25,67 +23,47 @@ function EChart({ option, className }) { }; }, []); - useEffect(() => { - if (instanceRef.current) instanceRef.current.setOption(option, true); - }, [option]); + const list = data && data.list ? data.list : []; + const categories = list.map(item => item.industryName); + const projectData = list.map(item => Number(item.projectCount) || 0); + const statutoryData = list.map(item => Number(item.statutoryProjectCount) || 0); + const maxVal = Math.max(...projectData, ...statutoryData, 1); + const yMax = Math.ceil(maxVal / 10) * 10 || 10; - return
; -} - -export default function EnterpriseTypeChart({ data }) { const option = useMemo(() => ({ color: [BLUE, ORANGE], - grid: { left: 38, right: 10, top: 26, bottom: 46 }, + grid: { left: 36, right: 10, top: 12, bottom: 28 }, tooltip: { trigger: 'axis', - axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(91, 143, 249, .08)' } }, backgroundColor: '#fff', - borderColor: '#e9edf4', + borderColor: '#e2e8f0', borderWidth: 1, - textStyle: { color: '#333', fontSize: 12 }, + textStyle: { color: '#1e293b', fontSize: 12 }, }, xAxis: { type: 'category', - data: data.categories, + data: categories, axisTick: { show: false }, - axisLine: { lineStyle: { color: '#edf0f5' } }, - axisLabel: { - color: '#666', - fontSize: 12, - interval: 0, - width: 58, - overflow: 'break', - lineHeight: 14, - }, + axisLine: { lineStyle: { color: '#e2e8f0' } }, + axisLabel: { color: '#64748b', fontSize: 11, interval: 0 }, }, yAxis: { type: 'value', min: 0, - max: 60, - splitNumber: 6, - axisLabel: { color: '#8792a2', fontSize: 12 }, - splitLine: { lineStyle: { color: '#e9edf4', type: 'dashed' } }, + max: yMax, + interval: yMax > 10 ? yMax / 3 : 5, + axisLabel: { color: '#8492a6', fontSize: 10, margin: 4 }, + splitLine: { lineStyle: { color: '#dce5ef', type: 'dashed' } }, }, series: [ - { name: '项目数', type: 'bar', barWidth: 12, data: data.projectCount, itemStyle: { borderRadius: [2, 2, 0, 0] } }, - { name: '金额', type: 'bar', barWidth: 12, data: data.amount, itemStyle: { borderRadius: [2, 2, 0, 0] } }, + { name: '项目数', type: 'bar', barWidth: 14, barGap: '30%', data: projectData, itemStyle: { borderRadius: [2, 2, 0, 0] } }, + { name: '法定项目', type: 'bar', barWidth: 14, data: statutoryData, itemStyle: { borderRadius: [2, 2, 0, 0] } }, ], - }), [data]); + }), [categories, projectData, statutoryData, yMax]); - return ( - 服务企业类型统计} - size="small" - className="institution-panel-card institution-enterprise-card" - styles={{ body: { padding: '20px 26px 15px' } }} - extra={ -
- 项目数 - 金额 -
- } - > - -
- ); + useEffect(() => { + if (instanceRef.current) instanceRef.current.setOption(option, true); + }, [option]); + + return
; } diff --git a/src/pages/Container/Institution/components/InfoAlerts.jsx b/src/pages/Container/Institution/components/InfoAlerts.jsx index a520dad..b1e7b46 100644 --- a/src/pages/Container/Institution/components/InfoAlerts.jsx +++ b/src/pages/Container/Institution/components/InfoAlerts.jsx @@ -1,37 +1,16 @@ import React from 'react'; -import { - AuditOutlined, - StarFilled, - UserDeleteOutlined -} from '@ant-design/icons'; - -const iconMap = { - Star: StarFilled, - Audit: AuditOutlined, - UserDelete: UserDeleteOutlined -}; - -function AlertItem({ item }) { - const Icon = iconMap[item.icon] || AuditOutlined; - - return ( -
-
- -
-
-
{item.title}
-
{item.value}
-
-
- ); -} export default function InfoAlerts({ data }) { return ( -
- {data.map((item) => ( - +
+ {data.map(item => ( +
+ {item.char} +
+ {item.title} + {item.value} +
+
))}
); diff --git a/src/pages/Container/Institution/components/ProjectCompletionTable.jsx b/src/pages/Container/Institution/components/ProjectCompletionTable.jsx index 0e7c826..2d47aab 100644 --- a/src/pages/Container/Institution/components/ProjectCompletionTable.jsx +++ b/src/pages/Container/Institution/components/ProjectCompletionTable.jsx @@ -1,35 +1,70 @@ import React from 'react'; -import { Card, Table } from 'antd'; -const columns = [ - { title: '序号', dataIndex: 'id', key: 'id', width: 70, align: 'center' }, - { title: '项目名称', dataIndex: 'projectName', key: 'projectName', ellipsis: true }, - { title: '项目状态', dataIndex: 'status', key: 'status', width: 120, align: 'center' }, - { title: '项目负责人', dataIndex: 'projectLeader', key: 'projectLeader', width: 120, align: 'center' }, - { title: '客户负责人', dataIndex: 'clientLeader', key: 'clientLeader', width: 120, align: 'center' }, - { title: '项目开始时间', dataIndex: 'startDate', key: 'startDate', width: 180, align: 'center' }, - { title: '项目结束时间', dataIndex: 'endDate', key: 'endDate', width: 180, align: 'center' }, - { title: '项目阶段', dataIndex: 'acceptanceDate', key: 'acceptanceDate', width: 180, align: 'center' }, - { title: '操作', key: 'action', width: 110, align: 'center', render: () => 查看详情 } -]; +export default function ProjectCompletionTable({ data, loading, onView }) { + const list = data && data.list ? data.list : []; + + // 加载占位:固定行数的骨架,保持表格布局 + const skeletonRows = Array.from({ length: 5 }); -export default function ProjectCompletionTable({ data }) { return ( - 项目完成情况统计} - size="small" - className="institution-panel-card institution-table-card" - styles={{ body: { padding: '12px 14px 14px' } }} - > - - +
+
+

项目执行情况

+ +
+
+
+ + + + + + + + + + + + + + {loading + ? skeletonRows.map((_, index) => ( + + + + + + + + + + + )) + : list.map((record, index) => ( + + + + + + + + + + + ))} + +
序号项目编号项目名称被评价企业评价类别项目负责人项目结束日期操作
{index + 1}{record.projectNo}{record.projectName}{record.customerName}{record.evalTypeName}{record.projectLeaderName}{record.planEndDate} + +
+
+ ); } diff --git a/src/pages/Container/Institution/components/ProjectTypeChart.jsx b/src/pages/Container/Institution/components/ProjectTypeChart.jsx index f89bccf..a74c04f 100644 --- a/src/pages/Container/Institution/components/ProjectTypeChart.jsx +++ b/src/pages/Container/Institution/components/ProjectTypeChart.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useRef } from 'react'; import * as echarts from 'echarts'; -import { Card } from 'antd'; + +const COLORS = ['#4285f4', '#58b874', '#ff9f2f']; function EChart({ option, className }) { const chartRef = useRef(null); @@ -13,7 +14,6 @@ function EChart({ option, className }) { const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null; if (observer) observer.observe(chartRef.current); window.addEventListener('resize', resize); - return () => { window.removeEventListener('resize', resize); if (observer) observer.disconnect(); @@ -29,89 +29,80 @@ function EChart({ option, className }) { return
; } -function Legend({ data }) { - return ( -
- {data.map((item) => ( - - - {item.name} - - ))} -
- ); -} +export default function ProjectTypeChart({ data }) { + const items = data && data.items ? data.items : []; + const totalProjectCount = data ? Number(data.totalProjectCount) || 0 : 0; + const computedTotal = totalProjectCount || items.reduce((sum, item) => sum + (Number(item.count) || 0), 0); -export default function ProjectTypeChart({ data, total }) { - const totalValue = total || data.reduce((sum, item) => sum + item.value, 0); const option = useMemo(() => ({ - color: data.map(item => item.color), + color: COLORS.slice(0, items.length), tooltip: { trigger: 'item', backgroundColor: '#fff', - borderColor: '#e9edf4', + borderColor: '#e2e8f0', borderWidth: 1, - textStyle: { color: '#333', fontSize: 12 }, + textStyle: { color: '#1e293b', fontSize: 12 }, formatter: '{b}
项目数:{c}
占比:{d}%', }, series: [ { - name: '项目类型占比', + name: '评价类别占比', type: 'pie', - radius: ['46%', '74%'], - center: ['50%', '50%'], + radius: ['48%', '72%'], + center: ['50%', '48%'], avoidLabelOverlap: true, - minAngle: 5, label: { show: false }, labelLine: { show: false }, itemStyle: { borderColor: '#fff', borderWidth: 2, + borderRadius: 4, }, - emphasis: { - scale: true, - scaleSize: 4, - }, - data: data.map(item => ({ name: item.name, value: item.value })), + data: items.map((item) => ({ + name: item.evalTypeName, + value: Number(item.count) || 0, + })), }, ], graphic: [ { type: 'text', left: 'center', - top: '42%', + top: '38%', style: { text: '项目总数', textAlign: 'center', - fill: '#a0a7b2', - fontSize: 15, + fill: '#94a3b8', + fontSize: 13, fontWeight: 600, }, }, { type: 'text', left: 'center', - top: '53%', + top: '50%', style: { - text: String(totalValue), + text: String(computedTotal), textAlign: 'center', - fill: '#111827', - fontSize: 24, + fill: '#1e293b', + fontSize: 22, fontWeight: 700, }, }, ], - }), [data, totalValue]); + }), [items, computedTotal]); return ( - 项目类型占比} - size="small" - className="institution-panel-card institution-project-card" - styles={{ body: { padding: '24px 22px 18px' } }} - > + <> - - +
+ {items.map((item, i) => ( + + + {item.evalTypeName} {Number(item.count) || 0} + + ))} +
+ ); } diff --git a/src/pages/Container/Institution/components/StatisticCards.jsx b/src/pages/Container/Institution/components/StatisticCards.jsx index c52f6b5..a1b102b 100644 --- a/src/pages/Container/Institution/components/StatisticCards.jsx +++ b/src/pages/Container/Institution/components/StatisticCards.jsx @@ -1,39 +1,13 @@ import React from 'react'; -import { - AppstoreOutlined, - AuditOutlined, - CompassOutlined, - DatabaseOutlined, - FileDoneOutlined, - FileProtectOutlined, - LineChartOutlined, - NotificationOutlined, - ProfileOutlined, - ReadOutlined, - SafetyCertificateOutlined -} from '@ant-design/icons'; - -const iconMap = { - FileDone: FileDoneOutlined, - LineChart: LineChartOutlined, - Appstore: AppstoreOutlined, - FileProtect: FileProtectOutlined, - Safety: SafetyCertificateOutlined, - Profile: ProfileOutlined, - Notification: NotificationOutlined, - Read: ReadOutlined, - Compass: CompassOutlined, - Database: DatabaseOutlined, - Audit: AuditOutlined -}; function StatisticCard({ item }) { - const Icon = iconMap[item.icon] || FileDoneOutlined; - return (
-
- +
+ {item.char}
{item.title}
diff --git a/src/pages/Container/Institution/index.css b/src/pages/Container/Institution/index.css index 39a9dd9..c3c49cb 100644 --- a/src/pages/Container/Institution/index.css +++ b/src/pages/Container/Institution/index.css @@ -1,256 +1,408 @@ -.institution-dashboard { +/* ============ 机构端首页 — 1:1 还原原型 dashboard-classic ============ */ + +.institution-dashboard { min-height: 100%; - padding: 0 2px 0; - background: #f3f3f3; - color: #333; + background: #f8fafc; + color: #1e293b; + line-height: 1.6; } -.institution-dashboard__top, -.institution-dashboard__charts { +/* ============ dashboard-classic 布局 ============ */ +.dashboard-classic { display: grid; - grid-template-columns: minmax(0, 2.12fr) 397px; - gap: 20px; - margin-bottom: 15px; + gap: 0.85rem; } -.institution-panel-card { - border: 0 !important; - border-radius: 2px !important; - box-shadow: none !important; -} - -.institution-panel-card > :is(.ant-card-head, .micro-temp-card-head) { - min-height: 45px; - padding: 0 16px; - border-bottom: 0; -} - -.institution-panel-card > :is(.ant-card-head, .micro-temp-card-head) :is(.ant-card-head-title, .micro-temp-card-head-title) { - padding: 14px 0 8px; -} - -.institution-card-title { - font-size: 14px; - font-weight: 700; - color: #303133; -} - -.institution-dashboard__summary { - font-size: 13px; - color: #3d3d3d; -} - -.institution-dashboard__summary strong { - margin-left: 8px; - font-size: 13px; - font-weight: 500; -} - -.institution-dashboard__summary .is-blue { - color: #1684ff; -} - -.institution-stat-grid { +.dashboard-classic-top, +.dashboard-classic-middle { display: grid; - grid-template-columns: repeat(5, minmax(120px, 1fr)); - gap: 26px 28px; + grid-template-columns: minmax(0, 3.2fr) minmax(300px, 1fr); + gap: 0.85rem; } -.institution-stat-card { +/* ============ dashboard-block 卡片 ============ */ +.dashboard-block { + background: #fff; + border: 1px solid #e2e8f0; + padding: 0.95rem; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); +} + +.dashboard-block-head { + min-height: 28px; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.8rem; + margin-bottom: 0.75rem; +} + +.dashboard-block-head h3 { + font-size: 0.88rem; + margin: 0; + font-weight: 600; + color: #1e293b; +} + +.dashboard-block-head > div { + display: flex; + gap: 1.4rem; + font-size: 0.7rem; + color: #64748b; +} + +.dashboard-block-head b { + color: #2563eb; +} + +/* ============ 当前项目节点统计 ============ */ +.dashboard-status-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0.75rem; +} + +.dashboard-status-grid article { display: flex; align-items: center; - min-height: 56px; - padding: 10px 13px; + gap: 0.65rem; + min-height: 62px; + padding: 0.65rem; background: #fff; - border-radius: 4px; - box-shadow: 0 6px 17px rgba(33, 57, 98, 0.09); + box-shadow: 0 4px 16px rgba(30, 64, 100, 0.07); } -.institution-stat-card__icon, -.institution-alert-item__icon { +.dashboard-status-grid article > i { + width: 34px; + height: 34px; + border-radius: 4px; display: flex; align-items: center; justify-content: center; - flex: 0 0 auto; + color: #fff; + font-style: normal; + font-weight: 700; + font-size: 13px; + flex: 0 0 34px; +} + +.dashboard-status-grid .blue { background: #4098f7; } +.dashboard-status-grid .violet { background: #8268ef; } +.dashboard-status-grid .green { background: #23bfa5; } +.dashboard-status-grid .orange { background: #ff9f2f; } +.dashboard-status-grid .cyan { background: #0eafbd; } +.dashboard-status-grid .red { background: #f0645b; } + +.dashboard-status-grid span, +.dashboard-status-grid strong { + display: block; +} + +.dashboard-status-grid span { + font-size: 0.7rem; + color: #64748b; +} + +.dashboard-status-grid strong { + font-size: 1rem; + margin-top: 0.15rem; + color: #1e293b; +} + +/* ============ 通知提醒 ============ */ +.dashboard-reminder-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; +} + +.dashboard-reminder-grid article { + display: flex; + align-items: center; + gap: 0.65rem; + min-height: 64px; + padding: 0.65rem; + cursor: pointer; +} + +.dashboard-reminder-grid article:last-child { + grid-column: 1 / 2; +} + +.dashboard-reminder-grid article.yellow { background: #fff9e8; } +.dashboard-reminder-grid article.red { background: #fff2ef; } +.dashboard-reminder-grid article.blue { background: #edf4ff; } + +.dashboard-reminder-grid i { width: 34px; height: 34px; + display: flex; + align-items: center; + justify-content: center; border-radius: 5px; color: #fff; - font-size: 19px; -} - -.institution-stat-card__content { - min-width: 0; - margin-left: 12px; -} - -.institution-stat-card__title, -.institution-alert-item__title { - overflow: hidden; - color: #333; - font-size: 12px; + font-style: normal; font-weight: 700; - line-height: 18px; - text-overflow: ellipsis; + font-size: 13px; + flex: 0 0 34px; +} + +.dashboard-reminder-grid .yellow i { background: #f7bd08; } +.dashboard-reminder-grid .red i { background: #ff725c; } +.dashboard-reminder-grid .blue i { background: #5b8def; } + +.dashboard-reminder-grid span, +.dashboard-reminder-grid strong { + display: block; +} + +.dashboard-reminder-grid span { + font-size: 0.68rem; + color: #64748b; +} + +.dashboard-reminder-grid strong { + font-size: 1rem; + margin-top: 0.12rem; + color: #1e293b; +} + +/* ============ 服务行业项目统计 / 评价类别占比 ============ */ +.dashboard-industry-block, +.dashboard-type-block { + min-height: 285px; +} + +/* 图例 */ +.chart-legend span { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.chart-legend i { + width: 14px; + height: 6px; + border-radius: 5px; + background: #4285f4; +} + +.chart-legend span:last-child i { + background: #ffab31; +} + +/* 柱状图容器 */ +.dashboard-bar-chart { + height: 225px; +} + +/* ECharts 柱状图 */ +.institution-bar-chart { + width: 100%; + height: 225px; +} + +/* ============ 评价类别占比 ============ */ +.institution-donut-chart { + width: 100%; + height: 185px; + display: flex; + justify-content: center; + align-items: center; +} + +.dashboard-type-legend { + display: flex; + justify-content: center; + gap: 0.8rem; + flex-wrap: wrap; + font-size: 0.62rem; + color: #64748b; + margin-top: 8px; +} + +.dashboard-type-legend span { + display: flex; + align-items: center; +} + +.dashboard-type-legend i { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 50%; + margin-right: 0.25rem; +} + +/* ============ 项目执行情况表格 ============ */ +.dashboard-project-block .data-table { + background: #fff; + border: 0; + border-radius: 0; + box-shadow: none; + overflow: visible; +} + +.dashboard-project-table table { + width: 100%; + border-collapse: collapse; +} + +.dashboard-project-table th, +.dashboard-project-table td { + padding: 0.65rem 0.55rem; + font-size: 0.7rem; +} + +.dashboard-project-table th { + text-align: left; + font-weight: 600; + color: #64748b; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; white-space: nowrap; } -.institution-stat-card__value { - color: #333; - font-size: 16px; - font-weight: 500; - line-height: 21px; +.dashboard-project-table td { + border-bottom: 1px solid #e2e8f0; + color: #1e293b; } -.institution-alert-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px 18px; +.dashboard-project-table tr:last-child td { + border-bottom: none; } -.institution-alert-item { - display: flex; - align-items: center; - min-height: 66px; - padding: 12px 16px; - border-radius: 2px; +.dashboard-project-table tr:hover td { + background: #f8fafc; } -.institution-alert-item__icon { - width: 34px; - height: 34px; - box-shadow: 0 5px 12px rgba(75, 91, 131, 0.2); -} - -.institution-alert-item__content { - min-width: 0; - margin-left: 13px; -} - -.institution-alert-item__value { - color: #222; - font-size: 17px; - font-weight: 700; - line-height: 22px; -} - -.institution-enterprise-card, -.institution-project-card { - height: 300px; -} - -.institution-chart-legend, -.institution-chart-legend span { - display: flex; - align-items: center; -} - -.institution-chart-legend { - gap: 10px; - color: #6f7785; - font-size: 12px; -} - -.institution-chart-legend i, -.institution-project-legend i { - display: inline-block; - flex: 0 0 auto; - width: 20px; - height: 8px; - margin-right: 5px; - border-radius: 4px; -} - -.institution-bar-chart { - width: 100%; - height: 232px; -} - -.institution-project-card :is(.ant-card-body, .micro-temp-card-body) { - text-align: center; -} - -.institution-donut-chart { - width: 218px; - height: 218px; - margin: -1px auto 4px; -} - -.institution-project-legend { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 10px 16px; - color: #697386; - font-size: 12px; - line-height: 18px; -} - -.institution-project-legend span { +/* ============ 原生按钮(匹配原型) ============ */ +.btn { display: inline-flex; align-items: center; + justify-content: center; + gap: 0.4rem; + padding: 0.42rem 0.85rem; + border: 1px solid var(--color-border, #e2e8f0); + border-radius: 8px; + font-size: 0.78rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + background: #fff; + color: #475569; + text-decoration: none; } -.institution-project-legend i { - width: 7px; - height: 7px; - margin-right: 6px; - border-radius: 50%; +.btn-primary { + background: #2563eb; + color: #fff; + border-color: #2563eb; +} +.btn-primary:hover { background: #1d4ed8; border-color: #1d4ed8; } + +.btn-ghost { + background: transparent; + color: #64748b; + border: none; +} +.btn-ghost:hover { background: #f1f5f9; color: #1e293b; } + +.btn-sm { padding: 0.3rem 0.75rem; font-size: 0.7rem; } + +.dashboard-btn-ghost { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.3rem 0.75rem; + border: none; + border-radius: 8px; + font-size: 0.7rem; + font-weight: 500; + cursor: pointer; + background: transparent; + color: #64748b; + transition: all 0.15s ease; +} +.dashboard-btn-ghost:hover { + background: #f1f5f9; + color: #1e293b; } -.institution-table-card { - min-height: 202px; +/* 表格操作按钮 */ +.dashboard-table-link { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.3rem 0.75rem; + border: none; + border-radius: 8px; + font-size: 0.7rem; + font-weight: 500; + cursor: pointer; + text-decoration: none; + background: #2563eb; + color: #fff; + transition: all 0.15s ease; } -.institution-completion-table :is(.ant-table, .micro-temp-table) { - color: #222; - font-size: 12px; +.dashboard-table-link:hover { + background: #1d4ed8; + text-decoration: none; + color: #fff; } -.institution-completion-table :is(.ant-table-thead, .micro-temp-table-thead) > tr > th { - height: 27px; - padding: 6px 10px; - background: #eef2f8 !important; - border-bottom: 0; - color: #333; - font-size: 12px; - font-weight: 700; +/* ============ 骨架占位(加载态) ============ */ +.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; } -.institution-completion-table :is(.ant-table-tbody, .micro-temp-table-tbody) > tr > td { - height: 29px; - padding: 6px 10px; - border-bottom: 0; +@keyframes sk-shimmer { + 0% { background-position: 100% 0; } + 100% { background-position: 0 0; } } -.institution-completion-table :is(.ant-table-tbody, .micro-temp-table-tbody) > tr:nth-child(even) > td { - background: #eaf2ff; +/* 图表占位块 */ +.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; } -.institution-completion-table :is(.ant-table-tbody, .micro-temp-table-tbody) > tr:hover > td { - background: #dfeeff !important; +/* 表格单元格骨架 */ +.sk-cell { + width: 70%; + height: 12px; + border-radius: 4px; } +.sk-cell--lg { width: 85%; } +.sk-cell--sm { width: 36px; } -.institution-table-link { - color: #1684ff; - font-size: 12px; -} - -@media (max-width: 1180px) { - .institution-dashboard__top, - .institution-dashboard__charts { +/* ============ 响应式 ============ */ +@media (max-width: 1280px) { + .dashboard-classic-top, + .dashboard-classic-middle { grid-template-columns: 1fr; } - .institution-stat-grid { - grid-template-columns: repeat(3, minmax(120px, 1fr)); + .dashboard-status-grid { + grid-template-columns: repeat(2, 1fr); } } @media (max-width: 760px) { - .institution-stat-grid, - .institution-alert-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); + .dashboard-status-grid { + grid-template-columns: repeat(2, 1fr); + } + .dashboard-reminder-grid { + grid-template-columns: 1fr; } } - diff --git a/src/pages/Container/Institution/index.js b/src/pages/Container/Institution/index.js index d850f42..cb01d19 100644 --- a/src/pages/Container/Institution/index.js +++ b/src/pages/Container/Institution/index.js @@ -1,8 +1,4 @@ -import React from 'react'; -import { Space, Card } from 'antd'; - -import StatisticCards from './components/StatisticCards'; -import InfoAlerts from './components/InfoAlerts'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import EnterpriseTypeChart from './components/EnterpriseTypeChart'; import ProjectTypeChart from './components/ProjectTypeChart'; import ProjectCompletionTable from './components/ProjectCompletionTable'; @@ -10,53 +6,197 @@ import './index.css'; import { STATISTIC_CARDS, - INFO_ALERTS, - PROJECT_SUMMARY, - ENTERPRISE_TYPE_STATS, - PROJECT_TYPE_DISTRIBUTION, - PROJECT_TOTAL, - PROJECT_COMPLETION_LIST } from './mockData'; +import { + fetchProjectNodeStats, + fetchNotices, + fetchIndustryStat, + fetchEvalTypeRatio, + fetchProjectExecution, +} from '~/api/institution'; + +// ==================== 骨架占位 ==================== +function Skeleton({ width = '100%', height = 14, radius = 4, className = '' }) { + return ( +