新增:监管端首页、驾驶舱;机构端首页,资料填报。
parent
e96c72adb5
commit
2af100fc98
|
|
@ -31,6 +31,7 @@
|
|||
"ahooks": "^3.9.5",
|
||||
"antd": "^5.11.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"echarts": "^6.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^18.2.0",
|
||||
|
|
@ -44,6 +45,7 @@
|
|||
"@babel/preset-react": "^7.29.7",
|
||||
"@cqsjjb/scripts": "latest",
|
||||
"@eslint-react/eslint-plugin": "^2.2.2",
|
||||
"ajv": "^8.20.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.8.1",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* 企业信息模块初始化与下拉数据拉取(绕过 DVA,避免 loading 状态导致页面重渲染死循环)
|
||||
/**
|
||||
* 浼佷笟淇℃伅妯″潡鍒濆鍖栦笌涓嬫媺鏁版嵁鎷夊彇锛堢粫杩?DVA锛岄伩鍏?loading 鐘舵€佸鑷撮〉闈㈤噸娓叉煋姝诲惊鐜級
|
||||
*/
|
||||
|
||||
import {
|
||||
|
|
@ -13,15 +13,16 @@ import {
|
|||
import { apiGet } from "./http";
|
||||
import { clearOrgInfoId, getOrgInfoId, setOrgInfoId } from "./orgContext";
|
||||
|
||||
/** 机构信息管理页路径(无机构数据时唯一可访问页) */
|
||||
/** 鏈烘瀯淇℃伅绠$悊椤佃矾寰勶紙鏃犳満鏋勬暟鎹椂鍞竴鍙闂〉锛?*/
|
||||
export const ORG_INFO_PAGE_PATH = "/certificate/container/EnterpriseInfo/OrgInfo";
|
||||
export const MATERIAL_REPORT_PAGE_PATH = "/certificate/container/EnterpriseInfo/MaterialReport";
|
||||
|
||||
let ensureOrgPromise = null;
|
||||
/** 最近一次 getInfo 转换结果,供机构信息页复用,避免重复请求 */
|
||||
/** 鏈€杩戜竴娆?getInfo 杞崲缁撴灉锛屼緵鏈烘瀯淇℃伅椤靛鐢紝閬垮厤閲嶅璇锋眰 */
|
||||
let lastOrgInfoDetail = null;
|
||||
|
||||
export function isOrgInfoPage(path = window.location.pathname) {
|
||||
return path === ORG_INFO_PAGE_PATH || path.startsWith(`${ORG_INFO_PAGE_PATH}/`);
|
||||
return path === ORG_INFO_PAGE_PATH || path.startsWith(`${ORG_INFO_PAGE_PATH}/`) || path === MATERIAL_REPORT_PAGE_PATH || path.startsWith(`${MATERIAL_REPORT_PAGE_PATH}/`);
|
||||
}
|
||||
|
||||
export function getOrgInfoDetail() {
|
||||
|
|
@ -53,8 +54,8 @@ function fetchOrgInfoContext() {
|
|||
}
|
||||
|
||||
/**
|
||||
* 确保已缓存机构 id(子表分页/请求头 orgInfoId 依赖此值)
|
||||
* @param {{ force?: boolean }} options force=true 时忽略缓存并重新 getInfo(进入企业信息模块时使用)
|
||||
* 纭繚宸茬紦瀛樻満鏋?id锛堝瓙琛ㄥ垎椤?璇锋眰澶?orgInfoId 渚濊禆姝ゅ€硷級
|
||||
* @param {{ force?: boolean }} options force=true 鏃跺拷鐣ョ紦瀛樺苟閲嶆柊 getInfo锛堣繘鍏ヤ紒涓氫俊鎭ā鍧楁椂浣跨敤锛?
|
||||
* @returns {Promise<{ hasOrg: boolean, orgInfoId: string|null, detail?: object, networkError?: boolean }>}
|
||||
*/
|
||||
export function ensureOrgContext(options = {}) {
|
||||
|
|
@ -93,3 +94,5 @@ export async function fetchOrgPositionPage(params = {}) {
|
|||
const res = await apiGet("/safety-eval/org-position/page", query);
|
||||
return fromPageResponse(res, toPositionForm);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,401 @@
|
|||
.material-report-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
color: #1f2329;
|
||||
font-family: PingFangSC-Regular, "Microsoft YaHei", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.material-report-header {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.material-report-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
padding-left: 16px;
|
||||
color: #141414;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.material-report-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin-right: 10px;
|
||||
color: #1e86ff;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.material-report-hero {
|
||||
position: relative;
|
||||
height: 244px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(235, 242, 253, 0.98) 0%, rgba(238, 244, 255, 0.95) 55%, rgba(222, 234, 253, 0.96) 100%);
|
||||
}
|
||||
|
||||
.material-report-hero::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(144deg, transparent 0 22%, rgba(255, 255, 255, 0.62) 22.4% 23.5%, transparent 24%),
|
||||
linear-gradient(28deg, transparent 0 60%, rgba(255, 255, 255, 0.72) 60.2% 61.2%, transparent 61.4%);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.material-report-hero h1 {
|
||||
position: absolute;
|
||||
left: 26%;
|
||||
top: 101px;
|
||||
margin: 0;
|
||||
color: #1f2329;
|
||||
font-size: 34px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.material-report-hero-art {
|
||||
position: absolute;
|
||||
right: 9%;
|
||||
top: 20px;
|
||||
width: 350px;
|
||||
height: 210px;
|
||||
}
|
||||
|
||||
.verify-platform {
|
||||
position: absolute;
|
||||
right: 44px;
|
||||
top: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 124px;
|
||||
height: 124px;
|
||||
border-radius: 25px;
|
||||
color: #fff;
|
||||
font-size: 72px;
|
||||
background: linear-gradient(135deg, #2e74ff, #1d44de);
|
||||
box-shadow: 0 16px 26px rgba(49, 116, 255, 0.28), inset 0 0 0 8px rgba(255, 255, 255, 0.18);
|
||||
transform: rotate(1deg);
|
||||
}
|
||||
|
||||
.verify-platform::after {
|
||||
position: absolute;
|
||||
left: -18px;
|
||||
right: -18px;
|
||||
bottom: -30px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 8px solid rgba(77, 151, 255, 0.3);
|
||||
border-top-color: transparent;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.verify-card {
|
||||
position: absolute;
|
||||
left: 76px;
|
||||
top: 123px;
|
||||
width: 72px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #418eff, #dfeeff);
|
||||
box-shadow: 0 12px 28px rgba(52, 116, 220, 0.2);
|
||||
}
|
||||
|
||||
.verify-card::before,
|
||||
.verify-card::after {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
height: 6px;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.verify-card::before { top: 16px; }
|
||||
.verify-card::after { top: 31px; width: 24px; }
|
||||
|
||||
.material-report-page--step-1 .material-report-steps {
|
||||
margin-top: 42px;
|
||||
}
|
||||
|
||||
.material-report-steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 58px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.material-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #222;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.material-step__dot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: #cfd7df;
|
||||
}
|
||||
|
||||
.material-step.is-active .material-step__dot {
|
||||
background: #1e8df8;
|
||||
}
|
||||
|
||||
.material-step__line {
|
||||
width: 130px;
|
||||
height: 1px;
|
||||
margin: 0 16px;
|
||||
background: #dcdfe6;
|
||||
}
|
||||
|
||||
.material-fill-step {
|
||||
width: 1010px;
|
||||
margin: 24px auto 0;
|
||||
}
|
||||
|
||||
.material-form-grid {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.material-form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(24, 1fr);
|
||||
column-gap: 18px;
|
||||
min-height: 33px;
|
||||
}
|
||||
|
||||
.material-form-field--24 {
|
||||
grid-column: span 24;
|
||||
}
|
||||
|
||||
.material-form-field--8 {
|
||||
grid-column: span 8;
|
||||
}
|
||||
|
||||
.material-form-field :is(.ant-form-item, .micro-temp-form-item) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.material-form-field :is(.ant-form-item-row, .micro-temp-form-item-row) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.material-form-field :is(.ant-form-item-label, .micro-temp-form-item-label) {
|
||||
width: 128px;
|
||||
flex: 0 0 128px;
|
||||
padding-right: 10px;
|
||||
line-height: 22px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.material-form-field--24 :is(.ant-form-item-label, .micro-temp-form-item-label) {
|
||||
width: 128px;
|
||||
flex-basis: 128px;
|
||||
}
|
||||
|
||||
.material-form-field :is(.ant-form-item-label, .micro-temp-form-item-label) > label {
|
||||
height: auto;
|
||||
color: #4e5969;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.material-form-field :is(.ant-form-item-control, .micro-temp-form-item-control) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.material-form-field :is(.ant-input, .micro-temp-input) {
|
||||
height: 24px;
|
||||
border-color: #d9dee8;
|
||||
border-radius: 0;
|
||||
color: #111;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.material-form-actions {
|
||||
margin-top: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.material-form-actions :is(.ant-btn, .micro-temp-btn) {
|
||||
min-width: 45px;
|
||||
height: 25px;
|
||||
padding: 0 14px;
|
||||
border-radius: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.material-save-btn {
|
||||
border-color: #06ad36 !important;
|
||||
background: #08b83e !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.material-result-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 68px;
|
||||
color: #111;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.review-loading-icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 118px;
|
||||
height: 118px;
|
||||
margin-bottom: 28px;
|
||||
border-radius: 50%;
|
||||
background: #3479f7;
|
||||
box-shadow: inset 0 0 0 5px #fff, 0 0 0 4px #6da0ff;
|
||||
}
|
||||
|
||||
.review-loading-ring {
|
||||
position: absolute;
|
||||
inset: -8px;
|
||||
border: 2px solid rgba(42, 121, 255, 0.25);
|
||||
border-top-color: #2f7dff;
|
||||
border-radius: 50%;
|
||||
animation: material-spin 1.1s linear infinite;
|
||||
}
|
||||
|
||||
.review-loading-icon > :is(.anticon, .micro-tempicon) {
|
||||
color: #fff;
|
||||
font-size: 55px;
|
||||
}
|
||||
|
||||
.review-loading-icon span {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 24px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 4px solid #8bddec;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transform: rotate(-42deg);
|
||||
}
|
||||
|
||||
.review-loading-icon span::after {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
bottom: -7px;
|
||||
width: 16px;
|
||||
height: 4px;
|
||||
border-radius: 3px;
|
||||
background: #9a6b36;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.review-success-illustration {
|
||||
position: relative;
|
||||
width: 190px;
|
||||
height: 160px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.review-success-illustration::before {
|
||||
position: absolute;
|
||||
left: 30px;
|
||||
top: 30px;
|
||||
width: 128px;
|
||||
height: 102px;
|
||||
border-radius: 45% 55% 50% 50%;
|
||||
background: #dfeaff;
|
||||
transform: rotate(-12deg);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.success-paper {
|
||||
position: absolute;
|
||||
left: 61px;
|
||||
top: 17px;
|
||||
width: 78px;
|
||||
height: 104px;
|
||||
border: 8px solid #77a7ff;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
box-shadow: 0 -14px 0 -7px #77a7ff;
|
||||
}
|
||||
|
||||
.success-paper i {
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 6px;
|
||||
margin: 13px auto 0;
|
||||
border-radius: 5px;
|
||||
background: #81aafa;
|
||||
}
|
||||
|
||||
.review-success-illustration > :is(.anticon, .micro-tempicon) {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 66px;
|
||||
color: #3479f7;
|
||||
font-size: 52px;
|
||||
border: 4px solid #fff;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.material-success-wrap {
|
||||
margin-top: 66px;
|
||||
}
|
||||
|
||||
@keyframes material-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.material-fill-step {
|
||||
width: calc(100% - 56px);
|
||||
}
|
||||
|
||||
.material-form-row {
|
||||
grid-template-columns: repeat(16, 1fr);
|
||||
}
|
||||
|
||||
.material-form-field--8 {
|
||||
grid-column: span 8;
|
||||
}
|
||||
|
||||
.material-report-hero h1 {
|
||||
left: 18%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.material-report-hero-art {
|
||||
opacity: 0.35;
|
||||
right: -40px;
|
||||
}
|
||||
|
||||
.material-step__line {
|
||||
width: 46px;
|
||||
}
|
||||
|
||||
.material-form-row {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Button, Form, Input, message, Space } from 'antd';
|
||||
import {
|
||||
CheckCircleFilled,
|
||||
FileSearchOutlined,
|
||||
LoadingOutlined,
|
||||
SafetyCertificateFilled
|
||||
} from '@ant-design/icons';
|
||||
import './index.css';
|
||||
import { materialFields, materialInitialValues, mockSubmitMaterial } from './mockData';
|
||||
|
||||
const steps = [
|
||||
{ key: 1, title: '填写信息' },
|
||||
{ key: 2, title: '认证审核中' },
|
||||
{ key: 3, title: '认证通过' }
|
||||
];
|
||||
|
||||
function PageHeader() {
|
||||
return (
|
||||
<div className="material-report-header">
|
||||
<div className="material-report-brand">
|
||||
<span className="material-report-logo"><SafetyCertificateFilled /></span>
|
||||
<span>巫溪县安全评价在线监管一件事</span>
|
||||
</div>
|
||||
<div className="material-report-hero">
|
||||
<h1>认证信息</h1>
|
||||
<div className="material-report-hero-art">
|
||||
<div className="verify-platform"><CheckCircleFilled /></div>
|
||||
<div className="verify-card" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepHeader({ current }) {
|
||||
return (
|
||||
<div className="material-report-steps">
|
||||
{steps.map((step, index) => {
|
||||
const active = current >= step.key;
|
||||
const currentStep = current === step.key;
|
||||
return (
|
||||
<div key={step.key} className={`material-step ${active ? 'is-active' : ''} ${currentStep ? 'is-current' : ''}`}>
|
||||
<span className="material-step__dot">{step.key}</span>
|
||||
<span className="material-step__title">{step.title}</span>
|
||||
{index < steps.length - 1 && <i className="material-step__line" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldItem({ field }) {
|
||||
const labelLines = field.label.split('\n');
|
||||
return (
|
||||
<div className={`material-form-field material-form-field--${field.span}`}>
|
||||
<Form.Item
|
||||
name={field.name}
|
||||
label={(
|
||||
<span>
|
||||
{labelLines.map((line) => <React.Fragment key={line}>{line}<br /></React.Fragment>)}
|
||||
</span>
|
||||
)}
|
||||
rules={field.span === 24 ? [{ required: true, message: `请输入${labelLines[0]}` }] : undefined}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FillStep({ onSubmitted }) {
|
||||
const [form] = Form.useForm();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const groupedRows = useMemo(() => {
|
||||
const rows = [];
|
||||
let current = [];
|
||||
let used = 0;
|
||||
materialFields.forEach((field) => {
|
||||
if (field.span === 24) {
|
||||
if (current.length) rows.push(current);
|
||||
rows.push([field]);
|
||||
current = [];
|
||||
used = 0;
|
||||
return;
|
||||
}
|
||||
if (used + field.span > 24) {
|
||||
rows.push(current);
|
||||
current = [];
|
||||
used = 0;
|
||||
}
|
||||
current.push(field);
|
||||
used += field.span;
|
||||
});
|
||||
if (current.length) rows.push(current);
|
||||
return rows;
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
const res = await mockSubmitMaterial(values);
|
||||
if (res?.success) {
|
||||
message.success('提交成功,正在进入认证审核');
|
||||
onSubmitted(res.data);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (!err?.errorFields) {
|
||||
message.error('提交失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="material-fill-step">
|
||||
<Form form={form} initialValues={materialInitialValues} colon={false} labelAlign="right">
|
||||
<div className="material-form-grid">
|
||||
{groupedRows.map((row, index) => (
|
||||
<div key={index} className="material-form-row">
|
||||
{row.map((field) => <FieldItem key={field.name} field={field} />)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Form>
|
||||
<div className="material-form-actions">
|
||||
<Space size={10}>
|
||||
<Button>取消</Button>
|
||||
<Button className="material-save-btn">保存</Button>
|
||||
<Button type="primary" loading={submitting} onClick={handleSubmit}>提交</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewLoadingStep() {
|
||||
return (
|
||||
<div className="material-result-wrap">
|
||||
<div className="review-loading-icon">
|
||||
<div className="review-loading-ring" />
|
||||
<FileSearchOutlined />
|
||||
<span />
|
||||
</div>
|
||||
<p>认证审核中,请耐心等待...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewSuccessStep() {
|
||||
return (
|
||||
<div className="material-result-wrap material-success-wrap">
|
||||
<div className="review-success-illustration">
|
||||
<div className="success-paper">
|
||||
<i /><i /><i /><i />
|
||||
</div>
|
||||
<CheckCircleFilled />
|
||||
</div>
|
||||
<p>审核通过</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MaterialReportPage() {
|
||||
const [current, setCurrent] = useState(1);
|
||||
const timerRef = useRef(null);
|
||||
|
||||
useEffect(() => () => clearTimeout(timerRef.current), []);
|
||||
|
||||
const handleSubmitted = () => {
|
||||
setCurrent(2);
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => {
|
||||
setCurrent(3);
|
||||
}, 2300);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`material-report-page material-report-page--step-${current}`}>
|
||||
<PageHeader />
|
||||
<StepHeader current={current} />
|
||||
{current === 1 && <FillStep onSubmitted={handleSubmitted} />}
|
||||
{current === 2 && <ReviewLoadingStep />}
|
||||
{current === 3 && <ReviewSuccessStep />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
export const materialInitialValues = {
|
||||
orgName: '重庆市安全生产科学研究有限公司',
|
||||
creditCode: '915001072028699512',
|
||||
registerAddress: '重庆市沙坪坝区大学城东路20号重庆科技学院第39栋西南角实习用房三楼',
|
||||
businessAddress: '重庆市沙坪坝区大学城东路20号重庆科技学院第39栋西南角实习用房三楼',
|
||||
longitude: '119.460574',
|
||||
latitude: '119.460574',
|
||||
county: '重庆市沙坪坝区',
|
||||
street: '大学城东路20号重庆科技学院第39栋',
|
||||
community: '大学城东路',
|
||||
gbIndustryCode: '915001072028699512',
|
||||
safetyIndustryCategory: '专业技术服务业',
|
||||
ownershipType: '冶金工贸',
|
||||
legalRepresentative: '郑远平',
|
||||
legalRepPhone: '023-68705577',
|
||||
principal: '郑远平',
|
||||
principalPhone: '023-68705577',
|
||||
safetyDeptHead: '',
|
||||
safetyDeptPhone: '023-68705577',
|
||||
safetyVp: '',
|
||||
safetyVpPhone: '023-68705577',
|
||||
productionDate: '',
|
||||
businessStatus: '开业',
|
||||
disclosureUrl: '',
|
||||
workplaceArea: '',
|
||||
archiveRoomArea: '',
|
||||
fullTimeEvaluatorCount: '',
|
||||
registeredSafetyEngineerCount: ''
|
||||
};
|
||||
|
||||
export const materialFields = [
|
||||
{ name: 'orgName', label: '生产经营单位名称', span: 24 },
|
||||
{ name: 'creditCode', label: '统一社会信用代码', span: 24 },
|
||||
{ name: 'registerAddress', label: '注册地址', span: 24 },
|
||||
{ name: 'businessAddress', label: '经营地址', span: 24 },
|
||||
{ name: 'longitude', label: '所在地坐标 经度', span: 8 },
|
||||
{ name: 'latitude', label: '所在地坐标 纬度', span: 8 },
|
||||
{ name: 'county', label: '所属(县、区)', span: 8 },
|
||||
{ name: 'street', label: '所属镇、街道', span: 8 },
|
||||
{ name: 'community', label: '属村(社区)', span: 8 },
|
||||
{ name: 'gbIndustryCode', label: '国民经济行业分类\n(GB/T4754-2017)', span: 8 },
|
||||
{ name: 'safetyIndustryCategory', label: '安全生产监管行业类别', span: 8 },
|
||||
{ name: 'ownershipType', label: '归属类型', span: 8 },
|
||||
{ name: 'legalRepresentative', label: '法定代表人', span: 8 },
|
||||
{ name: 'legalRepPhone', label: '联系电话', span: 8 },
|
||||
{ name: 'principal', label: '主要负责人', span: 8 },
|
||||
{ name: 'principalPhone', label: '联系电话', span: 8 },
|
||||
{ name: 'safetyDeptHead', label: '安全管理部门负责人', span: 8 },
|
||||
{ name: 'safetyDeptPhone', label: '联系电话', span: 8 },
|
||||
{ name: 'safetyVp', label: '主管安全副总', span: 8 },
|
||||
{ name: 'safetyVpPhone', label: '联系电话', span: 8 },
|
||||
{ name: 'productionDate', label: '投产日期', span: 8 },
|
||||
{ name: 'businessStatus', label: '企业经营状态', span: 8 },
|
||||
{ name: 'disclosureUrl', label: '信息公开网址', span: 8 },
|
||||
{ name: 'workplaceArea', label: '工作场所建筑面积', span: 8 },
|
||||
{ name: 'archiveRoomArea', label: '档案室面积', span: 8 },
|
||||
{ name: 'fullTimeEvaluatorCount', label: '专职安全评价师数量', span: 8 },
|
||||
{ name: 'registeredSafetyEngineerCount', label: '注册安全工程师数量', span: 8 }
|
||||
];
|
||||
|
||||
export function mockSubmitMaterial(values) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ success: true, data: { id: Date.now(), ...values } });
|
||||
}, 900);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import InstitutionDashboard from '../Index';
|
||||
|
||||
export default InstitutionDashboard;
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import * as echarts from 'echarts';
|
||||
import { Card } from 'antd';
|
||||
|
||||
const BLUE = '#5b8ff9';
|
||||
const ORANGE = '#ffb33e';
|
||||
|
||||
function EChart({ option, className }) {
|
||||
const chartRef = useRef(null);
|
||||
const instanceRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return undefined;
|
||||
instanceRef.current = echarts.init(chartRef.current, null, { renderer: 'svg' });
|
||||
const resize = () => instanceRef.current && instanceRef.current.resize();
|
||||
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();
|
||||
if (instanceRef.current) instanceRef.current.dispose();
|
||||
instanceRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (instanceRef.current) instanceRef.current.setOption(option, true);
|
||||
}, [option]);
|
||||
|
||||
return <div className={className} ref={chartRef} />;
|
||||
}
|
||||
|
||||
export default function EnterpriseTypeChart({ data }) {
|
||||
const option = useMemo(() => ({
|
||||
color: [BLUE, ORANGE],
|
||||
grid: { left: 38, right: 10, top: 26, bottom: 46 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(91, 143, 249, .08)' } },
|
||||
backgroundColor: '#fff',
|
||||
borderColor: '#e9edf4',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333', fontSize: 12 },
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.categories,
|
||||
axisTick: { show: false },
|
||||
axisLine: { lineStyle: { color: '#edf0f5' } },
|
||||
axisLabel: {
|
||||
color: '#666',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
width: 58,
|
||||
overflow: 'break',
|
||||
lineHeight: 14,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 60,
|
||||
splitNumber: 6,
|
||||
axisLabel: { color: '#8792a2', fontSize: 12 },
|
||||
splitLine: { lineStyle: { color: '#e9edf4', 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] } },
|
||||
],
|
||||
}), [data]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={<span className="institution-card-title">服务企业类型统计</span>}
|
||||
size="small"
|
||||
className="institution-panel-card institution-enterprise-card"
|
||||
styles={{ body: { padding: '20px 26px 15px' } }}
|
||||
extra={
|
||||
<div className="institution-chart-legend institution-chart-legend--top">
|
||||
<span><i style={{ backgroundColor: BLUE }} />项目数</span>
|
||||
<span><i style={{ backgroundColor: ORANGE }} />金额</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<EChart className="institution-bar-chart" option={option} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
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 (
|
||||
<div className="institution-alert-item" style={{ backgroundColor: item.bgColor }}>
|
||||
<div className="institution-alert-item__icon" style={{ backgroundColor: item.color }}>
|
||||
<Icon />
|
||||
</div>
|
||||
<div className="institution-alert-item__content">
|
||||
<div className="institution-alert-item__title">{item.title}</div>
|
||||
<div className="institution-alert-item__value">{item.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InfoAlerts({ data }) {
|
||||
return (
|
||||
<div className="institution-alert-grid">
|
||||
{data.map((item) => (
|
||||
<AlertItem key={item.key} item={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
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: () => <a className="institution-table-link">查看详情</a> }
|
||||
];
|
||||
|
||||
export default function ProjectCompletionTable({ data }) {
|
||||
return (
|
||||
<Card
|
||||
title={<span className="institution-card-title">项目完成情况统计</span>}
|
||||
size="small"
|
||||
className="institution-panel-card institution-table-card"
|
||||
styles={{ body: { padding: '12px 14px 14px' } }}
|
||||
>
|
||||
<Table
|
||||
className="institution-completion-table"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: 1180 }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import * as echarts from 'echarts';
|
||||
import { Card } from 'antd';
|
||||
|
||||
function EChart({ option, className }) {
|
||||
const chartRef = useRef(null);
|
||||
const instanceRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return undefined;
|
||||
instanceRef.current = echarts.init(chartRef.current, null, { renderer: 'svg' });
|
||||
const resize = () => instanceRef.current && instanceRef.current.resize();
|
||||
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();
|
||||
if (instanceRef.current) instanceRef.current.dispose();
|
||||
instanceRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (instanceRef.current) instanceRef.current.setOption(option, true);
|
||||
}, [option]);
|
||||
|
||||
return <div className={className} ref={chartRef} />;
|
||||
}
|
||||
|
||||
function Legend({ data }) {
|
||||
return (
|
||||
<div className="institution-project-legend">
|
||||
{data.map((item) => (
|
||||
<span key={item.name}>
|
||||
<i style={{ backgroundColor: item.color }} />
|
||||
{item.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: '#fff',
|
||||
borderColor: '#e9edf4',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333', fontSize: 12 },
|
||||
formatter: '{b}<br/>项目数:{c}<br/>占比:{d}%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '项目类型占比',
|
||||
type: 'pie',
|
||||
radius: ['46%', '74%'],
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
minAngle: 5,
|
||||
label: { show: false },
|
||||
labelLine: { show: false },
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
emphasis: {
|
||||
scale: true,
|
||||
scaleSize: 4,
|
||||
},
|
||||
data: data.map(item => ({ name: item.name, value: item.value })),
|
||||
},
|
||||
],
|
||||
graphic: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '42%',
|
||||
style: {
|
||||
text: '项目总数',
|
||||
textAlign: 'center',
|
||||
fill: '#a0a7b2',
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '53%',
|
||||
style: {
|
||||
text: String(totalValue),
|
||||
textAlign: 'center',
|
||||
fill: '#111827',
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
},
|
||||
},
|
||||
],
|
||||
}), [data, totalValue]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={<span className="institution-card-title">项目类型占比</span>}
|
||||
size="small"
|
||||
className="institution-panel-card institution-project-card"
|
||||
styles={{ body: { padding: '24px 22px 18px' } }}
|
||||
>
|
||||
<EChart className="institution-donut-chart" option={option} />
|
||||
<Legend data={data} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
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 (
|
||||
<div className="institution-stat-card">
|
||||
<div className="institution-stat-card__icon" style={{ backgroundColor: item.bgColor }}>
|
||||
<Icon />
|
||||
</div>
|
||||
<div className="institution-stat-card__content">
|
||||
<div className="institution-stat-card__title">{item.title}</div>
|
||||
<div className="institution-stat-card__value">{item.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StatisticCards({ data }) {
|
||||
return (
|
||||
<div className="institution-stat-grid">
|
||||
{data.map((item) => (
|
||||
<StatisticCard key={item.key} item={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
.institution-dashboard {
|
||||
min-height: 100%;
|
||||
padding: 0 2px 0;
|
||||
background: #f3f3f3;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.institution-dashboard__top,
|
||||
.institution-dashboard__charts {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2.12fr) 397px;
|
||||
gap: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(120px, 1fr));
|
||||
gap: 26px 28px;
|
||||
}
|
||||
|
||||
.institution-stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
padding: 10px 13px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 17px rgba(33, 57, 98, 0.09);
|
||||
}
|
||||
|
||||
.institution-stat-card__icon,
|
||||
.institution-alert-item__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
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-weight: 700;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.institution-stat-card__value {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.institution-alert-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 18px;
|
||||
}
|
||||
|
||||
.institution-alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 66px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.institution-project-legend i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.institution-table-card {
|
||||
min-height: 202px;
|
||||
}
|
||||
|
||||
.institution-completion-table :is(.ant-table, .micro-temp-table) {
|
||||
color: #222;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.institution-completion-table :is(.ant-table-tbody, .micro-temp-table-tbody) > tr > td {
|
||||
height: 29px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.institution-completion-table :is(.ant-table-tbody, .micro-temp-table-tbody) > tr:nth-child(even) > td {
|
||||
background: #eaf2ff;
|
||||
}
|
||||
|
||||
.institution-completion-table :is(.ant-table-tbody, .micro-temp-table-tbody) > tr:hover > td {
|
||||
background: #dfeeff !important;
|
||||
}
|
||||
|
||||
.institution-table-link {
|
||||
color: #1684ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.institution-dashboard__top,
|
||||
.institution-dashboard__charts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.institution-stat-grid {
|
||||
grid-template-columns: repeat(3, minmax(120px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.institution-stat-grid,
|
||||
.institution-alert-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import React from 'react';
|
||||
import { Space, Card } from 'antd';
|
||||
|
||||
import StatisticCards from './components/StatisticCards';
|
||||
import InfoAlerts from './components/InfoAlerts';
|
||||
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,
|
||||
PROJECT_COMPLETION_LIST
|
||||
} from './mockData';
|
||||
|
||||
export default function InstitutionDashboard() {
|
||||
return (
|
||||
<div className="institution-dashboard">
|
||||
<div className="institution-dashboard__top">
|
||||
<Card
|
||||
title={<span className="institution-card-title">当前评价状态状态统计</span>}
|
||||
size="small"
|
||||
className="institution-dashboard__status-card institution-panel-card"
|
||||
extra={
|
||||
<Space size={30} className="institution-dashboard__summary">
|
||||
<span>
|
||||
项目总数: <strong className="is-blue">{PROJECT_SUMMARY.total}</strong>
|
||||
</span>
|
||||
<span>
|
||||
超期数: <strong className="is-blue">{PROJECT_SUMMARY.overdue}</strong>
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
styles={{ body: { padding: '16px 34px 17px' } }}
|
||||
>
|
||||
<StatisticCards data={STATISTIC_CARDS} />
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={<span className="institution-card-title">信息提醒</span>}
|
||||
size="small"
|
||||
className="institution-dashboard__alerts-card institution-panel-card"
|
||||
styles={{ body: { padding: '20px 24px' } }}
|
||||
>
|
||||
<InfoAlerts data={INFO_ALERTS} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="institution-dashboard__charts">
|
||||
<EnterpriseTypeChart data={ENTERPRISE_TYPE_STATS} />
|
||||
<ProjectTypeChart data={PROJECT_TYPE_DISTRIBUTION} total={PROJECT_TOTAL} />
|
||||
</div>
|
||||
|
||||
<ProjectCompletionTable data={PROJECT_COMPLETION_LIST} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* 机构端首页 - 模拟数据
|
||||
*/
|
||||
export const STATISTIC_CARDS = [
|
||||
{ key: 'contract', title: '项目合同签订', value: 5632, icon: 'FileDone', color: '#409eff', bgColor: '#409eff' },
|
||||
{ key: 'riskAnalysis', title: '待风险分析', value: 951, icon: 'LineChart', color: '#8b7cf6', bgColor: '#8b7cf6' },
|
||||
{ key: 'projectTeam', title: '待成立项目组', value: 5632, icon: 'Appstore', color: '#48d1bd', bgColor: '#48d1bd' },
|
||||
{ key: 'workPlan', title: '待制定工作计划', value: 5632, icon: 'FileProtect', color: '#ff9f43', bgColor: '#ff9f43' },
|
||||
{ key: 'initialEval', title: '待初提', value: 5632, icon: 'Safety', color: '#8b7cf6', bgColor: '#8b7cf6' },
|
||||
{ key: 'checklist', title: '待编制检查表', value: 456, icon: 'Profile', color: '#8b7cf6', bgColor: '#8b7cf6' },
|
||||
{ key: 'industryNotice', title: '待从业告知', value: 5632, icon: 'Notification', color: '#ffb12a', bgColor: '#ffb12a' },
|
||||
{ key: 'siteSurvey', title: '待现场勘查', value: 5632, icon: 'Read', color: '#8b7cf6', bgColor: '#8b7cf6' },
|
||||
{ key: 'processControl', title: '过程管控', value: 951, icon: 'Compass', color: '#409eff', bgColor: '#409eff' },
|
||||
{ key: 'archive', title: '归档', value: 651, icon: 'Database', color: '#08bea7', bgColor: '#08bea7' }
|
||||
];
|
||||
|
||||
export const INFO_ALERTS = [
|
||||
{ key: 'orgQualification', title: '机构资质到期', value: 5, icon: 'Star', color: '#ffca0a', bgColor: '#fff9e7' },
|
||||
{ key: 'personnelQualification', title: '人员资质到期', value: 35, icon: 'Audit', color: '#ff7a59', bgColor: '#fff3ef' },
|
||||
{ key: 'personnelResignation', title: '人员离岗信息', value: 56, icon: 'UserDelete', color: '#6395f9', bgColor: '#f1f6ff' }
|
||||
];
|
||||
|
||||
export const PROJECT_SUMMARY = {
|
||||
total: 5632,
|
||||
overdue: 56
|
||||
};
|
||||
|
||||
export const ENTERPRISE_TYPE_STATS = {
|
||||
categories: ['矿山开采业', '危险化学品行业', '烟花爆竹行业', '金属冶炼与加工', '建筑施工', '交通运输业', '消防重点单位', '特种设备相关企业', '涉爆粉尘企业'],
|
||||
projectCount: [42, 35, 33, 35, 24, 42, 36, 42, 32],
|
||||
amount: [52, 54, 29, 48, 39, 36, 56, 35, 45]
|
||||
};
|
||||
|
||||
export const PROJECT_TYPE_DISTRIBUTION = [
|
||||
{ name: '定期检测', value: 5054, color: '#3f7df4' },
|
||||
{ name: '现状评价', value: 1540, color: '#63c174' },
|
||||
{ name: '控制效果评价', value: 1600, color: '#ffa533' },
|
||||
{ name: '预评价', value: 900, color: '#ff694f' },
|
||||
{ name: '设计专篇', value: 430, color: '#23c6c8' },
|
||||
{ name: '委托检测', value: 330, color: '#55c653' }
|
||||
];
|
||||
|
||||
export const PROJECT_TOTAL = 9854;
|
||||
|
||||
export const PROJECT_COMPLETION_LIST = [
|
||||
{
|
||||
id: 1,
|
||||
projectName: '玉田县志达贸易有限公司蓝兴加油站、LNG加气设施合并项目',
|
||||
status: '检测完成',
|
||||
projectLeader: '梁永利',
|
||||
clientLeader: '赵天祥',
|
||||
startDate: '2025-5-4 16:12:23',
|
||||
endDate: '2025-5-4 16:12:23',
|
||||
acceptanceDate: '2025-5-4 16:12:23'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
projectName: '北京首钢铁合金有限公司迁安分公司包芯线生产线扩建项目',
|
||||
status: '检测完成',
|
||||
projectLeader: '梁永利',
|
||||
clientLeader: '赵天祥',
|
||||
startDate: '2025-5-4 16:12:23',
|
||||
endDate: '2025-5-4 16:12:23',
|
||||
acceptanceDate: '2025-5-4 16:12:23'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
projectName: '中特伟业科技有限公司沧州金固废回收利用项目',
|
||||
status: '检测完成',
|
||||
projectLeader: '梁永利',
|
||||
clientLeader: '赵天祥',
|
||||
startDate: '2025-5-4 16:12:23',
|
||||
endDate: '2025-5-4 16:12:23',
|
||||
acceptanceDate: '2025-5-4 16:12:23'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
projectName: '荣信钢铁有限公司整合重组装备更新一期工程项目安全预评价',
|
||||
status: '检测完成',
|
||||
projectLeader: '梁永利',
|
||||
clientLeader: '赵天祥',
|
||||
startDate: '2025-5-4 16:12:23',
|
||||
endDate: '2025-5-4 16:12:23',
|
||||
acceptanceDate: '2025-5-4 16:12:23'
|
||||
}
|
||||
];
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
DashboardOutlined,
|
||||
FileProtectOutlined,
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
LogoutOutlined,
|
||||
FileTextOutlined,
|
||||
KeyOutlined,
|
||||
HomeOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
const menuItems = [
|
||||
|
|
@ -26,6 +27,16 @@ const menuItems = [
|
|||
label: "监管端",
|
||||
icon: <DashboardOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "/certificate/container/Supervision/Dashboard",
|
||||
label: "首页",
|
||||
icon: <DashboardOutlined />,
|
||||
},
|
||||
{
|
||||
key: "/certificate/container/Supervision/Cockpit",
|
||||
label: "驾驶舱",
|
||||
icon: <BarChartOutlined />,
|
||||
},
|
||||
{
|
||||
key: "/certificate/container/Supervision/test2",
|
||||
label: "Test2 测试",
|
||||
|
|
@ -207,6 +218,11 @@ const menuItems = [
|
|||
label: "机构信息管理",
|
||||
icon: <BankOutlined />,
|
||||
},
|
||||
{
|
||||
key: "/certificate/container/EnterpriseInfo/MaterialReport",
|
||||
label: "资料填报",
|
||||
icon: <FileTextOutlined />,
|
||||
},
|
||||
{
|
||||
key: "/certificate/container/EnterpriseInfo/QualificationCert",
|
||||
label: "资质信息管理",
|
||||
|
|
@ -239,6 +255,18 @@ const menuItems = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "/certificate/container/Institution",
|
||||
label: "机构端",
|
||||
icon: <HomeOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "/certificate/container/Institution/Dashboard",
|
||||
label: "首页",
|
||||
icon: <DashboardOutlined />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "/certificate/container/Test",
|
||||
label: "测试页面",
|
||||
|
|
@ -312,3 +340,5 @@ export function getSelectedKeys(path) {
|
|||
}
|
||||
return match ? [match] : [];
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,306 @@
|
|||
.cockpit-page {
|
||||
min-height: 100vh;
|
||||
background: #010817;
|
||||
color: #d8eeff;
|
||||
overflow: auto;
|
||||
font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.cockpit-stage {
|
||||
width: 100%;
|
||||
min-width: 1000px;
|
||||
min-height: calc(100vh - 100px);
|
||||
aspect-ratio: 1400 / 786;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 50% 42%, rgba(22, 91, 177, .28), transparent 34%),
|
||||
radial-gradient(circle at 50% 88%, rgba(18, 112, 218, .18), transparent 22%),
|
||||
linear-gradient(180deg, #061c44 0%, #020b25 50%, #020817 100%);
|
||||
}
|
||||
|
||||
.cockpit-stage::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 46px 0 0;
|
||||
background:
|
||||
linear-gradient(rgba(54, 136, 255, .06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(54, 136, 255, .05) 1px, transparent 1px);
|
||||
background-size: 42px 42px;
|
||||
mask-image: radial-gradient(circle at center, #000 0%, transparent 68%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cockpit-header {
|
||||
height: 45px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid rgba(20, 133, 232, .35);
|
||||
background: linear-gradient(90deg, rgba(4, 20, 56, .2), rgba(8, 67, 133, .45), rgba(4, 20, 56, .2));
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.cockpit-header h1 {
|
||||
margin: 0;
|
||||
font-size: 27px;
|
||||
line-height: 45px;
|
||||
letter-spacing: 1px;
|
||||
color: #e7f7ff;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 0 10px rgba(31, 178, 255, .8), 0 0 22px rgba(31, 178, 255, .42);
|
||||
}
|
||||
|
||||
.cockpit-header__time {
|
||||
position: absolute;
|
||||
right: 22px;
|
||||
top: 0;
|
||||
height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
color: #dceeff;
|
||||
}
|
||||
|
||||
.cockpit-header__time .anticon { color: #1fc7ff; font-size: 19px; }
|
||||
|
||||
.cockpit-header__deco {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 330px;
|
||||
height: 45px;
|
||||
background: linear-gradient(90deg, transparent, rgba(18, 123, 228, .24), transparent);
|
||||
}
|
||||
|
||||
.cockpit-header__deco--left { left: 308px; transform: skewX(36deg); }
|
||||
.cockpit-header__deco--right { right: 308px; transform: skewX(-36deg); }
|
||||
|
||||
.cockpit-layout {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: calc(100% - 45px);
|
||||
min-height: 680px;
|
||||
padding: 16px 16px 14px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 28.5%) minmax(420px, 1fr) minmax(280px, 28.5%);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cockpit-left,
|
||||
.cockpit-right,
|
||||
.cockpit-center {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cockpit-panel {
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, rgba(7, 39, 89, .62), rgba(1, 13, 38, .58));
|
||||
border: 1px solid rgba(24, 132, 234, .34);
|
||||
box-shadow: inset 0 0 22px rgba(10, 87, 180, .22), 0 0 12px rgba(0, 87, 184, .18);
|
||||
}
|
||||
|
||||
.cockpit-panel::before,
|
||||
.cockpit-panel::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-color: #13ceff;
|
||||
border-style: solid;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cockpit-panel::before { left: -1px; top: -1px; border-width: 2px 0 0 2px; }
|
||||
.cockpit-panel::after { right: -1px; bottom: -1px; border-width: 0 2px 2px 0; }
|
||||
|
||||
.cockpit-panel__title {
|
||||
height: 28px;
|
||||
padding: 0 12px 0 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #dff8ff;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
background: linear-gradient(90deg, rgba(14, 109, 207, .55), rgba(8, 37, 82, .14));
|
||||
text-shadow: 0 0 8px rgba(24, 205, 255, .5);
|
||||
}
|
||||
|
||||
.cockpit-panel__title i {
|
||||
flex: 1;
|
||||
height: 10px;
|
||||
background: repeating-linear-gradient(90deg, transparent 0 10px, rgba(34, 207, 255, .56) 10px 12px);
|
||||
opacity: .65;
|
||||
}
|
||||
|
||||
.cockpit-panel__diamond {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
transform: rotate(45deg);
|
||||
background: #30dcff;
|
||||
box-shadow: 0 0 8px #30dcff;
|
||||
}
|
||||
|
||||
.cockpit-panel__body { padding: 12px 12px 10px; }
|
||||
.lifecycle-panel { flex: 1.1; min-height: 438px; }
|
||||
.risk-panel { flex: .9; min-height: 294px; }
|
||||
.todo-panel { height: 236px; }
|
||||
.line-panel { height: 222px; }
|
||||
.review-panel { flex: 1; min-height: 250px; }
|
||||
.process-panel { height: 176px; }
|
||||
|
||||
.life-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px 22px;
|
||||
padding: 8px 2px 16px;
|
||||
}
|
||||
|
||||
.life-stat { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.life-stat__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #bff4ff;
|
||||
font-size: 18px;
|
||||
border: 1px solid rgba(60, 201, 255, .45);
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(53, 198, 255, .32), rgba(11, 56, 120, .68));
|
||||
box-shadow: inset 0 0 14px rgba(46, 204, 255, .34);
|
||||
}
|
||||
|
||||
.life-stat p { margin: 0 0 3px; color: #cde9ff; font-size: 11px; white-space: normal; line-height: 1.1; overflow-wrap: anywhere; }
|
||||
.life-stat strong { color: #20caff; font-size: 13px; font-family: Arial, sans-serif; }
|
||||
.progress-list { padding: 2px 8px 10px; border-top: 1px solid rgba(54, 142, 219, .18); }
|
||||
.progress-row { height: 31px; display: grid; grid-template-columns: 122px 1fr 34px; align-items: center; gap: 8px; font-size: 11px; color: #e2f5ff; }
|
||||
.progress-track { height: 9px; border-radius: 999px; background: rgba(5, 35, 83, .88); overflow: hidden; box-shadow: inset 0 0 9px rgba(5, 17, 44, .9); }
|
||||
.progress-track i { display: block; height: 100%; border-radius: inherit; box-shadow: 0 0 8px currentColor; }
|
||||
.progress-row b { color: #e8f8ff; font-size: 11px; }
|
||||
.industry-chart { height: 178px; }
|
||||
|
||||
.risk-panel h3 { margin: 0 0 8px; color: #eaf9ff; font-size: 12px; font-weight: 700; }
|
||||
.risk-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.risk-item {
|
||||
min-height: 38px;
|
||||
padding: 7px 9px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
background: linear-gradient(90deg, rgba(8, 57, 118, .78), rgba(5, 26, 70, .65));
|
||||
border: 1px solid rgba(19, 159, 234, .3);
|
||||
}
|
||||
.risk-item:nth-child(3), .risk-item:nth-child(4) { border-color: rgba(255, 207, 43, .45); background: linear-gradient(90deg, rgba(116, 89, 15, .54), rgba(6, 32, 73, .62)); }
|
||||
.risk-item:nth-child(5) { grid-column: 1 / -1; }
|
||||
.risk-item span { color: #e8f7ff; font-size: 11px; line-height: 1.3; }
|
||||
.risk-item b { color: #21caff; font-size: 17px; }
|
||||
.risk-pie { height: 118px; }
|
||||
|
||||
.kpi-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; height: 58px; }
|
||||
.kpi-card {
|
||||
position: relative;
|
||||
padding: 8px 10px 8px 14px;
|
||||
background: linear-gradient(180deg, rgba(20, 134, 222, .72), rgba(4, 43, 96, .84));
|
||||
border: 1px solid rgba(37, 207, 255, .56);
|
||||
box-shadow: inset 0 0 20px rgba(42, 208, 255, .16), 0 0 10px rgba(0, 161, 255, .18);
|
||||
}
|
||||
.kpi-card::before { content: ""; position: absolute; top: -1px; left: 28px; width: 54px; height: 4px; background: repeating-linear-gradient(90deg, #2adfff 0 9px, transparent 9px 15px); }
|
||||
.kpi-card p { margin: 0 0 6px; text-align: center; color: #d6f7ff; font-weight: 700; font-size: 12px; }
|
||||
.kpi-card strong { font-size: 22px; color: #fff; letter-spacing: 1px; }
|
||||
.kpi-card span { float: right; margin-top: 7px; color: #d9ebff; font-size: 11px; }
|
||||
.kpi-card b { color: #fff; }
|
||||
|
||||
.map-shell { flex: 1; min-height: 456px; position: relative; }
|
||||
.map-chart { position: absolute; inset: 0 0 0; }
|
||||
.map-card {
|
||||
position: absolute;
|
||||
width: 170px;
|
||||
height: 56px;
|
||||
padding: 9px 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, rgba(0, 107, 221, .9), rgba(1, 71, 165, .86));
|
||||
border: 1px solid #19b9ff;
|
||||
box-shadow: inset 0 0 18px rgba(18, 195, 255, .38), 0 0 10px rgba(0, 132, 255, .34);
|
||||
}
|
||||
.map-card strong { display: block; font-size: 14px; margin-bottom: 5px; }
|
||||
.map-card span { font-size: 12px; }
|
||||
.map-card--1 { left: 45%; top: 45%; }
|
||||
.map-card--2 { right: 4%; top: 54%; }
|
||||
.map-card--3 { left: 42%; bottom: 13%; }
|
||||
.map-card--4 { right: 13%; bottom: 12%; }
|
||||
.map-legend { position: absolute; left: 36px; bottom: 34px; margin: 0; padding: 0; list-style: none; color: #d8ebff; font-size: 12px; }
|
||||
.map-legend li { margin: 7px 0; display: flex; align-items: center; gap: 8px; }
|
||||
.map-legend li::before { content: ""; width: 12px; height: 4px; background: #c9ec75; }
|
||||
.map-legend li:nth-child(2)::before { background: #ffe376; }
|
||||
.map-legend li:nth-child(3)::before { background: #e7a2d0; }
|
||||
.map-legend li:nth-child(4)::before { background: #ff9f75; }
|
||||
|
||||
.todo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px 10px; }
|
||||
.todo-item { display: grid; grid-template-columns: 34px 1fr; grid-template-rows: 18px 18px; align-items: center; column-gap: 8px; }
|
||||
.todo-item span { grid-row: 1 / 3; width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; color: #fff; font-size: 16px; background: radial-gradient(circle, rgba(73, 198, 255, .5), rgba(7, 51, 112, .95)); border: 1px solid rgba(69, 204, 255, .64); box-shadow: inset 0 0 15px rgba(53, 202, 255, .3); }
|
||||
.todo-item p { margin: 0; color: #d8edff; font-size: 11px; white-space: normal; line-height: 1.1; overflow-wrap: anywhere; }
|
||||
.todo-item b { color: #13c8ff; font-size: 15px; }
|
||||
.period-tabs { position: absolute; left: 14px; top: 42px; display: flex; z-index: 2; }
|
||||
.period-tabs span { width: 34px; height: 19px; display: grid; place-items: center; color: #d8f2ff; border: 1px solid rgba(46, 159, 232, .48); background: rgba(7, 37, 88, .6); font-size: 11px; }
|
||||
.period-tabs span:first-child { background: rgba(5, 126, 176, .5); }
|
||||
.line-chart { height: 176px; }
|
||||
|
||||
.review-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 11px 14px; }
|
||||
.review-stat { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.review-stat span { width: 37px; height: 37px; display: grid; place-items: center; color: #20d3ff; font-size: 17px; border-radius: 50%; background: radial-gradient(circle, rgba(26, 188, 255, .35), rgba(5, 37, 93, .9)); border: 2px solid rgba(35, 210, 255, .65); }
|
||||
.review-stat p { margin: 0; color: #e1f5ff; font-size: 12px; white-space: normal; line-height: 1.1; overflow-wrap: anywhere; }
|
||||
.review-stat b { color: #18c9ff; font-size: 15px; }
|
||||
.review-stat b.danger { color: #ff3e53; }
|
||||
.review-pie { height: 144px; }
|
||||
|
||||
.process-grid { height: 128px; display: grid; grid-template-columns: repeat(5, 1fr); grid-template-rows: repeat(2, 52px); gap: 18px 28px; align-content: center; }
|
||||
.process-node { position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; border: 1px solid #078bff; background: linear-gradient(180deg, rgba(12, 71, 150, .72), rgba(2, 24, 69, .85)); box-shadow: inset 0 0 18px rgba(13, 146, 255, .25); }
|
||||
.process-node strong { color: #16d3ff; font-size: 18px; line-height: 20px; }
|
||||
.process-node span { color: #fff; font-size: 12px; }
|
||||
.process-node i { position: absolute; right: -26px; top: 50%; width: 22px; height: 16px; transform: translateY(-50%); background: linear-gradient(90deg, transparent, rgba(25, 210, 255, .85)); clip-path: polygon(0 0, 70% 50%, 0 100%, 30% 50%); }
|
||||
.process-node:nth-child(5) i { right: 50%; top: 60px; transform: translateX(50%) rotate(90deg); }
|
||||
.process-node:nth-child(n+6) i { right: auto; left: -26px; transform: translateY(-50%) rotate(180deg); }
|
||||
.process-node:last-child i { display: none; }
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
.cockpit-stage { min-width: 1000px; }
|
||||
.cockpit-layout { grid-template-columns: minmax(276px, 28.5%) minmax(410px, 1fr) minmax(276px, 28.5%); gap: 10px; padding: 14px; }
|
||||
.map-card { width: 150px; }
|
||||
.cockpit-panel__body { padding: 10px; }
|
||||
.todo-grid { gap: 12px 8px; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.map-summary {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: 84px;
|
||||
width: 150px;
|
||||
padding: 10px 12px;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
color: #dff8ff;
|
||||
background: linear-gradient(180deg, rgba(1, 83, 178, .78), rgba(0, 31, 88, .84));
|
||||
border: 1px solid rgba(38, 215, 255, .7);
|
||||
box-shadow: inset 0 0 18px rgba(21, 185, 255, .3), 0 0 13px rgba(0, 132, 255, .35);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.map-summary strong {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.map-summary span {
|
||||
font-size: 12px;
|
||||
color: #d9f2ff;
|
||||
}
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import * as echarts from 'echarts';
|
||||
import {
|
||||
AuditOutlined,
|
||||
BellOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
ContainerOutlined,
|
||||
FileDoneOutlined,
|
||||
FileProtectOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
HourglassOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ScheduleOutlined,
|
||||
SnippetsOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './index.css';
|
||||
import { cockpitMockData } from './mockData';
|
||||
import chongqingGeoJson from './data/chongqingGeoJson.json';
|
||||
|
||||
echarts.registerMap('chongqing', chongqingGeoJson);
|
||||
|
||||
const iconMap = [
|
||||
FileDoneOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
SnippetsOutlined,
|
||||
ScheduleOutlined,
|
||||
ClockCircleOutlined,
|
||||
AuditOutlined,
|
||||
BellOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
ContainerOutlined,
|
||||
];
|
||||
|
||||
function EChart({ option, className, events }) {
|
||||
const chartRef = useRef(null);
|
||||
const instanceRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return undefined;
|
||||
instanceRef.current = echarts.init(chartRef.current, null, { renderer: 'svg' });
|
||||
|
||||
const resize = () => instanceRef.current && instanceRef.current.resize();
|
||||
const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
|
||||
if (observer) observer.observe(chartRef.current);
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
if (events) {
|
||||
Object.entries(events).forEach(([eventName, handler]) => instanceRef.current.on(eventName, handler));
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (events && instanceRef.current) {
|
||||
Object.entries(events).forEach(([eventName, handler]) => instanceRef.current.off(eventName, handler));
|
||||
}
|
||||
window.removeEventListener('resize', resize);
|
||||
if (observer) observer.disconnect();
|
||||
if (instanceRef.current) instanceRef.current.dispose();
|
||||
instanceRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (instanceRef.current) {
|
||||
instanceRef.current.setOption(option, true);
|
||||
}
|
||||
}, [option]);
|
||||
|
||||
return <div className={className} ref={chartRef} />;
|
||||
}
|
||||
|
||||
function ScreenHeader({ data }) {
|
||||
return (
|
||||
<header className="cockpit-header">
|
||||
<div className="cockpit-header__deco cockpit-header__deco--left" />
|
||||
<h1>{data.title}</h1>
|
||||
<div className="cockpit-header__time"><ClockCircleOutlined />{data.currentTime}</div>
|
||||
<div className="cockpit-header__deco cockpit-header__deco--right" />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function Panel({ title, children, className = '' }) {
|
||||
return (
|
||||
<section className={`cockpit-panel ${className}`}>
|
||||
<div className="cockpit-panel__title">
|
||||
<span className="cockpit-panel__diamond" />
|
||||
<span>{title}</span>
|
||||
<i />
|
||||
</div>
|
||||
<div className="cockpit-panel__body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function LifecyclePanel({ data }) {
|
||||
const barOption = useMemo(() => ({
|
||||
color: ['#1aa7ff', '#d7a92c'],
|
||||
grid: { left: 28, right: 10, top: 20, bottom: 42 },
|
||||
legend: { right: 8, top: 0, itemWidth: 10, itemHeight: 6, textStyle: { color: '#b9d8ff', fontSize: 10 } },
|
||||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(3, 25, 66, .92)', borderColor: '#1aa7ff', textStyle: { color: '#dff7ff' } },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.industry.map(item => item.name),
|
||||
axisLabel: { color: '#b9cfff', fontSize: 10, interval: 0, rotate: 0, width: 38, overflow: 'break' },
|
||||
axisLine: { lineStyle: { color: '#17416e' } },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', max: 220, splitNumber: 4,
|
||||
axisLabel: { color: '#b9cfff', fontSize: 10 },
|
||||
splitLine: { lineStyle: { color: 'rgba(78, 143, 219, .2)', type: 'dashed' } },
|
||||
},
|
||||
series: [
|
||||
{ name: '类型数', type: 'bar', stack: 'total', barWidth: 14, data: data.industry.map(item => item.rectification) },
|
||||
{ name: '机构数', type: 'bar', stack: 'total', barWidth: 14, data: data.industry.map(item => item.institution) },
|
||||
],
|
||||
}), [data.industry]);
|
||||
|
||||
return (
|
||||
<Panel title="资质全生命周期管理" className="lifecycle-panel">
|
||||
<div className="life-grid">
|
||||
{data.lifecycleStats.map((item, index) => (
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
<div className="progress-list">
|
||||
{data.progress.map(item => (
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<EChart className="industry-chart" option={barOption} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function RiskPanel({ data }) {
|
||||
const pieOption = useMemo(() => ({
|
||||
color: ['#5f8cff', '#e96fff', '#5fd8ae', '#b6a0ff'],
|
||||
tooltip: { trigger: 'item', backgroundColor: 'rgba(3, 25, 66, .92)', borderColor: '#1aa7ff', textStyle: { color: '#dff7ff' } },
|
||||
legend: { orient: 'vertical', right: 2, top: 'center', itemWidth: 10, itemHeight: 6, textStyle: { color: '#c6dcff', fontSize: 10 } },
|
||||
series: [{ type: 'pie', radius: ['46%', '70%'], center: ['30%', '55%'], avoidLabelOverlap: true, label: { show: false }, data: data.riskPie }],
|
||||
}), [data.riskPie]);
|
||||
|
||||
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>)}
|
||||
</div>
|
||||
<h3>备案机构违法触发统计</h3>
|
||||
<EChart className="risk-pie" option={pieOption} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function KpiCards({ data }) {
|
||||
return <div className="kpi-row">{data.kpis.map(item => <div className="kpi-card" key={item.title}><p>{item.title}</p><strong>{item.value}</strong><span>同比 <b>↗ {item.change}</b></span></div>)}</div>;
|
||||
}
|
||||
|
||||
function MapSection({ data }) {
|
||||
const [selectedAreaName, setSelectedAreaName] = useState('重庆市');
|
||||
const statMap = useMemo(() => new Map(data.mapAreaStats.map(item => [item.name, item])), [data.mapAreaStats]);
|
||||
const mapData = useMemo(() => {
|
||||
return (chongqingGeoJson.features || []).map((feature, index) => {
|
||||
const name = feature.properties && feature.properties.name;
|
||||
const stat = statMap.get(name) || {
|
||||
name,
|
||||
value: 18 + index * 7,
|
||||
projectCount: 18 + index * 7,
|
||||
hiddenDanger: 0,
|
||||
institutionCount: 0,
|
||||
rectificationRate: '100%',
|
||||
};
|
||||
return { ...stat, name };
|
||||
});
|
||||
}, [statMap]);
|
||||
const selectedArea = statMap.get(selectedAreaName) || mapData[0] || data.mapAreaStats[0];
|
||||
const values = mapData.map(item => Number(item.value) || 0);
|
||||
const maxValue = Math.max(...values, 100);
|
||||
|
||||
const mapOption = useMemo(() => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(2, 31, 82, .96)',
|
||||
borderColor: '#1aa7ff',
|
||||
borderWidth: 1,
|
||||
padding: [8, 10],
|
||||
textStyle: { color: '#eaf9ff', fontSize: 12 },
|
||||
formatter: params => {
|
||||
const item = params.data || statMap.get(params.name);
|
||||
if (!item) return params.name;
|
||||
return [
|
||||
`<strong>${params.name}</strong>`,
|
||||
`评价项目数:${item.projectCount || item.value || 0}`,
|
||||
`备案机构数:${item.institutionCount || 0}`,
|
||||
`隐患数量:${item.hiddenDanger || 0}`,
|
||||
`整改率:${item.rectificationRate || '-'}`,
|
||||
].join('<br/>');
|
||||
},
|
||||
},
|
||||
visualMap: {
|
||||
min: 0,
|
||||
max: maxValue,
|
||||
show: false,
|
||||
inRange: {
|
||||
color: ['#b9ef7c', '#ffe178', '#ffb16f', '#ff8d86', '#de8ff0'],
|
||||
},
|
||||
},
|
||||
geo: {
|
||||
map: 'chongqing',
|
||||
roam: true,
|
||||
zoom: 1.1,
|
||||
top: 10,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
selectedMode: 'single',
|
||||
itemStyle: {
|
||||
borderColor: '#31d8ff',
|
||||
borderWidth: 1.2,
|
||||
shadowBlur: 16,
|
||||
shadowColor: 'rgba(0, 179, 255, .52)',
|
||||
},
|
||||
emphasis: {
|
||||
label: { show: true, color: '#062142', fontWeight: 700 },
|
||||
itemStyle: { areaColor: '#49f0ce', borderColor: '#f8ffff', borderWidth: 1.8 },
|
||||
},
|
||||
select: {
|
||||
label: { show: true, color: '#061a35', fontWeight: 700 },
|
||||
itemStyle: { areaColor: '#31d6ff', borderColor: '#ffffff', borderWidth: 2.2 },
|
||||
},
|
||||
label: { show: true, color: '#244269', fontSize: 10 },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '区域项目统计',
|
||||
type: 'map',
|
||||
map: 'chongqing',
|
||||
geoIndex: 0,
|
||||
selectedMode: 'single',
|
||||
data: mapData.map(item => ({ ...item, selected: item.name === selectedAreaName })),
|
||||
},
|
||||
{
|
||||
name: '重点乡镇',
|
||||
type: 'effectScatter',
|
||||
coordinateSystem: 'geo',
|
||||
rippleEffect: { brushType: 'stroke', scale: 3.2 },
|
||||
symbolSize: value => Math.max(8, Math.min(18, value[2] / 4)),
|
||||
itemStyle: { color: '#12f4ff', shadowBlur: 10, shadowColor: '#12f4ff' },
|
||||
label: { show: false },
|
||||
data: data.mapScatterPoints.map(item => ({ name: item.name, value: [...item.coord, item.value] })),
|
||||
},
|
||||
],
|
||||
}), [data.mapScatterPoints, mapData, maxValue, selectedAreaName, statMap]);
|
||||
|
||||
const mapEvents = useMemo(() => ({
|
||||
click: params => {
|
||||
if (params && params.name) setSelectedAreaName(params.name);
|
||||
},
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<div className="map-shell">
|
||||
<EChart className="map-chart" option={mapOption} events={mapEvents} />
|
||||
{data.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 && selectedArea.name}</strong>
|
||||
<span>项目数:{selectedArea && (selectedArea.projectCount || selectedArea.value)}</span>
|
||||
<span>隐患:{selectedArea && selectedArea.hiddenDanger}</span>
|
||||
<span>整改率:{selectedArea && selectedArea.rectificationRate}</span>
|
||||
</div>
|
||||
<ul className="map-legend"><li>0-10</li><li>11-30</li><li>31-50</li><li>51-100</li></ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function TodoPanel({ data }) {
|
||||
return (
|
||||
<Panel title="执业全过程管控" className="todo-panel">
|
||||
<div className="todo-grid">
|
||||
{data.todo.map((item, index) => {
|
||||
const Icon = iconMap[index] || FileDoneOutlined;
|
||||
return <div className="todo-item" key={item.label}><span><Icon /></span><p>{item.label}</p><b>{item.value}</b></div>;
|
||||
})}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewLinePanel({ data }) {
|
||||
const lineOption = useMemo(() => ({
|
||||
color: ['#0da5ff', '#29e487'],
|
||||
grid: { left: 34, right: 12, top: 28, bottom: 32 },
|
||||
legend: { right: 0, top: 0, itemWidth: 14, itemHeight: 4, textStyle: { color: '#c6dcff', fontSize: 10 } },
|
||||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(3, 25, 66, .92)', borderColor: '#1aa7ff', textStyle: { color: '#dff7ff' } },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: data.reviewLine.xAxis, axisLabel: { color: '#c6dcff', fontSize: 10 }, axisLine: { lineStyle: { color: '#17416e' } }, axisTick: { show: false } },
|
||||
yAxis: { type: 'value', min: 0, max: 100, splitNumber: 5, axisLabel: { color: '#c6dcff', fontSize: 10 }, splitLine: { lineStyle: { color: 'rgba(78, 143, 219, .22)', type: 'dashed' } } },
|
||||
series: [
|
||||
{ name: '项目数', type: 'line', smooth: true, symbolSize: 5, areaStyle: { opacity: .22 }, data: data.reviewLine.project },
|
||||
{ name: '监督检查数', type: 'line', smooth: true, symbolSize: 5, areaStyle: { opacity: .14 }, data: data.reviewLine.supervision },
|
||||
],
|
||||
}), [data.reviewLine]);
|
||||
return <Panel title="评价类型趋势" className="line-panel"><div className="period-tabs"><span>年</span><span>季</span><span>月</span></div><EChart className="line-chart" option={lineOption} /></Panel>;
|
||||
}
|
||||
|
||||
function ReviewProgressPanel({ data }) {
|
||||
const pieOption = useMemo(() => ({
|
||||
color: ['#1677ff', '#00c089', '#ff7a1a', '#f7cb16', '#ab5fde'],
|
||||
tooltip: { trigger: 'item', backgroundColor: 'rgba(3, 25, 66, .92)', borderColor: '#1aa7ff', textStyle: { color: '#dff7ff' } },
|
||||
legend: { orient: 'vertical', right: 0, top: 'center', itemWidth: 10, itemHeight: 8, textStyle: { color: '#c6dcff', fontSize: 10 } },
|
||||
series: [{ type: 'pie', radius: ['32%', '68%'], center: ['28%', '56%'], label: { show: false }, data: data.reviewPie }],
|
||||
}), [data.reviewPie]);
|
||||
|
||||
return (
|
||||
<Panel title="复盘评估改进提效" className="review-panel">
|
||||
<div className="review-stats">{data.reviewProgress.map(item => <div className="review-stat" key={item.label}><span><FundProjectionScreenOutlined /></span><p>{item.label}:<b className={item.danger ? 'danger' : ''}>{item.value}</b></p></div>)}</div>
|
||||
<EChart className="review-pie" option={pieOption} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function ProcessPanel({ data }) {
|
||||
return (
|
||||
<Panel title="项目流程" className="process-panel">
|
||||
<div className="process-grid">{data.process.map((item, index) => <div className="process-node" key={item.label}><strong>{item.value}</strong><span>{item.label}</span>{index < data.process.length - 1 && <i />}</div>)}</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SupervisionCockpit() {
|
||||
const data = cockpitMockData;
|
||||
return (
|
||||
<div className="cockpit-page">
|
||||
<div className="cockpit-stage">
|
||||
<ScreenHeader data={data} />
|
||||
<main className="cockpit-layout">
|
||||
<aside className="cockpit-left"><LifecyclePanel data={data} /><RiskPanel data={data} /></aside>
|
||||
<section className="cockpit-center"><KpiCards data={data} /><MapSection data={data} /><ProcessPanel data={data} /></section>
|
||||
<aside className="cockpit-right"><TodoPanel data={data} /><ReviewLinePanel data={data} /><ReviewProgressPanel data={data} /></aside>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
export const cockpitMockData = {
|
||||
title: '重庆市安全评价在线监管一件事',
|
||||
currentTime: '2025年05月23日 12:30:30',
|
||||
lifecycleStats: [
|
||||
{ label: '本年新增备案机构数', value: 450 },
|
||||
{ label: '当前备案机构数', value: 265 },
|
||||
{ label: '本年注销机构数', value: 23 },
|
||||
{ label: '全部备案机构数', value: 46 },
|
||||
{ label: '退出备案评价师数', value: 85 },
|
||||
{ label: '当前备案评价师数', value: 125 },
|
||||
],
|
||||
progress: [
|
||||
{ label: '评价机构备案完成率', value: 95, color: '#21d8ff' },
|
||||
{ label: '备案机构开展业务率', value: 80, color: '#18f7d2' },
|
||||
{ label: '备案机构新增率', value: 62, color: '#ffd11a' },
|
||||
],
|
||||
industry: [
|
||||
{ name: '煤矿开采业', rectification: 30, institution: 135 },
|
||||
{ name: '金属', rectification: 72, institution: 125 },
|
||||
{ name: '非金属矿及其他矿', rectification: 35, institution: 140 },
|
||||
{ name: '陆地石油和天然气', rectification: 0, institution: 84 },
|
||||
{ name: '陆上油气管道运输', rectification: 100, institution: 200 },
|
||||
{ name: '石油加工业', rectification: 30, institution: 135 },
|
||||
{ name: '化学原料', rectification: 72, institution: 175 },
|
||||
{ name: '金属冶炼', rectification: 70, institution: 116 },
|
||||
],
|
||||
riskItems: [
|
||||
{ label: '重大违法失信数', value: 56 },
|
||||
{ label: '法人资质终止数', value: 13 },
|
||||
{ label: '资质证书有效期届满未延续数', value: 43 },
|
||||
{ label: '评价机构超资质范围执业数', value: 45 },
|
||||
{ label: '经营地址、法人等相关信息变更数', value: 24 },
|
||||
],
|
||||
riskPie: [
|
||||
{ name: '机构违法行为', value: 28 },
|
||||
{ name: '资质维持基础行为', value: 25 },
|
||||
{ name: '过程控制违规行为', value: 22 },
|
||||
{ name: '其他情况', value: 25 },
|
||||
],
|
||||
kpis: [
|
||||
{ title: '备案项目开展评价率', value: '56.3%', change: '3.5%' },
|
||||
{ title: '企业评价项目合格率', value: '98.5%', change: '5.6%' },
|
||||
{ title: '隐患整改率', value: '99.8%', change: '0.6%' },
|
||||
],
|
||||
mapAreaStats: [
|
||||
{ name: '重庆市', value: 128, projectCount: 985, hiddenDanger: 126, institutionCount: 265, rectificationRate: '99.8%' },
|
||||
{ name: '渝中区', value: 88, projectCount: 88, hiddenDanger: 12, institutionCount: 26, rectificationRate: '99.2%' },
|
||||
{ name: '江北区', value: 76, projectCount: 76, hiddenDanger: 9, institutionCount: 22, rectificationRate: '98.8%' },
|
||||
{ name: '南岸区', value: 69, projectCount: 69, hiddenDanger: 8, institutionCount: 18, rectificationRate: '98.5%' },
|
||||
{ name: '九龙坡区', value: 82, projectCount: 82, hiddenDanger: 11, institutionCount: 24, rectificationRate: '98.9%' },
|
||||
{ name: '沙坪坝区', value: 73, projectCount: 73, hiddenDanger: 10, institutionCount: 20, rectificationRate: '98.6%' },
|
||||
{ name: '两江新区', value: 94, projectCount: 94, hiddenDanger: 13, institutionCount: 28, rectificationRate: '99.4%' },
|
||||
{ name: '万州区', value: 65, projectCount: 65, hiddenDanger: 8, institutionCount: 16, rectificationRate: '97.9%' },
|
||||
{ name: '涪陵区', value: 58, projectCount: 58, hiddenDanger: 7, institutionCount: 14, rectificationRate: '98.1%' },
|
||||
{ name: '永川区', value: 61, projectCount: 61, hiddenDanger: 7, institutionCount: 15, rectificationRate: '98.3%' },
|
||||
{ name: '合川区', value: 54, projectCount: 54, hiddenDanger: 6, institutionCount: 13, rectificationRate: '97.8%' },
|
||||
{ name: '长寿区', value: 52, projectCount: 52, hiddenDanger: 6, institutionCount: 12, rectificationRate: '98.0%' },
|
||||
{ name: '璧山区', value: 48, projectCount: 48, hiddenDanger: 5, institutionCount: 11, rectificationRate: '98.7%' },
|
||||
{ name: '大足区', value: 44, projectCount: 44, hiddenDanger: 5, institutionCount: 10, rectificationRate: '97.6%' },
|
||||
{ name: '綦江区', value: 41, projectCount: 41, hiddenDanger: 4, institutionCount: 9, rectificationRate: '97.2%' },
|
||||
{ name: '奉节县', value: 36, projectCount: 36, hiddenDanger: 4, institutionCount: 8, rectificationRate: '96.9%' },
|
||||
],
|
||||
mapScatterPoints: [
|
||||
{ name: '渝中区', value: 88, coord: [106.568955, 29.552642] },
|
||||
{ name: '江北区', value: 76, coord: [106.574271, 29.606703] },
|
||||
{ name: '九龙坡区', value: 82, coord: [106.51107, 29.50197] },
|
||||
{ name: '万州区', value: 65, coord: [108.380246, 30.807807] },
|
||||
{ name: '涪陵区', value: 58, coord: [107.394905, 29.703652] },
|
||||
],
|
||||
todo: [
|
||||
{ label: '合同签订', value: 59 },
|
||||
{ label: '待风险分析', value: 58 },
|
||||
{ label: '待成立项目组', value: 126 },
|
||||
{ label: '待制定工作计划', value: 587 },
|
||||
{ label: '待初勘', value: 54 },
|
||||
{ label: '待编制检查表', value: 487 },
|
||||
{ label: '待从业告知', value: 25 },
|
||||
{ label: '待现场勘查', value: 14 },
|
||||
{ label: '过程管控', value: 156 },
|
||||
],
|
||||
reviewLine: {
|
||||
xAxis: ['定期检测', '现状评价', '控制效果评价', '预评价', '设计专篇', '委托检测'],
|
||||
project: [72, 48, 61, 57, 45, 94],
|
||||
supervision: [54, 60, 51, 38, 62, 55],
|
||||
},
|
||||
reviewProgress: [
|
||||
{ label: '现场检查数', value: 56 },
|
||||
{ label: '现场检查问题数', value: 5, danger: true },
|
||||
{ label: '项目复查数', value: 45 },
|
||||
{ label: '现场复查问题数', value: 14, danger: true },
|
||||
{ label: '检查机构数', value: 36 },
|
||||
{ label: '检查发现问题数', value: 4, danger: true },
|
||||
],
|
||||
reviewPie: [
|
||||
{ name: '评价执行超期未完成数', value: 38 },
|
||||
{ name: '评价现场检查确认风险隐患数', value: 12 },
|
||||
{ name: '复查评估报告数', value: 14 },
|
||||
{ name: '监管检查处罚数', value: 21 },
|
||||
{ name: '项目主要负责人变更数', value: 15 },
|
||||
],
|
||||
process: [
|
||||
{ label: '项目合同签订', value: 52 },
|
||||
{ label: '风险分析', value: 58 },
|
||||
{ label: '成立项目组', value: 59 },
|
||||
{ label: '制定工作计划', value: 98 },
|
||||
{ label: '初勘', value: 51 },
|
||||
{ label: '归档', value: 236 },
|
||||
{ label: '过程管控', value: 136 },
|
||||
{ label: '现场勘查', value: 145 },
|
||||
{ label: '从业告知', value: 278 },
|
||||
{ label: '编制检查表', value: 25 },
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
.supervision-dashboard-page {
|
||||
min-height: 100%;
|
||||
background: #f4f4f4;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.supervision-dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(760px, 2fr) minmax(360px, 1fr);
|
||||
gap: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard-left,
|
||||
.dashboard-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.supervision-home-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.supervision-home-card > h3,
|
||||
.overview-group > h3 {
|
||||
margin: 0 0 14px;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
min-height: 232px;
|
||||
padding: 14px 18px 18px;
|
||||
}
|
||||
|
||||
.overview-group + .overview-group {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.overview-item p {
|
||||
margin: 0 0 8px;
|
||||
color: #8b8f99;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.overview-item strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 21px;
|
||||
line-height: 24px;
|
||||
color: #24272d;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.overview-item div {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
color: #8b8f99;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.overview-item i {
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.overview-item i.up { color: #ff4d4f; }
|
||||
.overview-item i.down { color: #4caf50; }
|
||||
|
||||
.todo-home-card {
|
||||
height: 118px;
|
||||
padding: 14px 12px 16px;
|
||||
}
|
||||
|
||||
.todo-home-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.todo-home-item {
|
||||
height: 58px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.todo-home-item p {
|
||||
margin: 0 0 4px;
|
||||
color: #8b8f99;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.todo-home-item strong {
|
||||
color: #2d2f33;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.filing-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filing-card {
|
||||
height: 128px;
|
||||
padding: 14px 14px 18px;
|
||||
}
|
||||
|
||||
.filing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.filing-item {
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 0 18px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filing-item > span {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.filing-item p {
|
||||
margin: 0 0 3px;
|
||||
color: #555;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filing-item strong {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.flow-card {
|
||||
position: relative;
|
||||
min-height: 350px;
|
||||
padding: 16px 14px 14px;
|
||||
}
|
||||
|
||||
.region-select {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: 12px;
|
||||
width: 230px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 4px;
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.flow-summary {
|
||||
margin: 48px 14px 24px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 34px;
|
||||
padding: 0 16px;
|
||||
border-radius: 4px;
|
||||
background: #dff4ff;
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.flow-grid {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(110px, 1fr));
|
||||
gap: 36px 28px;
|
||||
padding: 0 14px 10px;
|
||||
}
|
||||
|
||||
.flow-node {
|
||||
position: relative;
|
||||
height: 62px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 12px;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 6px 18px rgba(31, 45, 61, .08);
|
||||
}
|
||||
|
||||
.flow-node > span {
|
||||
flex: 0 0 auto;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
font-size: 21px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.flow-node p {
|
||||
margin: 0 0 2px;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flow-node strong {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.flow-node-6 { grid-column: 1; grid-row: 2; }
|
||||
.flow-node-7 { grid-column: 2; grid-row: 2; }
|
||||
.flow-node-8 { grid-column: 3; grid-row: 2; }
|
||||
.flow-node-9 { grid-column: 4; grid-row: 2; }
|
||||
.flow-node-10 { grid-column: 5; grid-row: 2; }
|
||||
|
||||
.arrow-right,
|
||||
.arrow-left,
|
||||
.arrow-down {
|
||||
position: absolute;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.arrow-right {
|
||||
right: -28px;
|
||||
top: 22px;
|
||||
width: 28px;
|
||||
height: 18px;
|
||||
background: linear-gradient(90deg, #8ec6ff, #2f8cff);
|
||||
clip-path: polygon(0 35%, 60% 35%, 60% 0, 100% 50%, 60% 100%, 60% 65%, 0 65%);
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
left: -28px;
|
||||
top: 22px;
|
||||
width: 28px;
|
||||
height: 18px;
|
||||
background: linear-gradient(270deg, #8ec6ff, #2f8cff);
|
||||
clip-path: polygon(100% 35%, 40% 35%, 40% 0, 0 50%, 40% 100%, 40% 65%, 100% 65%);
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
left: 50%;
|
||||
bottom: -34px;
|
||||
width: 18px;
|
||||
height: 30px;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(180deg, #8ec6ff, #2f8cff);
|
||||
clip-path: polygon(35% 0, 65% 0, 65% 55%, 100% 55%, 50% 100%, 0 55%, 35% 55%);
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
padding: 14px 12px 10px;
|
||||
}
|
||||
|
||||
.line-home-card {
|
||||
height: 244px;
|
||||
}
|
||||
|
||||
.bar-home-card {
|
||||
flex: 1;
|
||||
min-height: 336px;
|
||||
}
|
||||
|
||||
.home-line-chart {
|
||||
height: 194px;
|
||||
}
|
||||
|
||||
.home-bar-chart {
|
||||
height: 286px;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.supervision-dashboard-grid {
|
||||
grid-template-columns: minmax(720px, 2fr) minmax(340px, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.supervision-home-card {
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.flow-grid {
|
||||
gap: 36px 26px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import * as echarts from 'echarts';
|
||||
import {
|
||||
AuditOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
FileDoneOutlined,
|
||||
FileSearchOutlined,
|
||||
FormOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
SoundOutlined,
|
||||
ReconciliationOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ScheduleOutlined,
|
||||
SnippetsOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './index.css';
|
||||
|
||||
const overviewGroups = [
|
||||
{
|
||||
title: '备案整体情况',
|
||||
items: [
|
||||
{ label: '已备案机构数', value: 985, yoy: '23%', qoq: '5%', yoyUp: true, qoqUp: false },
|
||||
{ label: '开发评价企业数', value: 5846, yoy: '3%', qoq: '1%', yoyUp: true, qoqUp: false },
|
||||
{ label: '开展业务机构数', value: 89512, yoy: '15%', qoq: '2%', yoyUp: true, qoqUp: false },
|
||||
{ label: '备案评价师数', value: 10005, yoy: '10%', qoq: '5%', yoyUp: false, qoqUp: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '本年数据统计',
|
||||
items: [
|
||||
{ label: '开展评价企业数', value: 523, yoy: '23%', qoq: '5%', yoyUp: true, qoqUp: false },
|
||||
{ label: '评价项目数', value: 5600, yoy: '3%', qoq: '1%', yoyUp: true, qoqUp: false },
|
||||
{ label: '评价服务机构数', value: 1200, yoy: '15%', qoq: '2%', yoyUp: true, qoqUp: false },
|
||||
{ label: '评价服务项目数', value: 2500, yoy: '10%', qoq: '5%', yoyUp: false, qoqUp: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = [
|
||||
{ label: '待审核', value: 956, color: '#f1f3ff' },
|
||||
{ label: '待审核', value: 548, color: '#d9f6fb' },
|
||||
{ label: '待审核', value: 1548, color: '#faf4dc' },
|
||||
];
|
||||
|
||||
const filingCards = [
|
||||
{ group: '本地备案机构数', label: '平台备案数', value: 2654, icon: SafetyCertificateOutlined, color: '#ffb739', bg: '#fffbe8' },
|
||||
{ group: '本地备案机构数', label: '完全备案确认', value: 4561, icon: CheckCircleOutlined, color: '#3aaef6', bg: '#eefaff' },
|
||||
{ group: '异地备案机构数', label: '平台备案数', value: 126, icon: SafetyCertificateOutlined, color: '#60ddc7', bg: '#ecfbf8' },
|
||||
{ group: '异地备案机构数', label: '完全备案确认', value: 254, icon: CheckCircleOutlined, color: '#928cf1', bg: '#f3f3ff' },
|
||||
];
|
||||
|
||||
const flowNodes = [
|
||||
{ label: '项目合同签订', value: 5632, icon: FileDoneOutlined, color: '#409eff' },
|
||||
{ label: '待风险分析', value: 951, icon: FundProjectionScreenOutlined, color: '#8d7cf6' },
|
||||
{ label: '待成立项目组', value: 5632, icon: SnippetsOutlined, color: '#43d2c5' },
|
||||
{ label: '待制定工作计划', value: 5632, icon: ScheduleOutlined, color: '#ff9f42' },
|
||||
{ label: '待初勘', value: 5632, icon: AuditOutlined, color: '#8b7df3' },
|
||||
{ label: '归档', value: 651, icon: ReconciliationOutlined, color: '#08c7ad' },
|
||||
{ label: '过程管控', value: 951, icon: ClockCircleOutlined, color: '#3ca6f5' },
|
||||
{ label: '待现场勘查', value: 5632, icon: FileSearchOutlined, color: '#8b7df3' },
|
||||
{ label: '待从业告知', value: 5632, icon: SoundOutlined, color: '#ffb327' },
|
||||
{ label: '待编制检查表', value: 456, icon: FormOutlined, color: '#8b7df3' },
|
||||
];
|
||||
|
||||
function EChart({ option, className }) {
|
||||
const ref = useRef(null);
|
||||
const chartRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return undefined;
|
||||
chartRef.current = echarts.init(ref.current, null, { renderer: 'svg' });
|
||||
const resize = () => chartRef.current && chartRef.current.resize();
|
||||
const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
|
||||
if (observer) observer.observe(ref.current);
|
||||
window.addEventListener('resize', resize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
if (observer) observer.disconnect();
|
||||
if (chartRef.current) chartRef.current.dispose();
|
||||
chartRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartRef.current) chartRef.current.setOption(option, true);
|
||||
}, [option]);
|
||||
|
||||
return <div className={className} ref={ref} />;
|
||||
}
|
||||
|
||||
function Card({ title, children, className = '' }) {
|
||||
return (
|
||||
<section className={`supervision-home-card ${className}`}>
|
||||
<h3>{title}</h3>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Trend({ label, value, up }) {
|
||||
return <span>{label} <i className={up ? 'up' : 'down'}>{up ? '▲' : '▼'}</i> {value}</span>;
|
||||
}
|
||||
|
||||
function Overview() {
|
||||
return (
|
||||
<Card title="备案整体情况" className="overview-card">
|
||||
{overviewGroups.map(group => (
|
||||
<div className="overview-group" key={group.title}>
|
||||
{group.title !== '备案整体情况' && <h3>{group.title}</h3>}
|
||||
<div className="overview-grid">
|
||||
{group.items.map(item => (
|
||||
<div className="overview-item" key={item.label}>
|
||||
<p>{item.label}</p>
|
||||
<strong>{item.value}</strong>
|
||||
<div><Trend label="同比" value={item.yoy} up={item.yoyUp} /><Trend label="环比" value={item.qoq} up={item.qoqUp} /></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Todo() {
|
||||
return (
|
||||
<Card title="待办事项" className="todo-home-card">
|
||||
<div className="todo-home-grid">
|
||||
{todoItems.map((item, index) => <div className="todo-home-item" style={{ background: item.color }} key={`${item.label}-${index}`}><p>{item.label}</p><strong>{item.value}</strong></div>)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function FilingGroup({ title, cards }) {
|
||||
return (
|
||||
<Card title={title} className="filing-card">
|
||||
<div className="filing-grid">
|
||||
{cards.map(item => {
|
||||
const Icon = item.icon;
|
||||
return <div className="filing-item" style={{ background: item.bg }} key={item.label}><span style={{ background: item.color }}><Icon /></span><div><p>{item.label}</p><strong style={{ color: item.color }}>{item.value}</strong></div></div>;
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function FlowStatus() {
|
||||
return (
|
||||
<Card title="当前评价状态状态统计" className="flow-card">
|
||||
<div className="region-select">请选择县区 <span>⌄</span></div>
|
||||
<div className="flow-summary"><span>评价项目合计:228</span><span>评价完成:185</span><span>正在进行中:21</span><span>项目中断:22</span></div>
|
||||
<div className="flow-grid">
|
||||
{flowNodes.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return <div className={`flow-node flow-node-${index + 1}`} key={item.label}><span style={{ background: item.color }}><Icon /></span><div><p>{item.label}</p><strong>{item.value}</strong></div>{index < 4 && <i className="arrow-right" />}{index === 4 && <i className="arrow-down" />}{index > 5 && <i className="arrow-left" />}</div>;
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function LineChart() {
|
||||
const option = useMemo(() => ({
|
||||
color: ['#409eff', '#ff9f43'],
|
||||
grid: { left: 38, right: 16, top: 34, bottom: 28 },
|
||||
legend: { right: 8, top: 0, itemWidth: 14, itemHeight: 6, textStyle: { color: '#555', fontSize: 12 } },
|
||||
tooltip: { trigger: 'axis', backgroundColor: '#fff', borderColor: '#edf0f5', textStyle: { color: '#333' } },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: ['定期检测', '现状评价', '控制效果评价', '预评价', '设计专篇', '委托检测'], axisLabel: { color: '#555', fontSize: 11 }, axisTick: { show: false }, axisLine: { lineStyle: { color: '#e6eaf0' } } },
|
||||
yAxis: { type: 'value', max: 200, splitNumber: 4, axisLabel: { color: '#777' }, splitLine: { lineStyle: { color: '#eef1f5', type: 'dashed' } } },
|
||||
series: [
|
||||
{ name: '企业数', type: 'line', smooth: true, symbolSize: 6, data: [5, 38, 52, 33, 92, 78, 120, 116, 132], areaStyle: { color: 'rgba(64,158,255,.08)' } },
|
||||
{ name: '评价数', type: 'line', smooth: true, symbolSize: 6, data: [4, 47, 31, 42, 72, 75, 64, 140, 121] },
|
||||
],
|
||||
}), []);
|
||||
return <Card title="评价类型分类" className="chart-card line-home-card"><EChart className="home-line-chart" option={option} /></Card>;
|
||||
}
|
||||
|
||||
function BarChart() {
|
||||
const option = useMemo(() => ({
|
||||
color: ['#5b9bff', '#ffc75b'],
|
||||
grid: { left: 38, right: 18, top: 38, bottom: 36 },
|
||||
legend: { right: 8, top: 0, itemWidth: 12, itemHeight: 12, textStyle: { color: '#555', fontSize: 12 } },
|
||||
tooltip: { trigger: 'axis', backgroundColor: '#fff', borderColor: '#edf0f5', textStyle: { color: '#333' } },
|
||||
xAxis: { type: 'category', data: ['矿山开采业', '危险化学品行业', '烟花爆竹行业', '金属冶炼与加工', '建筑施工', '交通运输业'], axisLabel: { color: '#555', fontSize: 11, interval: 0 }, axisTick: { show: false }, axisLine: { lineStyle: { color: '#e6eaf0' } } },
|
||||
yAxis: { type: 'value', max: 200, splitNumber: 4, axisLabel: { color: '#777' }, splitLine: { lineStyle: { color: '#eef1f5' } } },
|
||||
series: [
|
||||
{ name: '企业数', type: 'bar', barWidth: 12, data: [158, 76, 112, 108, 158, 143] },
|
||||
{ name: '评价数', type: 'bar', barWidth: 12, data: [92, 136, 57, 136, 123, 121] },
|
||||
],
|
||||
}), []);
|
||||
return <Card title="企业类型评价统计" className="chart-card bar-home-card"><EChart className="home-bar-chart" option={option} /></Card>;
|
||||
}
|
||||
|
||||
export default function SupervisionDashboard() {
|
||||
return (
|
||||
<div className="supervision-dashboard-page">
|
||||
<div className="supervision-dashboard-grid">
|
||||
<div className="dashboard-left">
|
||||
<Overview />
|
||||
<div className="filing-row">
|
||||
<FilingGroup title="本地备案机构数" cards={filingCards.slice(0, 2)} />
|
||||
<FilingGroup title="异地备案机构数" cards={filingCards.slice(2)} />
|
||||
</div>
|
||||
<FlowStatus />
|
||||
</div>
|
||||
<div className="dashboard-right">
|
||||
<Todo />
|
||||
<LineChart />
|
||||
<BarChart />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue