port WeiXian 文件按块拆分

master
LiuJiaNan 2026-07-22 09:10:11 +08:00
parent c68862b954
commit 1ce00bc82c
10 changed files with 468 additions and 428 deletions

View File

@ -60,3 +60,15 @@ export const getFirePointDeviceList = declareRequest(
"portXiaoFangFirePointDeviceListLoading",
"Post > @/fireCheck/firePoint/deviceList",
);
export const getEightWorkInfoScreenStatusStatistics = declareRequest(
"portWeiXianEightWorkInfoScreenStatusStatisticsLoading",
"Post > @/eightwork/eightworkInfo/screenStatusStatistics",
);
export const getEightWorkInfoScreenWorkRecord = declareRequest(
"portWeiXianEightWorkInfoScreenWorkRecordLoading",
"Post > @/eightwork/eightworkInfo/screenWorkRecord",
);
export const getEightWorkInfoScreenWorkTrend = declareRequest(
"portWeiXianEightWorkInfoScreenWorkTrendLoading",
"Post > @/eightwork/eightworkInfo/screenWorkTrend",
);

View File

@ -0,0 +1,69 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { useContext, useEffect, useState } from "react";
import SeamlessScroll from "zy-react-library/components/SeamlessScroll";
import { NS_BI_STATISTICS } from "~/enumerate/namespace";
import Panel from "~/pages/Container/Map/components/Content/port/Panel";
import { Context } from "~/pages/Container/Map/js/context";
import "./index.less";
const rankStyleNames = ["first", "second", "third"];
/** 负责查询并展示危险作业记录及其排行样式。 */
function WorkRecordPanel(props) {
const { portArea } = useContext(Context);
const [records, setRecords] = useState([]);
useEffect(() => {
const loadRecords = async () => {
const { data = [] } = await props.getEightWorkInfoScreenWorkRecord({
portArea,
pageIndex: 1,
pageSize: 9999,
});
setRecords(data);
};
loadRecords();
}, []);
return (
<Panel title="安全作业记录" className="port-danger-work__record">
<div className="port-danger-work-record__content">
<div className="port-danger-work-record__table">
<div className="port-danger-work-record__row">
<div className="port-danger-work-record__cell">序号</div>
<div className="port-danger-work-record__cell">作业类型</div>
<div className="port-danger-work-record__cell">状态</div>
<div className="port-danger-work-record__cell">下一步操作人</div>
</div>
<div className="port-danger-work-record__table-body">
<SeamlessScroll list={records} step={0.5}>
{records.map((item, index) => (
<div key={index} className="port-danger-work-record__row">
<div className="port-danger-work-record__cell">
<div
className={`port-danger-work-record__rank port-danger-work-record__rank--${rankStyleNames[index] || "default"}`}
>
{index + 1}
</div>
</div>
<div className="port-danger-work-record__cell">
{item.workTypeName}
</div>
<div className="port-danger-work-record__cell">
{item.statusName}
</div>
<div className="port-danger-work-record__cell">
{item.nextStepName}
</div>
</div>
))}
</SeamlessScroll>
</div>
</div>
</div>
</Panel>
);
}
export default Connect([NS_BI_STATISTICS], true)(WorkRecordPanel);

View File

@ -0,0 +1,71 @@
.port-danger-work__record {
margin-top: 10px;
background-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.8)
);
}
.port-danger-work-record__content {
padding: 10px;
border: 1px solid;
border-top: none;
border-image: linear-gradient(
to bottom,
rgba(58, 122, 149, 0),
rgba(58, 122, 149, 1)
)
1;
}
.port-danger-work-record__table {
margin-top: 5px;
}
.port-danger-work-record__table-body {
height: 400px;
overflow-y: hidden;
}
.port-danger-work-record__row {
display: flex;
}
.port-danger-work-record__row:nth-child(odd) {
background-color: rgba(42, 86, 158, 0.53);
}
.port-danger-work-record__cell {
flex: 1;
padding: 5px;
color: #fff;
font-size: 12px;
text-align: center;
}
.port-danger-work-record__cell:first-child {
flex: none;
flex-basis: 50px;
text-align: left;
}
.port-danger-work-record__cell:nth-child(2) {
flex-basis: 20%;
}
.port-danger-work-record__rank {
display: flex;
align-items: center;
justify-content: center;
width: 21px;
height: 15px;
margin: auto;
color: #fff;
font-size: 11px;
line-height: 1;
border-radius: 2px;
}
.port-danger-work-record__rank--first {
background: linear-gradient(135deg, #ff6d73, #f23f47);
}
.port-danger-work-record__rank--second {
background: linear-gradient(135deg, #ffb856, #ff871f);
}
.port-danger-work-record__rank--third {
background: linear-gradient(135deg, #ffe477, #f6be1b);
}
.port-danger-work-record__rank--default {
background: linear-gradient(135deg, #5a8fdc, #3367b4);
}

View File

@ -0,0 +1,78 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { Spin } from "antd";
import { useContext, useEffect, useState } from "react";
import CountUp from "react-countup";
import iconBackground from "~/assets/images/map_bi/content/icobg.png";
import applyIcon from "~/assets/images/map_bi/content/icon27.png";
import approvingIcon from "~/assets/images/map_bi/content/icon28.png";
import archivedIcon from "~/assets/images/map_bi/content/icon29.png";
import labelBackground from "~/assets/images/map_bi/content/label.png";
import { NS_BI_STATISTICS } from "~/enumerate/namespace";
import Panel from "~/pages/Container/Map/components/Content/port/Panel";
import { Context } from "~/pages/Container/Map/js/context";
import "./index.less";
const defaultStatistics = [
{ image: applyIcon, label: "申请数", count: 0 },
{ image: approvingIcon, label: "审批中", count: 0 },
{ image: archivedIcon, label: "归档数", count: 0 },
];
/** 负责查询并展示危险作业申请、审批和归档状态统计。 */
function WorkStatusPanel(props) {
const { portArea } = useContext(Context);
const [statistics, setStatistics] = useState(defaultStatistics);
useEffect(() => {
const loadStatistics = async () => {
const { data = {} } = await props.getEightWorkInfoScreenStatusStatistics({
portArea,
});
const counts = [
data.appliedCount,
data.approvingCount,
data.archivedCount,
];
setStatistics(
defaultStatistics.map((item, index) => ({
...item,
count: counts[index],
})),
);
};
loadStatistics();
}, []);
return (
<Panel title="安全作业状态统计" className="port-danger-work__status">
<Spin
spinning={
props.biStatistics
.portWeiXianEightWorkInfoScreenStatusStatisticsLoading
}
>
<div className="port-danger-work-status__options">
{statistics.map((item) => (
<div key={item.label} className="port-danger-work-status__option">
<div
className="port-danger-work-status__icon"
style={{ backgroundImage: `url(${iconBackground})` }}
>
<img src={item.image} alt="" />
</div>
<div
className="port-danger-work-status__label"
style={{ backgroundImage: `url(${labelBackground})` }}
>
{item.label}<CountUp end={+item.count} />
</div>
</div>
))}
</div>
</Spin>
</Panel>
);
}
export default Connect([NS_BI_STATISTICS], true)(WorkStatusPanel);

View File

@ -0,0 +1,62 @@
.port-danger-work__status {
background-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.8)
);
}
.port-danger-work-status__options {
display: flex;
justify-content: space-between;
padding: 13px 15px;
border: 1px solid;
border-top: none;
border-image: linear-gradient(
to bottom,
rgba(58, 122, 149, 0),
rgba(58, 122, 149, 1)
)
1;
}
.port-danger-work-status__option {
display: flex;
flex-direction: column;
align-items: center;
}
.port-danger-work-status__icon {
width: 70px;
height: 70px;
padding-top: 13px;
text-align: center;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.port-danger-work-status__icon img {
width: 25px;
height: 26px;
animation: portDangerWorkStatusSlideY 2s infinite;
}
.port-danger-work-status__label {
display: flex;
justify-content: center;
width: 120px;
height: 24px;
margin-top: 6px;
color: #fff;
font-size: 12px;
line-height: 25px;
text-align: center;
background-repeat: no-repeat;
background-size: 100% 100%;
}
@keyframes portDangerWorkStatusSlideY {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
100% {
transform: translateY(0);
}
}

View File

@ -0,0 +1,143 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { useMount } from "ahooks";
import { Spin } from "antd";
import * as echarts from "echarts";
import { useContext, useRef } from "react";
import { NS_BI_STATISTICS } from "~/enumerate/namespace";
import Panel from "~/pages/Container/Map/components/Content/port/Panel";
import { Context } from "~/pages/Container/Map/js/context";
import "./index.less";
/** 将接口的作业趋势记录转换为图表横轴、图例与序列数据。 */
function formatWorkTrendData(data) {
const workDates = [...new Set(data.map(item => item.workDate))];
const workTypes = [
...new Map(data.map(item => [item.workType, item.workTypeName])),
];
return {
xData: workDates,
date: workTypes.map(([workType]) =>
data
.filter(item => item.workType === workType)
.map(item => Number(item.count)),
),
name: workTypes.map(([, workTypeName]) => workTypeName),
};
}
/** 负责初始化安全作业趋势图表并在卸载时释放图表实例。 */
function WorkTrendPanel(props) {
const { portArea } = useContext(Context);
const chartRef = useRef(null);
const chartInstance = useRef(null);
const initEcharts = (mainRef, chartInstance, data) => {
const colors = [
"29,128,219",
"1,245,255",
"228,187,45",
"45,118,228",
"188,45,228",
"228,72,45",
"58,45,228",
"221,44,67",
];
const series = data.date.map((seriesData, index) => ({
name: data.name[index],
type: "line",
symbol: "circle",
showAllSymbol: true,
symbolSize: 0,
smooth: true,
lineStyle: { width: 2, color: `rgba(${colors[index]},1)` },
itemStyle: { color: `rgba(${colors[index]},1)` },
tooltip: { show: true },
areaStyle: {
color: new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{ offset: 0, color: `rgba(${colors[index]},0.3)` },
{ offset: 1, color: `rgba(${colors[index]},0)` },
],
false,
),
shadowColor: "rgba(25,163,223, 0.5)",
shadowBlur: 20,
},
data: seriesData,
}));
chartInstance.current = echarts.init(mainRef.current);
chartInstance.current.setOption({
tooltip: { trigger: "axis" },
legend: {
top: "0",
align: "left",
type: "plain",
data: data.name,
textStyle: { color: "#fff", fontSize: 12 },
itemWidth: 18,
},
grid: { top: "20%", left: "10%", right: "5%", bottom: "15%" },
xAxis: [
{
type: "category",
boundaryGap: false,
axisLine: { show: true, lineStyle: { color: "#15578b" } },
axisLabel: {
color: "#fff",
fontSize: 12,
formatter(value) {
return value.substring(5);
},
},
splitLine: { show: false, lineStyle: { color: "#fff" } },
axisTick: { show: false },
data: data.xData,
},
],
yAxis: [
{
min: 0,
splitLine: {
show: true,
lineStyle: { color: "rgba(221,221,221,0.1)" },
},
axisLine: { show: false, lineStyle: { color: "#fff" } },
axisLabel: { show: true, color: "#fff", padding: 10 },
},
],
series,
});
};
useMount(() => {
const loadTrendData = async () => {
const { data = [] } = await props.getEightWorkInfoScreenWorkTrend({
portArea,
});
initEcharts(chartRef, chartInstance, formatWorkTrendData(data));
};
loadTrendData();
return () => chartInstance.current?.dispose();
});
return (
<Panel title="安全作业情况统计" className="port-danger-work__trend">
<div className="port-danger-work-trend__content">
<Spin
spinning={
props.biStatistics.portWeiXianEightWorkInfoScreenWorkTrendLoading
}
>
<div ref={chartRef} className="port-danger-work-trend__chart" />
</Spin>
</div>
</Panel>
);
}
export default Connect([NS_BI_STATISTICS], true)(WorkTrendPanel);

View File

@ -0,0 +1,23 @@
.port-danger-work__trend {
margin-top: 10px;
background-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.8)
);
}
.port-danger-work-trend__content {
padding: 10px 15px;
border: 1px solid;
border-top: none;
border-image: linear-gradient(
to bottom,
rgba(58, 122, 149, 0),
rgba(58, 122, 149, 1)
)
1;
}
.port-danger-work-trend__chart {
width: 100%;
height: 240px;
}

View File

@ -1,140 +0,0 @@
import * as echarts from "echarts";
export const initEcharts1 = (mainRef, chartInstance, data) => {
const colors = [
"29,128,219",
"1,245,255",
"228,187,45",
"45,118,228",
"188,45,228",
"228,72,45",
"58,45,228",
"221,44,67",
];
const date = data.date;
const name = data.name;
const series = [];
for (let i = 0; i < date.length; i++) {
series.push({
name: name[i],
type: "line",
symbol: "circle",
showAllSymbol: true,
symbolSize: 0,
smooth: true,
lineStyle: {
width: 2,
color: `rgba(${colors[i]},1)`,
},
itemStyle: {
color: `rgba(${colors[i]},1)`,
},
tooltip: {
show: true,
},
areaStyle: {
color: new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{
offset: 0,
color: `rgba(${colors[i]},0.3)`,
},
{
offset: 1,
color: `rgba(${colors[i]},0)`,
},
],
false,
),
shadowColor: "rgba(25,163,223, 0.5)",
shadowBlur: 20,
},
data: date[i],
});
}
chartInstance.current = echarts.init(mainRef.current);
const option = {
tooltip: {
trigger: "axis",
},
legend: {
top: "0",
align: "left",
type: "plain",
data: name,
textStyle: {
color: "#fff",
fontSize: 12,
},
itemWidth: 18,
},
grid: {
top: "20%",
left: "10%",
right: "5%",
bottom: "15%",
},
xAxis: [
{
type: "category",
boundaryGap: false,
axisLine: {
show: true,
lineStyle: {
color: "#15578b",
},
},
axisLabel: {
color: "#fff",
fontSize: 12,
formatter(data) {
return data.substring(5);
},
},
splitLine: {
show: false,
lineStyle: {
color: "#fff",
},
},
axisTick: {
show: false,
},
data: data.clickDate,
},
],
yAxis: [
{
min: 0,
splitLine: {
show: true,
lineStyle: {
color: "rgba(221,221,221,0.1)",
},
},
axisLine: {
show: false,
lineStyle: {
color: "#fff",
},
},
axisLabel: {
show: true,
color: "#fff",
padding: 10,
},
},
],
series,
};
chartInstance.current.setOption(option);
};

View File

@ -1,137 +1,16 @@
import { useMount } from "ahooks";
import { useContext, useRef, useState } from "react";
import CountUp from "react-countup";
import anquan_bg1 from "~/assets/images/map_bi/content/anquan_bg1.png";
import anquan_bg2 from "~/assets/images/map_bi/content/anquan_bg2.png";
import anquan_bg3 from "~/assets/images/map_bi/content/anquan_bg3.png";
import anquan_bg4 from "~/assets/images/map_bi/content/anquan_bg4.png";
import icobg from "~/assets/images/map_bi/content/icobg.png";
import icon27 from "~/assets/images/map_bi/content/icon27.png";
import icon28 from "~/assets/images/map_bi/content/icon28.png";
import icon29 from "~/assets/images/map_bi/content/icon29.png";
import label from "~/assets/images/map_bi/content/label.png";
import Title from "~/pages/Container/Map/components/Content/port/Title";
import { Context } from "~/pages/Container/Map/js/context";
import { initEcharts1 } from "./echarts";
import "./index.less";
const WeiXian = () => {
const { currentPort, portArea, currentBranchOffice } = useContext(Context);
const [block3List, setBlock3List] = useState([
{ WORK_TYPE: "动火作业", STATUS_NAME: "现场负责人", USER_NAME: "李长健" },
{ WORK_TYPE: "动火作业", STATUS_NAME: "延时监火", USER_NAME: "运行乙班" },
{ WORK_TYPE: "动火作业", STATUS_NAME: "延时监火", USER_NAME: "卸车部运行乙班" },
{ WORK_TYPE: "动火作业", STATUS_NAME: "动火后", USER_NAME: "王启龙" },
{ WORK_TYPE: "动火作业", STATUS_NAME: "延时监火", USER_NAME: "装船部" },
{ WORK_TYPE: "动火作业", STATUS_NAME: "延时监火", USER_NAME: "装船队机修乙班" },
{ WORK_TYPE: "动火作业", STATUS_NAME: "动火后", USER_NAME: "彭俊伟" },
{ WORK_TYPE: "动火作业", STATUS_NAME: "延时监火", USER_NAME: "装船队机修乙班" },
{ WORK_TYPE: "动火作业", STATUS_NAME: "延时监火", USER_NAME: "运行乙班" },
{ WORK_TYPE: "动火作业", STATUS_NAME: "延时监火", USER_NAME: "卸车队运行甲班" },
]);
const [block1OptionsList, setBlock1OptionsList] = useState([
{ img: icon27, label: "申请数", count: 0 },
{ img: icon28, label: "审批中", count: 0 },
{ img: icon29, label: "归档数", count: 0 },
]);
const main1Ref = useRef(null);
const chartInstance = useRef(null);
useMount(() => {
initEcharts1(main1Ref, chartInstance, {
clickDate: ["2025-12-31", "2026-01-01", "2026-01-02", "2026-01-03", "2026-01-04"],
date: [
["7.0", "0.0", "0.0", "0.0", "0.0"],
["0.0", "0.0", "0.0", "0.0", "0.0"],
["0.0", "0.0", "0.0", "0.0", "0.0"],
["0.0", "0.0", "0.0", "0.0", "0.0"],
["0.0", "0.0", "0.0", "0.0", "0.0"],
["0.0", "0.0", "0.0", "0.0", "0.0"],
["0.0", "0.0", "0.0", "0.0", "0.0"],
["0.0", "0.0", "0.0", "0.0", "0.0"],
],
name: ["动火作业", "盲板作业", "有限空间作业", "高处作业", "吊装作业", "临时用电", "动土作业", "断路作业"],
});
return () => {
if (chartInstance.current) {
chartInstance.current.dispose();
chartInstance.current = null;
}
};
});
import WorkRecordPanel from "./WorkRecordPanel";
import WorkStatusPanel from "./WorkStatusPanel";
import WorkTrendPanel from "./WorkTrendPanel";
/** 危险作业模块仅负责按展示顺序组合业务区块。 */
function WeiXian() {
return (
<div className="port_weixian">
<div className="block1">
<Title title="安全作业状态统计" />
<div className="options">
{block1OptionsList.map((item, index) => (
<div key={index} className="option">
<div className="circular" style={{ backgroundImage: `url(${icobg})` }}>
<img src={item.img} alt="" />
</div>
<div className="label" style={{ backgroundImage: `url(${label})` }}>
{item.label}
<CountUp end={+item.count} />
</div>
</div>
))}
</div>
</div>
<div className="block2">
<Title title="安全作业情况统计" />
<div className="content">
<div ref={main1Ref} style={{ width: "100%", height: "240px" }} />
</div>
</div>
<div className="block3">
<Title title="安全作业记录" />
<div className="content">
<div className="table">
<div className="tr">
<div className="td">序号</div>
<div className="td">作业类型</div>
<div className="td">状态</div>
<div className="td">下一步操作人</div>
</div>
{block3List && block3List.length > 0
? (
block3List.map((item, index) => (
<div key={index} className="tr">
<div className="td">
<div
className="bg"
style={{
...(index === 0 && { backgroundImage: `url(${anquan_bg1})` }),
...(index === 1 && { backgroundImage: `url(${anquan_bg2})` }),
...(index === 2 && { backgroundImage: `url(${anquan_bg3})` }),
...(index > 2 && { backgroundImage: `url(${anquan_bg4})` }),
}}
>
{index + 1}
</div>
</div>
<div className="td">{item.WORK_TYPE}</div>
<div className="td">{item.STATUS_NAME}</div>
<div className="td">{item.USER_NAME}</div>
</div>
))
)
: (
<div className="tr">
<div className="empty">暂无数据</div>
</div>
)}
</div>
</div>
</div>
<div className="port-danger-work">
<WorkStatusPanel />
<WorkTrendPanel />
<WorkRecordPanel />
</div>
);
};
}
export default WeiXian;

View File

@ -1,157 +0,0 @@
.port_weixian {
.block1 {
background-image: linear-gradient(to bottom,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.8));
.options {
display: flex;
justify-content: space-between;
padding: 13px 15px;
border: 1px solid;
border-image: linear-gradient(to bottom,
rgba(58, 122, 149, 0),
rgba(58, 122, 149, 1)) 1;
border-top: none;
.option {
display: flex;
flex-direction: column;
align-items: center;
.circular {
background-size: 100% 100%;
background-repeat: no-repeat;
width: 70px;
height: 70px;
text-align: center;
padding-top: 13px;
img {
width: 25px;
height: 26px;
animation: weixianSlideY 2s infinite;
}
}
.label {
display: flex;
justify-content: center;
background-size: 100% 100%;
background-repeat: no-repeat;
width: 120px;
height: 24px;
margin-top: 6px;
font-size: 12px;
line-height: 25px;
text-align: center;
color: #fff;
}
}
}
}
.block2 {
margin-top: 10px;
background-image: linear-gradient(to bottom,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.8));
.content {
padding: 10px 15px;
border: 1px solid;
border-image: linear-gradient(to bottom,
rgba(58, 122, 149, 0),
rgba(58, 122, 149, 1)) 1;
border-top: none;
}
}
.block3 {
margin-top: 10px;
background-image: linear-gradient(to bottom,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.8));
.content {
border: 1px solid;
border-image: linear-gradient(to bottom,
rgba(58, 122, 149, 0),
rgba(58, 122, 149, 1)) 1;
border-top: none;
padding: 10px;
.options {
display: flex;
justify-content: flex-end;
.title {
cursor: pointer;
}
}
.table {
margin-top: 5px;
.scroll{
height: 400px;
overflow-y: hidden;
}
.tr {
display: flex;
&:nth-child(odd) {
background-color: rgba(42, 86, 158, 0.53);
}
.td {
flex: 1;
text-align: center;
font-size: 12px;
color: #fff;
padding: 5px;
&:nth-child(1) {
flex: none;
flex-basis: 50px;
text-align: left;
}
&:nth-child(2) {
flex-basis: 20%;
}
.bg {
background-size: 100% 100%;
background-repeat: no-repeat;
width: 21px;
height: 15px;
margin: auto;
line-height: 15px;
padding-left: 4px;
}
}
&:nth-child(n + 11) {
.bg {
padding-left: 0;
}
}
}
}
}
}
@keyframes weixianSlideY {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
100% {
transform: translateY(0);
}
}
}